Skip to content

feat: recommending safe packages #1284

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 43 commits into from
Aug 11, 2021
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
37d2d7d
update: to latest stable version of black`
peb-peb Jul 17, 2021
d303f2e
fix: tests for test_helper_script (#1255)
peb-peb Jul 19, 2021
375b4f4
docs: remove adding checker names for tests (#1256)
BreadGenie Jul 19, 2021
eedfbe1
test: change way pytest is run in CI (#1251)
terriko Jul 19, 2021
ad6d2b1
refactor(scanner): scan strings without splitting the lines (#1227)
BreadGenie Jul 21, 2021
31ca3b0
fix: extract apk packages for alpine and android (#1258)
imsahil007 Jul 21, 2021
1a5ce74
feat(checker): Add sudo checker (#1259)
imsahil007 Jul 21, 2021
117e325
Fix yaml and toml tests in test_config (#1253)
terriko Jul 21, 2021
77e9021
feat(checker): Add Lua Checker (#1257)
BreadGenie Jul 21, 2021
2338715
feat(checker): Add mdadm Checker (#1261)
BreadGenie Jul 21, 2021
6288d52
feat(checker): Add mtr Checker (#1263)
BreadGenie Jul 21, 2021
105d2b8
feat(checker): Add TrouSerS checker (#1266)
BreadGenie Jul 21, 2021
c7615dd
feat: Add recommdended dev tools list (#1212)
terriko Jul 21, 2021
5941d75
feat(checker): Add gnome-shell checker (#1200)
BreadGenie Jul 22, 2021
3ddbb06
fix: rename development requirements file for Snyk (#1272)
terriko Jul 26, 2021
ec8a220
fix: condensed downloads (#1274)
BreadGenie Jul 27, 2021
8ca0c91
refactor: helper script for is_executable() and parse_string() (#1246)
peb-peb Jul 27, 2021
3171111
feat(checker): Add open-vm-tools Checker (#1275)
BreadGenie Jul 27, 2021
6bbc0af
feat(checker): Add nano Checker (#1277)
BreadGenie Jul 27, 2021
227ddf8
feat(checker): Add pscs-lite Checker (#1280)
BreadGenie Jul 27, 2021
c4ec775
feat(checker): Add poppler Checker (#1283)
BreadGenie Jul 27, 2021
eca217c
fix(cvedb): skip reject cve entries (#1282)
imsahil007 Jul 27, 2021
53c0ad8
feat(cvedb): Add NVD CVE Retrieval API (#1218)
imsahil007 Jul 27, 2021
1ba41bd
fix: too many SQL variables (#1278) (#1279)
anthonyharrison Jul 27, 2021
4b4fbf5
Create SECURITY.md
terriko Jul 28, 2021
167fe44
fix: mark failing nvd test as skipped (for now) (#1286)
terriko Jul 28, 2021
f5e3d9f
feat(checker): Add pigz Checker (#1288)
BreadGenie Jul 29, 2021
7898bc8
update main with upstream
peb-peb Aug 3, 2021
2ca3ac9
Merge branch 'main' of https://github.com/intel/cve-bin-tool
peb-peb Aug 4, 2021
b5f60b9
feat: recommending safe packages
peb-peb Jul 27, 2021
11da696
feat: done console output
peb-peb Aug 3, 2021
e9fc05d
fix: black and isort
peb-peb Aug 3, 2021
f03ab41
rebase branch with main
peb-peb Aug 4, 2021
9c7d359
refactor`: "--verbose" -> "--affected-versions"
peb-peb Aug 4, 2021
1a455a2
fix: output color schema
peb-peb Aug 4, 2021
ca10757
refactor: added changes suggested by @Molkree
peb-peb Aug 5, 2021
e8a493f
refactor: console affected-versions output
peb-peb Aug 8, 2021
3641b07
fix: console affected-versions output
peb-peb Aug 8, 2021
01a7c5f
test: added tests for test_output_engine
peb-peb Aug 8, 2021
bb1e80c
refactor: update `format_version_range` docstring
peb-peb Aug 8, 2021
c61e9d0
fix: flake8
peb-peb Aug 8, 2021
709fd69
refactor: output_engine/console
peb-peb Aug 9, 2021
0e1ca78
refactor: cve_scanner
peb-peb Aug 9, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions cve_bin_tool/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ def main(argv=None):
action="store",
help="add a unique tag to differentiate between multiple intermediate reports",
)
output_group.add_argument(
"--affected-versions",
action="count",
default=0,
help="Lists versions of product affected by a given CVE (to facilitate upgrades)",
)
parser.add_argument("-V", "--version", action="version", version=VERSION)
parser.add_argument(
"-u",
Expand Down Expand Up @@ -233,6 +239,7 @@ def main(argv=None):
"tag": "",
"merge": None,
"nvd": "json",
"affected_versions": 0,
}

