Skip to content
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

feat: provide multiple output formats for a single scan (#1724) #1740

Merged
merged 15 commits into from
Jul 11, 2022
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ Note that you will need to obtain a copy of the vulnerability data before the to

The CVE Binary Tool provides console-based output by default. If you wish to provide another format, you can specify this and a filename on the command line using `--format`. The valid formats are CSV, JSON, console, HTML and PDF. The output filename can be specified using the `--output-file` flag.

You can also specify multiple output formats by using comma (',') as separator:

```bash
cve-bin-tool file -f csv,json,html -o report
```

Note: Please don't use spaces between comma (',') and the output formats.

The reported vulnerabilities can additionally be reported in the
Vulnerability Exchange (VEX) format by specifying `--vex` command line option.
The generated VEX file can then be used as an `--input-file` to support
Expand Down Expand Up @@ -183,6 +191,8 @@ Usage:
provide custom theme directory for HTML Report
-f {csv,json,console,html,pdf}, --format {csv,json,console,html,pdf}
update output format (default: console)
specify multiple output formats by using comma (',') as a separator
note: don't use spaces between comma (',') and the output formats.
-c CVSS, --cvss CVSS minimum CVSS score (as integer in range 0 to 10) to
report (default: 0)
-S {low,medium,high,critical}, --severity {low,medium,high,critical}
Expand Down
31 changes: 23 additions & 8 deletions cve_bin_tool/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def main(argv=None):
f'Available checkers: {", ".join(VersionScanner.available_checkers())}'
)
+ "\n\nPlease disclose issues responsibly!",
formatter_class=argparse.RawDescriptionHelpFormatter,
formatter_class=argparse.RawTextHelpFormatter,
)

nvd_database_group = parser.add_argument_group(
Expand Down Expand Up @@ -187,8 +187,14 @@ def main(argv=None):
"-f",
"--format",
action="store",
choices=["csv", "json", "console", "html", "pdf"],
help="update output format (default: console)",
help=textwrap.dedent(
"""\
update output format (default: console)
specify multiple output formats by using comma (',') as a separator
note: don't use spaces between comma (',') and the output formats.
"""
),
metavar="{csv,json,console,html,pdf}",
default="console",
)
output_group.add_argument(
Expand Down Expand Up @@ -509,14 +515,23 @@ def main(argv=None):

cvedb_orig.remove_cache_backup()

output_formats = set(args["format"].split(","))
output_formats = [output_format.strip() for output_format in output_formats]
extensions = ["csv", "json", "console", "html", "pdf"]
for output_format in output_formats:
if output_format not in extensions:
LOGGER.error(
f"Argument -f/--format: invalid choice: {output_format} (choose from 'csv', 'json', 'console', 'html', 'pdf')"
)
return -1

# Check for PDF support
output_format = args["format"]
if output_format == "pdf" and importlib.util.find_spec("reportlab") is None:
LOGGER.info("PDF output not available. Default to console.")
if "pdf" in output_formats and importlib.util.find_spec("reportlab") is None:
LOGGER.info("PDF output not available.")
LOGGER.info(
"If you want to produce PDF output, please install reportlab using pip install reportlab"
)
output_format = "console"
output_formats.remove("pdf")

merged_reports = None
if args["merge"]:
Expand Down Expand Up @@ -710,7 +725,7 @@ def main(argv=None):
)

if not args["quiet"]:
output.output_file(output_format)
output.output_file_wrapper(output_formats)
if args["backport_fix"] or args["available_fix"]:
distro_info = args["backport_fix"] or args["available_fix"]
is_backport = True if args["backport_fix"] else False
Expand Down
15 changes: 11 additions & 4 deletions cve_bin_tool/output_engine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,13 @@ def generate_vex(self, all_cve_data: Dict[ProductInfo, CVEData], filename: str):
with open(filename, "w") as outfile:
json.dump(vex_output, outfile, indent=" ")

def output_file_wrapper(self, output_types=["console"]):
if "console" in output_types:
self.output_file()
output_types.remove("console")
for output_type in output_types:
self.output_file(output_type)

def output_file(self, output_type="console"):

"""Generate a file for list of CVE"""
Expand All @@ -623,6 +630,9 @@ def output_file(self, output_type="console"):
if output_type == "console":
# short circuit file opening logic if we are actually
# just writing to stdout
if self.filename:
self.filename = add_extension_if_not(self.filename, "txt")
self.logger.info(f"Console output stored at {self.filename}")
self.output_cves(self.filename, output_type)
return

