Skip to content

Commit a5b2e22

Browse files
authored
feat: NoScan Changes for JSON output (#5294)
Signed-off-by: joydeep049 <[email protected]>
1 parent 3b7fdb2 commit a5b2e22

File tree

5 files changed

+492
-79
lines changed

5 files changed

+492
-79
lines changed

cve_bin_tool/cli.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,8 +1164,9 @@ def main(argv=None):
11641164
LOGGER.debug(f"Triage Data: {triage_data}")
11651165
parsed_data[product_info] = triage_data
11661166

1167-
if not args["no_scan"]:
1168-
cve_scanner.get_cves(product_info, triage_data)
1167+
# Always call get_cves to collect component information
1168+
# The method handles both normal and no-scan modes internally
1169+
cve_scanner.get_cves(product_info, triage_data)
11691170
total_files = version_scanner.total_scanned_files
11701171
LOGGER.info(f"Total files: {total_files}")
11711172

cve_bin_tool/output_engine/__init__.py

Lines changed: 79 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -71,53 +71,84 @@ def output_csv(
7171
affected_versions: int = 0,
7272
metrics: bool = False,
7373
strip_scan_dir: bool = False,
74+
no_scan: bool = False,
7475
):
75-
"""Output a CSV of CVEs"""
76-
formatted_output = format_output(
77-
all_cve_data,
78-
scanned_dir,
79-
strip_scan_dir,
80-
all_cve_version_info,
81-
detailed,
82-
affected_versions,
83-
metrics,
84-
)
76+
"""Output a CSV of CVEs or components (in no-scan mode)"""
77+
if no_scan:
78+
# In no-scan mode, output component information
79+
fieldnames = [
80+
"vendor",
81+
"product",
82+
"version",
83+
"paths",
84+
"scan_mode",
85+
]
86+
writer = csv.DictWriter(outfile, fieldnames=fieldnames)
87+
writer.writeheader()
88+
89+
for product_info, component_data in all_cve_data.items():
90+
if strip_scan_dir:
91+
paths = ", ".join(
92+
[strip_path(path, scanned_dir) for path in component_data["paths"]]
93+
)
94+
else:
95+
paths = ", ".join(component_data["paths"])
96+
97+
row = {
98+
"vendor": product_info.vendor,
99+
"product": product_info.product,
100+
"version": product_info.version,
101+
"paths": paths,
102+
"scan_mode": "no-scan",
103+
}
104+
writer.writerow(row)
105+
else:
106+
# Normal CVE output
107+
formatted_output = format_output(
108+
all_cve_data,
109+
scanned_dir,
110+
strip_scan_dir,
111+
all_cve_version_info,
112+
detailed,
113+
affected_versions,
114+
metrics,
115+
)
85116

86-
# Remove triage response and justification from the CSV output.
87-
for cve_entry in formatted_output:
88-
cve_entry.pop("response", None)
89-
cve_entry.pop("justification", None)
90-
91-
# Trim any leading -, =, +, @, tab or CR to avoid excel macros
92-
for cve_entry in formatted_output:
93-
for key, value in cve_entry.items():
94-
cve_entry[key] = value.strip("-=+@\t\r")
95-
96-
fieldnames = [
97-
"vendor",
98-
"product",
99-
"version",
100-
"cve_number",
101-
"severity",
102-
"score",
103-
"source",
104-
"cvss_version",
105-
"cvss_vector",
106-
"paths",
107-
"remarks",
108-
"comments",
109-
]
110-
if metrics:
111-
fieldnames.append("epss_probability")
112-
fieldnames.append("epss_percentile")
113-
if detailed:
114-
fieldnames.append("description")
115-
if affected_versions != 0:
116-
fieldnames.append("affected_versions")
117-
writer = csv.DictWriter(outfile, fieldnames=fieldnames)
118-
119-
writer.writeheader()
120-
writer.writerows(formatted_output)
117+
# Remove triage response and justification from the CSV output.
118+
for cve_entry in formatted_output:
119+
cve_entry.pop("response", None)
120+
cve_entry.pop("justification", None)
121+
122+
# Trim any leading -, =, +, @, tab or CR to avoid excel macros
123+
for cve_entry in formatted_output:
124+
for key, value in cve_entry.items():
125+
cve_entry[key] = value.strip("-=+@\t\r")
126+
127+
fieldnames = [
128+
"vendor",
129+
"product",
130+
"version",
131+
"cve_number",
132+
"severity",
133+
"score",
134+
"source",
135+
"cvss_version",
136+
"cvss_vector",
137+
"paths",
138+
"remarks",
139+
"comments",
140+
]
141+
if metrics:
142+
fieldnames.append("epss_probability")
143+
fieldnames.append("epss_percentile")
144+
if detailed:
145+
fieldnames.append("description")
146+
if affected_versions != 0:
147+
fieldnames.append("affected_versions")
148+
writer = csv.DictWriter(outfile, fieldnames=fieldnames)
149+
150+
writer.writeheader()
151+
writer.writerows(formatted_output)
121152

122153

123154
# load pdfs only if reportlab is found. if not, make a stub that prints a
@@ -744,6 +775,7 @@ def output_cves(self, outfile, output_type="console"):
744775
self.affected_versions,
745776
self.metrics,
746777
self.strip_scan_dir,
778+
self.no_scan,
747779
)
748780
elif output_type == "json2":
749781
output_json2(
@@ -758,6 +790,7 @@ def output_cves(self, outfile, output_type="console"):
758790
self.exploits,
759791
self.metrics,
760792
self.strip_scan_dir,
793+
self.no_scan,
761794
)
762795
elif output_type == "csv":
763796
output_csv(
@@ -769,6 +802,7 @@ def output_cves(self, outfile, output_type="console"):
769802
self.affected_versions,
770803
self.metrics,
771804
self.strip_scan_dir,
805+
self.no_scan,
772806
)
773807
elif output_type == "pdf":
774808
output_pdf(

cve_bin_tool/output_engine/json_output.py

Lines changed: 105 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from typing import IO
99

1010
from cve_bin_tool.cvedb import CVEDB
11-
from cve_bin_tool.util import CVEData, ProductInfo, VersionInfo
11+
from cve_bin_tool.util import CVEData, ProductInfo, VersionInfo, strip_path
1212
from cve_bin_tool.version import VERSION
1313

1414
from .util import format_output, get_cve_summary
@@ -54,6 +54,44 @@ def vulnerabilities_builder(
5454
return vulnerabilities
5555

5656

57+
def components_builder(
58+
all_cve_data,
59+
scanned_dir,
60+
strip_scan_dir,
61+
):
62+
"""
63+
Builds a dictionary of identified components for no-scan mode.
64+
"""
65+
components = {}
66+
components["summary"] = {
67+
"total_components": len(all_cve_data),
68+
"components_with_paths": sum(
69+
1 for data in all_cve_data.values() if data["paths"]
70+
),
71+
}
72+
73+
component_reports = []
74+
for product_info, component_data in all_cve_data.items():
75+
if strip_scan_dir:
76+
paths = ", ".join(
77+
[strip_path(path, scanned_dir) for path in component_data["paths"]]
78+
)
79+
else:
80+
paths = ", ".join(component_data["paths"])
81+
82+
component_entry = {
83+
"vendor": product_info.vendor,
84+
"product": product_info.product,
85+
"version": product_info.version,
86+
"paths": paths,
87+
"scan_mode": "no-scan",
88+
}
89+
component_reports.append(component_entry)
90+
91+
components["components"] = component_reports
92+
return components
93+
94+
5795
def db_entries_count():
5896
"""
5997
Retrieves the count of CVE entries from the database grouped by data source.
@@ -103,18 +141,36 @@ def output_json(
103141
affected_versions: int = 0,
104142
metrics: bool = False,
105143
strip_scan_dir: bool = False,
144+
no_scan: bool = False,
106145
):
107-
"""Output a JSON of CVEs"""
108-
formatted_output = format_output(
109-
all_cve_data,
110-
scanned_dir,
111-
strip_scan_dir,
112-
all_cve_version_info,
113-
detailed,
114-
affected_versions,
115-
metrics,
116-
)
117-
json.dump(formatted_output, outfile, indent=2)
146+
"""Output a JSON of CVEs or components (in no-scan mode)"""
147+
if no_scan:
148+
# In no-scan mode, output component information instead of CVE data
149+
output = {}
150+
output["scan_mode"] = "no-scan"
151+
output["metadata"] = {
152+
"tool": {"name": "cve-bin-tool", "version": f"{VERSION}"},
153+
"generation_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
154+
"description": "Component identification scan without vulnerability assessment",
155+
}
156+
output["components"] = components_builder(
157+
all_cve_data,
158+
scanned_dir,
159+
strip_scan_dir,
160+
)
161+
json.dump(output, outfile, indent=2)
162+
else:
163+
# Normal CVE output
164+
formatted_output = format_output(
165+
all_cve_data,
166+
scanned_dir,
167+
strip_scan_dir,
168+
all_cve_version_info,
169+
detailed,
170+
affected_versions,
171+
metrics,
172+
)
173+
json.dump(formatted_output, outfile, indent=2)
118174

119175

120176
def output_json2(
@@ -129,23 +185,41 @@ def output_json2(
129185
exploits: bool = False,
130186
metrics: bool = False,
131187
strip_scan_dir: bool = False,
188+
no_scan: bool = False,
132189
):
133-
"""Output a JSON of CVEs in JSON2 format"""
134-
output = {}
135-
output["$schema"] = ""
136-
output["metadata"] = metadata_builder(organized_parameters)
137-
output["database_info"] = {
138-
"last_updated": time_of_last_update.strftime("%Y-%m-%d %H:%M:%S"),
139-
"total_entries": db_entries_count(),
140-
}
141-
output["vulnerabilities"] = vulnerabilities_builder(
142-
all_cve_data,
143-
exploits,
144-
all_cve_version_info,
145-
scanned_dir,
146-
detailed,
147-
affected_versions,
148-
metrics,
149-
strip_scan_dir,
150-
)
151-
json.dump(output, outfile, indent=2)
190+
"""Output a JSON of CVEs in JSON2 format or components (in no-scan mode)"""
191+
if no_scan:
192+
# In no-scan mode, output component information in JSON2 format
193+
output = {}
194+
output["$schema"] = ""
195+
output["metadata"] = metadata_builder(organized_parameters)
196+
output["scan_mode"] = "no-scan"
197+
output["description"] = (
198+
"Component identification scan without vulnerability assessment"
199+
)
200+
output["components"] = components_builder(
201+
all_cve_data,
202+
scanned_dir,
203+
strip_scan_dir,
204+
)
205+
json.dump(output, outfile, indent=2)
206+
else:
207+
# Normal CVE output in JSON2 format
208+
output = {}
209+
output["$schema"] = ""
210+
output["metadata"] = metadata_builder(organized_parameters)
211+
output["database_info"] = {
212+
"last_updated": time_of_last_update.strftime("%Y-%m-%d %H:%M:%S"),
213+
"total_entries": db_entries_count(),
214+
}
215+
output["vulnerabilities"] = vulnerabilities_builder(
216+
all_cve_data,
217+
exploits,
218+
all_cve_version_info,
219+
scanned_dir,
220+
detailed,
221+
affected_versions,
222+
metrics,
223+
strip_scan_dir,
224+
)
225+
json.dump(output, outfile, indent=2)

cve_bin_tool/version_scanner.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,16 @@ def scan_file(self, filename: str) -> Iterator[ScanInfo]:
262262
# check if it's a Linux kernel image
263263
is_linux_kernel, output = self.is_linux_kernel(filename)
264264

265-
if not is_exec and not is_linux_kernel:
265+
# In no-scan mode, also check if it's a language-specific file
266+
is_language_file = False
267+
if self.no_scan:
268+
# Check if filename matches any language parser patterns
269+
for pattern in valid_files.keys():
270+
if pattern in filename:
271+
is_language_file = True
272+
break
273+
274+
if not is_exec and not is_linux_kernel and not is_language_file:
266275
return None
267276

268277
# parse binary file's strings
@@ -279,6 +288,18 @@ def scan_file(self, filename: str) -> Iterator[ScanInfo]:
279288
for scan_info in parse(filename, output, self.cve_db, self.logger):
280289
yield ScanInfo(scan_info.product_info, "".join(self.file_stack))
281290

291+
# In no-scan mode, also try to parse language-specific files directly
292+
if self.no_scan and is_language_file:
293+
# Create a mock output string that includes the filename pattern
294+
for pattern in valid_files.keys():
295+
if pattern in filename:
296+
mock_output = f"mock: {pattern}"
297+
for scan_info in parse(
298+
filename, mock_output, self.cve_db, self.logger
299+
):
300+
yield ScanInfo(scan_info.product_info, "".join(self.file_stack))
301+
break
302+
282303
yield from self.run_checkers(filename, lines)
283304

284305
def run_checkers(self, filename: str, lines: str) -> Iterator[ScanInfo]:

0 commit comments

Comments
 (0)