with ErrorHandler(mode=ErrorMode.NoTrace):
Expand Down Expand Up @@ -438,6 +445,7 @@ def main(argv=None):
# Creates a Object for OutputEngine
output = OutputEngine(
all_cve_data=cve_scanner.all_cve_data,
all_cve_version_info=cve_scanner.all_cve_version_info,
scanned_dir=args["directory"],
filename=args["output_file"],
themes_dir=args["html_theme"],
Expand All @@ -449,6 +457,7 @@ def main(argv=None):
is_report=args["report"],
append=args["append"],
merge_report=merged_reports,
affected_versions=args["affected_versions"],
)

if not args["quiet"]:
Expand Down
12 changes: 11 additions & 1 deletion cve_bin_tool/cve_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from cve_bin_tool.linkify import linkify_cve
from cve_bin_tool.log import LOGGER
from cve_bin_tool.theme import cve_theme
from cve_bin_tool.util import CVE, CVEData, ProductInfo
from cve_bin_tool.util import CVE, CVEData, ProductInfo, VersionInfo


class CVEScanner:
Expand All @@ -31,6 +31,7 @@ class CVEScanner:
products_with_cve: int
products_without_cve: int
all_cve_data: DefaultDict[ProductInfo, CVEData]
all_cve_version_info: DefaultDict[str, VersionInfo]

RANGE_UNSET: str = ""
dbname: str = os.path.join(DISK_LOCATION_DEFAULT, DBNAME)
Expand All @@ -49,6 +50,7 @@ def __init__(
self.products_with_cve = 0
self.products_without_cve = 0
self.all_cve_data = defaultdict(CVEData)
self.all_cve_version_info = defaultdict(VersionInfo)

def get_cves(self, product_info: ProductInfo, triage_data: TriageData):
"""Get CVEs against a specific version of a product.
Expand Down Expand Up @@ -121,6 +123,7 @@ def get_cves(self, product_info: ProductInfo, triage_data: TriageData):
and parsed_version >= parse_version(versionStartIncluding)
):
passes_start = True

if (
versionStartExcluding is not self.RANGE_UNSET
and parsed_version > parse_version(versionStartExcluding)
Expand All @@ -147,6 +150,7 @@ def get_cves(self, product_info: ProductInfo, triage_data: TriageData):
and parsed_version < parse_version(versionEndExcluding)
):
passes_end = True

if (
versionEndIncluding is self.RANGE_UNSET
and versionEndExcluding is self.RANGE_UNSET
Expand All @@ -156,6 +160,12 @@ def get_cves(self, product_info: ProductInfo, triage_data: TriageData):
# if it fits into both ends of the range, add the cve number
if passes_start and passes_end:
cve_list.append(cve_number)
self.all_cve_version_info[cve_number] = VersionInfo(
start_including=versionStartIncluding,
start_excluding=versionStartExcluding,
end_including=versionEndIncluding,
end_excluding=versionEndExcluding,
)

# Go through and get all the severities
if cve_list:
Expand Down
13 changes: 11 additions & 2 deletions cve_bin_tool/output_engine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,9 +271,11 @@ def __init__(
is_report: bool = False,
append: Union[str, bool] = False,
merge_report: Union[None, List[str]] = None,
affected_versions: int = 0,
all_cve_version_info=None,
):
self.logger = logger or LOGGER.getChild(self.__class__.__name__)
self.all_cve_data = all_cve_data
self.all_cve_version_info = all_cve_version_info
self.scanned_dir = scanned_dir
self.filename = os.path.abspath(filename) if filename else ""
self.products_with_cve = products_with_cve
Expand All @@ -285,6 +287,8 @@ def __init__(
self.append = append
self.tag = tag
self.merge_report = merge_report
self.affected_versions = affected_versions
self.all_cve_data = all_cve_data

def output_cves(self, outfile, output_type="console"):
"""Output a list of CVEs
Expand Down Expand Up @@ -317,7 +321,12 @@ def output_cves(self, outfile, output_type="console"):
outfile,
)
else: # console, or anything else that is unrecognised
output_console(self.all_cve_data, self.time_of_last_update)
output_console(
self.all_cve_data,
self.all_cve_version_info,
self.time_of_last_update,
self.affected_versions,
)