Expand All @@ -648,10 +658,7 @@ def output_file(self, output_type="console"):
self.filename = generate_filename(output_type)

# Log the filename generated
if output_type == "html" or output_type == "pdf":
self.logger.info(f"{output_type.upper()} report stored at {self.filename}")
else:
self.logger.info(f"Output stored at {self.filename}")
self.logger.info(f"{output_type.upper()} report stored at {self.filename}")

# call to output_cves
mode = "w"
Expand Down
19 changes: 13 additions & 6 deletions cve_bin_tool/output_engine/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,20 +208,27 @@ def intermediate_output(
def add_extension_if_not(filename: str, output_type: str) -> str:
"""
summary: Checks if the filename ends with the extension and if not
adds one.
adds one. And if the filename ends with a different extension it replaces the extension.

Args:
filename (str): filename from OutputEngine
output_type (str): contains a value from ["json", "csv", "html"]
output_type (str): contains a value from ["json", "csv", "html", "pdf"]

Returns:
str: Filename with extension according to output_type
"""
if not filename.endswith(f".{output_type}"):
updated_filename = f"{filename}.{output_type}"
return updated_filename
else:
import re

extensions = ["json", "csv", "html", "pdf", "txt"]
for extension in extensions:
if not filename.endswith(f".{extension}"):
continue
if extension == output_type:
return filename
filename = re.sub(f".{extension}$", f".{output_type}", filename)
return filename
filename = f"{filename}.{output_type}"
return filename


def group_cve_by_remark(
Expand Down
12 changes: 11 additions & 1 deletion doc/MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,10 @@ which is useful if you're trying the latest code from
provide output filename (default: output to stdout)
--html-theme HTML_THEME
provide custom theme directory for HTML Report
-f {csv,json,console,html}, --format {csv,json,console,html}
-f {csv,json,console,html,pdf}, --format {csv,json,console,html,pdf}
update output format (default: console)
specify multiple output formats by using comma (',') as a separator
note: don't use spaces between comma (',') and the output formats.
-c CVSS, --cvss CVSS minimum CVSS score (as integer in range 0 to 10) to
report (default: 0)
-S {low,medium,high,critical}, --severity {low,medium,high,critical}
Expand Down Expand Up @@ -670,6 +672,14 @@ associated with it, we've opted to make PDF support only available to users who
have installed the library themselves. Once the library is installed, the PDF
report option will function.

You can also specify multiple output formats by using comma (',') as separator:

```console
cve-bin-tool file -f csv,json,html -o report
```

Note: Please don't use spaces between comma (',') and the output formats.

### -c CVSS, --cvss CVSS

This option specifies the minimum CVSS score (as integer in range 0 to 10) of the CVE to report. The default value is 0 which results in all CVEs being reported.
Expand Down
2 changes: 1 addition & 1 deletion test/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ def test_console_output_depending_reportlab_existence(self, caplog):
my_test_filename_pathlib.unlink()

pkg_to_spoof = "reportlab"
not_installed_msg = "PDF output not available. Default to console."
not_installed_msg = "PDF output not available."
execution = [
"cve-bin-tool",
"-f",
Expand Down
34 changes: 33 additions & 1 deletion test/test_output_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,7 @@ def test_output_file(self):
if filename.is_file():
contains_filename = True

if "Output stored at" in cm.output[0]:
if "JSON report stored at" in cm.output[0]:
contains_msg = True

# reset everything back
Expand All @@ -907,6 +907,38 @@ def test_output_file(self):
self.assertEqual(contains_filename, True)
self.assertEqual(contains_msg, True)

def test_output_file_wrapper(self):
"""Test file generation logic in output_file_wrapper"""
logger = logging.getLogger()
self.output_engine.filename = "test-report"

with self.assertLogs(logger, logging.INFO) as cm:
self.output_engine.output_file_wrapper(output_types=["json", "html"])

html_file = Path("test-report.html")
json_file = Path("test-report.json")

if html_file.is_file() and json_file.is_file():
contains_filename = True
else:
contains_filename = False

if (
"JSON report stored at" in cm.output[0]
and "HTML report stored at" in cm.output[1]
):
contains_msg = True
else:
contains_msg = False

# reset everything back
html_file.unlink()
json_file.unlink()
self.output_engine.filename = ""

self.assertEqual(contains_filename, True)
self.assertEqual(contains_msg, True)

def test_output_file_filename_already_exists(self):
"""Tests output_file when filename already exist"""

Expand Down