Skip to content

Commit 10c585f

Browse files
authored
Add basic json output (#418)
* Refactor code to prepare for other output formats * Add naive json formatting * Structure json more nicely * Add test for formatted_modules in OutputEngine * Add test for output_json in OutputEngine * Refactor OutputEngine tests to reduce code repetition
1 parent f8f9f5e commit 10c585f

File tree

3 files changed

+87
-23
lines changed

3 files changed

+87
-23
lines changed

cve_bin_tool/OutputEngine.py

+40-19
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import csv
2+
import json
23
import os
34

45
from datetime import datetime
@@ -20,37 +21,56 @@ def generate_filename(self, extention=None):
2021
self.filename = f"output.cve-bin-tool.{now}.{extention}"
2122

2223
def output_cves(self, outfile, output=None):
23-
2424
""" Output a list of CVEs
25-
format is modules[checker_name][version] = dict{id: severity}
25+
format self.modules[checker_name][version] = dict{id: severity}
26+
to other formats like CSV or JSON
27+
"""
28+
if output == "json":
29+
self.output_json(outfile)
30+
else: # csv, console, or anything else that is unrecognised
31+
self.output_csv(outfile)
32+
33+
def formatted_modules(self):
34+
""" Returns self.modules converted into form
35+
formatted_output[modulesname][version] = {
36+
"cve": cve_number,
37+
"severity": cve_severity,
38+
}
2639
"""
27-
# if the output is csv we must open a file
28-
if output == "csv":
29-
outfile = open(f"{outfile}", "w")
40+
formatted_output = {}
41+
for modulename in self.modules:
42+
formatted_output[modulename] = {}
43+
for version, cves in self.modules[modulename].items():
44+
formatted_output[modulename][version] = []
45+
for cve_number, cve_severity in cves.items():
46+
formatted_output[modulename][version].append(
47+
{"cve": cve_number, "severity": cve_severity,}
48+
)
49+
return formatted_output
3050

31-
writer = csv.writer(outfile)
32-
writer.writerow(["Module Name", "Version", "CVE Number", "Severity"])
51+
def output_json(self, outfile):
52+
""" Output a JSON of CVEs """
53+
json.dump(self.formatted_modules(), outfile)
3354

55+
def output_csv(self, outfile):
56+
""" Output a CSV of CVEs """
3457
writer = csv.writer(outfile)
58+
writer.writerow(["Module Name", "Version", "CVE Number", "Severity"])
3559
for modulename, versions in self.modules.items():
3660
for version, cve_list in versions.items():
3761
for cve_number, cve_severity in cve_list.items():
3862
row = [modulename, version, cve_number, cve_severity]
3963
writer.writerow(row)
4064

41-
# We must also close the file
42-
if output == "csv":
43-
outfile.close()
44-
45-
def output_csv(self):
65+
def output_file(self, output="csv"):
4666

47-
""" Generate a CSV file for list of CVE """
67+
""" Generate a file for list of CVE """
4868

4969
# Check if we need to generate a filename
5070
if self.filename == None:
51-
self.generate_filename("csv")
71+
self.generate_filename(output)
5272
else:
53-
self.filename = f"{self.filename}.csv"
73+
self.filename = f"{self.filename}.{output}"
5474
# check if the filename already exists
5575
file_list = os.listdir(os.getcwd())
5676
if self.filename in file_list:
@@ -60,7 +80,7 @@ def output_csv(self):
6080
self.logger.info(
6181
"Generating a new filename with Default Naming Convention"
6282
)
63-
self.generate_filename("csv")
83+
self.generate_filename(output)
6484

6585
# try opening that file
6686
try:
@@ -70,10 +90,11 @@ def output_csv(self):
7090
except Exception as E:
7191
self.logger.warning(E)
7292
self.logger.info("Switching Back to Default Naming Convention")
73-
self.generate_filename("csv")
93+
self.generate_filename(output)
7494

7595
# Log the filename generated
76-
self.logger.info(f"CSV output stored at {os.getcwd()}/{self.filename}")
96+
self.logger.info(f"Output stored at {os.getcwd()}/{self.filename}")
7797

7898
# call to output_cves
79-
self.output_cves(self.filename, "csv")
99+
with open(self.filename, "w") as f:
100+
self.output_cves(f, output)

cve_bin_tool/cli.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -491,10 +491,9 @@ def main(argv=None, outfile=sys.stdout):
491491
and args.output == "console"
492492
):
493493
output.output_cves(outfile)
494-
495-
# If the args are passed for csv we will generate a CSV output
496-
if args.output == "csv":
497-
output.output_csv()
494+
else:
495+
# If the args are passed for csv/json we will generate a file output
496+
output.output_file(args.output)
498497

499498
# Use the number of files with known cves as error code
500499
# as requested by folk planning to automate use of this script.

test/test_output_engine.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""
2+
CVE-bin-tool OutputEngine tests
3+
"""
4+
import unittest
5+
import io
6+
import json
7+
8+
from cve_bin_tool.OutputEngine import OutputEngine
9+
10+
11+
class TestOutputEngine(unittest.TestCase):
12+
""" Test the OutputEngine class functions """
13+
14+
MOCK_MODULES = {
15+
"modulename0": {
16+
"1.0": {"CVE-1234-1234": "MEDIUM", "CVE-1234-9876": "LOW",},
17+
"2.8.6": {"CVE-1234-1111": "LOW",},
18+
},
19+
"modulename1": {"3.2.1.0": {"CVE-1234-5678": "HIGH",}},
20+
}
21+
FORMATTED_MODULES = {
22+
"modulename0": {
23+
"1.0": [
24+
{"cve": "CVE-1234-1234", "severity": "MEDIUM"},
25+
{"cve": "CVE-1234-9876", "severity": "LOW"},
26+
],
27+
"2.8.6": [{"cve": "CVE-1234-1111", "severity": "LOW"},],
28+
},
29+
"modulename1": {"3.2.1.0": [{"cve": "CVE-1234-5678", "severity": "HIGH"},]},
30+
}
31+
32+
def setUp(self):
33+
self.output_engine = OutputEngine(modules=self.MOCK_MODULES)
34+
self.mock_file = io.StringIO()
35+
36+
def test_formatted_modules(self):
37+
""" Test reformatting modules """
38+
self.assertEqual(self.output_engine.formatted_modules(), self.FORMATTED_MODULES)
39+
40+
def test_output_json(self):
41+
""" Test formatting output as JSON """
42+
self.output_engine.output_json(self.mock_file)
43+
self.mock_file.seek(0) # reset file position
44+
self.assertEqual(json.load(self.mock_file), self.FORMATTED_MODULES)

0 commit comments

Comments
 (0)