if isinstance(self.append, str):
save_intermediate(
Expand Down
124 changes: 109 additions & 15 deletions cve_bin_tool/output_engine/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,58 @@
from ..input_engine import Remarks
from ..linkify import linkify_cve
from ..theme import cve_theme
from ..util import ProductInfo
from ..util import ProductInfo, VersionInfo


def format_version_range(
start_including, start_excluding, end_including, end_excluding
):
"""
formats version info to desirable output
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
formats version info to desirable output
Format version info to desirable output

Just to be consistent with another function in this module


Example:
```
format_version_info('2.2.8', '', '2.2.11', '') => "[2.2.8 - 2.2.11]"
format_version_info('2.2.8', '', '', '2.2.11') => "[2.2.8 - 2.2.11)"
format_version_info('', '2.2.8', '2.2.11', '') => "(2.2.8 - 2.2.11]"
format_version_info('', '2.2.8', '', '2.2.11') => "(2.2.8 - 2.2.11])"
format_version_info('2.2.8', '', '', '') => ">= 2.2.8"
format_version_info('', '2.2.8', '', '') => "> 2.2.8"
format_version_info('', '', '2.2.11', '') => "<= 2.2.11"
format_version_info('', '', '', '2.2.11') => "< 2.2.11"
format_version_infor([]) => "-"
```

Reference for Interval terminologies: https://en.wikipedia.org/wiki/Interval_(mathematics)
"""

versions = (start_including, start_excluding, end_including, end_excluding)
if versions:
# should refactor to use python 3.10's "Structural Pattern Matching"
if start_including and end_including:
return f"[{start_including} - {end_including}]"
elif start_including and end_excluding:
return f"[{start_including} - {end_excluding})"
elif start_excluding and end_including:
return f"({start_excluding} - {end_including}]"
elif start_excluding and end_excluding:
return f"({start_excluding} - {end_excluding})"
elif start_including:
return f">= {start_including}"
elif start_excluding:
return f"> {start_excluding}"
elif end_including:
return f"<= {end_including}"
elif end_excluding:
return f"< {end_excluding}"
return "-"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can anyone think of a better approach for this? OR we could just wait for python 3.10 and add the "Structural Pattern Matching" (Maybe we should open a "good first issue" issue for this as a reminder for future)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Waiting for Python 3.10 is not an option as cve-bin-tool supports all currently supported Python versions. So 3.9 will go out of support in 2025, only then 3.10 will become the minimum supported version (of course if cve-bin-tool continues to support all versions).

I would still rewrite it a bit because if versions check is useless (tuple of empty strings evaluates to True):

def format_version_range(version_info: VersionInfo) -> str:
    (start_including, start_excluding, end_including, end_excluding) = version_info
    if start_including and end_including:
        return f"[{start_including} - {end_including}]"
    if start_including and end_excluding:
        return f"[{start_including} - {end_excluding})"
    if start_excluding and end_including:
        return f"({start_excluding} - {end_including}]"
    if start_excluding and end_excluding:
        return f"({start_excluding} - {end_excluding})"
    if start_including:
        return f">= {start_including}"
    if start_excluding:
        return f"> {start_excluding}"
    if end_including:
        return f"<= {end_including}"
    if end_excluding:
        return f"< {end_excluding}"
    return "-"



def output_console(
all_cve_data: Dict[ProductInfo, CVEData],
all_cve_version_info: Dict[str, VersionInfo],
time_of_last_update,
affected_versions: int,
console=Console(theme=cve_theme),
):
"""Output list of CVEs in a tabular format with color support"""
Expand Down Expand Up @@ -65,6 +111,35 @@ def output_console(
"cvss_version": cve.cvss_version,
}
)
if affected_versions != 0:
try:
start_including = dict(all_cve_version_info)[
cve.cve_number
].start_including
start_excluding = dict(all_cve_version_info)[
cve.cve_number
].start_excluding
end_including = dict(all_cve_version_info)[
cve.cve_number
].end_including
end_excluding = dict(all_cve_version_info)[
cve.cve_number
].end_excluding
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all, all_cve_version_info is already a dictionary so you don't need to convert it to dict again.
Second, just use unpacking:

Suggested change
start_including = dict(all_cve_version_info)[
cve.cve_number
].start_including
start_excluding = dict(all_cve_version_info)[
cve.cve_number
].start_excluding
end_including = dict(all_cve_version_info)[
cve.cve_number
].end_including
end_excluding = dict(all_cve_version_info)[
cve.cve_number
].end_excluding
(
start_including,
start_excluding,
end_including,
end_excluding,
) = all_cve_version_info[cve.cve_number]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or go further and:

  1. Add default values "" to your VersionInfo class.
  2. Change format_version_range to accept VersionInfo class.
  3. Rewrite this whole if like this:
            if affected_versions != 0:
                try:
                    version_info = all_cve_version_info[cve.cve_number]
                except KeyError:  # TODO: handle 'UNKNOWN' and some cves more cleanly
                    version_info = VersionInfo()
                cve_by_remarks[cve.remarks][-1].update(
                    {"affected_versions": format_version_range(version_info)}
                )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was using all_cve_version_info with defaultdict and was facing issues with it and this was the solution I decided to go with (it never occurred to me that I could just use dict here) :(

        if affected_versions != 0:
            try:
                version_info = all_cve_version_info[cve.cve_number]
            except KeyError:  # TODO: handle 'UNKNOWN' and some cves more cleanly
                version_info = VersionInfo()
            cve_by_remarks[cve.remarks][-1].update(
                {"affected_versions": format_version_range(version_info)}
            )

This was a very clean change :)
But, can we do something better? (OR can we remove # TODO: handle 'UNKNOWN' and some cves more cleanly)

except KeyError: # TODO: handle 'UNKNOWN' and some cves more cleanly
start_including = ""
start_excluding = ""
end_including = ""
end_excluding = ""
cve_by_remarks[cve.remarks][-1].update(
{
"affected_versions": format_version_range(
start_including,
start_excluding,
end_including,
end_excluding,
)
}
)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote a comment for this, but am not able to find it now. So, I'm writing it again :(

Can we do something else (instead of try... except block) to handle the UNKNOWN and some cves which are not found in the all_cve_version_info dictionary?

For UNKNOWN, maybe we could could add a key: value pair of "UNKNOWN": VersionInfo('', '', '', '') at the start/end of this dictionary.

But, when some cves are not found in the all_cve_version_info, what could be a more clean/absolute way of handling both these error?

I'm facing this issue when running it over wireshark-1.10.14-25.el7.i686.rpm and currently I get this:

(cve_env) peb@ooo:/cve-bin-tool$ python3 -m cve_bin_tool.cli -u never --affected-versions ../test/wireshark-1.10.14-25.el7.i686.rpm
┏━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓
┃ Vendor    ┃ Product   ┃ Version ┃ CVE Number     ┃ Severity ┃ Score (CVSS Version) ┃ Affected Versions ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩
│ wireshark │ wireshark │ 1.10.14 │ CVE-2015-3182  │ MEDIUM   │ 5.5 (v3)             │ -                 │
│ wireshark │ wireshark │ 1.10.14 │ CVE-2015-3814  │ MEDIUM   │ 5 (v2)               │ -                 │
│ wireshark │ wireshark │ 1.10.14 │ CVE-2017-17935 │ HIGH     │ 7.5 (v3)             │ <= 2.2.11         │
│ wireshark │ wireshark │ 1.10.14 │ CVE-2017-17997 │ HIGH     │ 7.5 (v3)             │ <= 2.2.11         │
│ wireshark │ wireshark │ 1.10.14 │ CVE-2017-6014  │ HIGH     │ 7.5 (v3)             │ <= 2.2.4          │
│ wireshark │ wireshark │ 1.10.14 │ CVE-2018-14438 │ HIGH     │ 7.5 (v3)             │ <= 2.6.2          │
│ wireshark │ wireshark │ 1.10.14 │ CVE-2018-6836  │ CRITICAL │ 9.8 (v3)             │ <= 2.4.4          │
│ wireshark │ wireshark │ 1.10.14 │ CVE-2020-26575 │ HIGH     │ 7.5 (v3)             │ <= 3.2.7          │
└───────────┴───────────┴─────────┴────────────────┴──────────┴──────────────────────┴───────────────────┘

here, all_cve_version_info returns

defaultdict(<class 'cve_bin_tool.util.VersionInfo'>, 
  {
    'CVE-2017-17935': VersionInfo(start_including='', start_excluding='', end_including='2.2.11', end_excluding=''), 
    'CVE-2017-17997': VersionInfo(start_including='', start_excluding='', end_including='2.2.11', end_excluding=''), 
    'CVE-2017-6014': VersionInfo(start_including='', start_excluding='', end_including='2.2.4', end_excluding=''), 
    'CVE-2018-14438': VersionInfo(start_including='', start_excluding='', end_including='2.6.2', end_excluding=''), 
    'CVE-2018-6836': VersionInfo(start_including='', start_excluding='', end_including='2.4.4', end_excluding=''), 
    'CVE-2020-26575': VersionInfo(start_including='', start_excluding='', end_including='3.2.7', end_excluding='')
  }
)

Here, all_cve_version_info doesn't contain CVE-2015-3182 and CVE-2015-3814.


for remarks in sorted(cve_by_remarks):
color = remarks_colors[remarks]
Expand All @@ -79,24 +154,43 @@ def output_console(
table.add_column("CVE Number")
table.add_column("Severity")
table.add_column("Score (CVSS Version)")
if affected_versions != 0:
table.add_column("Affected Versions")
# table.add_column("CVSS Version")

for cve_data in cve_by_remarks[remarks]:
color = cve_data["severity"].lower()
table.add_row(
Text.styled(cve_data["vendor"], color),
Text.styled(cve_data["product"], color),
Text.styled(cve_data["version"], color),
linkify_cve(Text.styled(cve_data["cve_number"], color)),
Text.styled(cve_data["severity"], color),
Text.styled(
str(cve_data["score"])
+ " (v"
+ str(cve_data["cvss_version"])
+ ")",
color,
),
)
if affected_versions != 0:
table.add_row(
Text.styled(cve_data["vendor"], color),
Text.styled(cve_data["product"], color),
Text.styled(cve_data["version"], color),
linkify_cve(Text.styled(cve_data["cve_number"], color)),
Text.styled(cve_data["severity"], color),
Text.styled(
str(cve_data["score"])
+ " (v"
+ str(cve_data["cvss_version"])
+ ")",
color,
),
Text.styled(cve_data["affected_versions"], color),
)
else:
table.add_row(
Text.styled(cve_data["vendor"], color),
Text.styled(cve_data["product"], color),
Text.styled(cve_data["version"], color),
linkify_cve(Text.styled(cve_data["cve_number"], color)),
Text.styled(cve_data["severity"], color),
Text.styled(
str(cve_data["score"])
+ " (v"
+ str(cve_data["cvss_version"])
+ ")",
color,
),
)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to deduplicate this? 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah

            cells = [
                Text.styled(cve_data["vendor"], color),
                Text.styled(cve_data["product"], color),
                Text.styled(cve_data["version"], color),
                linkify_cve(Text.styled(cve_data["cve_number"], color)),
                Text.styled(cve_data["severity"], color),
                Text.styled(
                    f"{cve_data['score']} (v{cve_data['cvss_version']})", color
                ),
            ]
            if affected_versions != 0:
                cells.append(Text.styled(cve_data["affected_versions"], color))
            table.add_row(*cells)

# Print the table to the console
console.print(table)
for cve_data in cve_by_remarks[remarks]:
Expand Down
7 changes: 7 additions & 0 deletions cve_bin_tool/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ class ProductInfo(NamedTuple):
version: str


class VersionInfo(NamedTuple):
start_including: str
start_excluding: str
end_including: str
end_excluding: str


class CVEData(defaultdict):
def __missing__(self, key):
if key == "cves":
Expand Down
Loading