1616import json
1717import re
1818import subprocess
19+ import datetime
1920from tempfile import NamedTemporaryFile
2021from shutil import which
2122import pandas as pd
3031 LOG_SPAM ,
3132 df_to_csv_file ,
3233 df_from_csv_file ,
34+ df_log ,
3335)
3436
3537###############################################################################
@@ -85,6 +87,7 @@ def __init__(self):
8587 self .df_vulnix = None
8688 self .df_grype = None
8789 self .df_osv = None
90+ self .df_cvebin = None
8891 self .df_report = None
8992
9093 def _parse_vulnix (self , json_str ):
@@ -155,8 +158,8 @@ def scan_grype(self, sbom_path):
155158 self ._parse_grype (ret )
156159
157160 def _parse_osv (self , df_osv ):
158- self . df_osv = df_osv
159- if not self .df_osv . empty :
161+ if not df_osv . empty :
162+ self .df_osv = df_osv
160163 self .df_osv ["scanner" ] = "osv"
161164 self .df_osv .replace (np .nan , "" , regex = True , inplace = True )
162165 self .df_osv .drop_duplicates (keep = "first" , inplace = True )
@@ -174,9 +177,61 @@ def scan_osv(self, sbom_path):
174177 df_osv = osv .to_dataframe ()
175178 self ._parse_osv (df_osv )
176179
180+ def _parse_cvebin (self , df_cvebin ):
181+ if not df_cvebin .empty :
182+ df_log (df_cvebin , LOG_SPAM )
183+ df_cvebin ["scanner" ] = "cvebin"
184+ select_cols = {
185+ "product" : "package" ,
186+ "version" : "version" ,
187+ "cve_number" : "vuln_id" ,
188+ "scanner" : "scanner" ,
189+ }
190+ df_cvebin = df_cvebin .rename (columns = select_cols )[select_cols .values ()]
191+ df_cvebin ["year_maybe" ] = df_cvebin .apply (_guess_vuln_year , axis = 1 )
192+ df_log (df_cvebin , LOG_SPAM )
193+ # Drop old vulnerabilities. Below, we drop vulnerabilities that have not
194+ # been fixed during the past ~2 years assuming they are false positives.
195+ df_cvebin = df_cvebin [
196+ df_cvebin ["year_maybe" ] > (datetime .date .today ().year ) - 2
197+ ]
198+ df_cvebin .replace (np .nan , "" , regex = True , inplace = True )
199+ df_cvebin .drop_duplicates (keep = "first" , inplace = True )
200+ self .df_cvebin = df_cvebin
201+ if _LOG .level <= logging .DEBUG :
202+ df_to_csv_file (self .df_cvebin , "df_cvebin.csv" )
203+
204+ def scan_cvebin (self , sbom_path ):
205+ """Run cve-bin-tool scan using the SBOM at sbom_path as input"""
206+ _LOG .info ("Running cve-bin-tool scan" )
207+ prefix = "cve_bin_tool_"
208+ csv_suffix = ".csv"
209+ with NamedTemporaryFile (delete = False , prefix = prefix , suffix = csv_suffix ) as fcsv :
210+ cmd = [
211+ "cve-bin-tool" ,
212+ "--update=daily" ,
213+ # cve-bin-tool reports many false positive vulnerabilities.
214+ # We disable OSV datasource for two reasons: (1) vulnxscan
215+ # includes OSV vulnerabilities via the osv.py and (2) many
216+ # (all?) OSV vulnerabilities reported by cve-bin-tool are
217+ # not valid.
218+ "--disable-data-source=OSV" ,
219+ f"--sbom-file={ sbom_path } " ,
220+ "--sbom=cyclonedx" ,
221+ f"--output-file={ fcsv .name } " ,
222+ "--format=csv" ,
223+ ]
224+ exec_cmd (cmd , raise_on_error = False )
225+ if pathlib .Path (fcsv .name ).stat ().st_size > 0 :
226+ df_cvebin = df_from_csv_file (fcsv .name )
227+ self ._parse_cvebin (df_cvebin )
228+
177229 def _generate_report (self ):
178230 # Concatenate vulnerability data from different scanners
179- df = pd .concat ([self .df_vulnix , self .df_grype , self .df_osv ], ignore_index = True )
231+ df = pd .concat (
232+ [self .df_vulnix , self .df_grype , self .df_osv , self .df_cvebin ],
233+ ignore_index = True ,
234+ )
180235 if df .empty :
181236 _LOG .debug ("No scanners reported any findings" )
182237 return
@@ -194,7 +249,7 @@ def _generate_report(self):
194249 df = df .pivot_table (index = group_cols , columns = "scanner" , values = "count" )
195250 # Pivot creates a multilevel index, we'll get rid of it:
196251 df .reset_index (drop = False , inplace = True )
197- scanners = ["grype" , "osv" ]
252+ scanners = ["grype" , "osv" , "cvebin" ]
198253 if self .df_vulnix is not None :
199254 scanners .append ("vulnix" )
200255 df .reindex (group_cols + scanners , axis = 1 )
@@ -206,6 +261,7 @@ def _generate_report(self):
206261 # Reformat values in 'scanner' columns
207262 df ["grype" ] = df .apply (lambda row : _reformat_scanner (row .grype ), axis = 1 )
208263 df ["osv" ] = df .apply (lambda row : _reformat_scanner (row .osv ), axis = 1 )
264+ df ["cvebin" ] = df .apply (lambda row : _reformat_scanner (row .cvebin ), axis = 1 )
209265 if "vulnix" in scanners :
210266 df ["vulnix" ] = df .apply (lambda row : _reformat_scanner (row .vulnix ), axis = 1 )
211267 # Add column 'url'
@@ -291,6 +347,14 @@ def _vuln_sortcol(row):
291347 return str (row .vuln_id )
292348
293349
350+ def _guess_vuln_year (row ):
351+ match = re .match (r".*[A-Za-z][-_]([1-2][0-9]{3})[-_][0-9]+.*" , row .vuln_id )
352+ if match :
353+ year = match .group (1 )
354+ return int (year )
355+ return int (datetime .date .today ().year )
356+
357+
294358def _vuln_url (row ):
295359 osv_url = "https://osv.dev/"
296360 nvd_url = "https://nvd.nist.gov/vuln/detail/"
@@ -369,6 +433,7 @@ def main():
369433 # Fail early if following commands are not in path
370434 exit_unless_command_exists ("grype" )
371435 exit_unless_command_exists ("vulnix" )
436+ exit_unless_command_exists ("cve-bin-tool" )
372437
373438 target_path = args .TARGET .as_posix ()
374439 target_path_abs = args .TARGET .resolve ().as_posix ()
@@ -387,6 +452,7 @@ def main():
387452 _LOG .info ("Using cdx SBOM '%s'" , sbom_cdx_path )
388453 _LOG .info ("Using csv SBOM '%s'" , sbom_csv_path )
389454 scanner .scan_vulnix (target_path_abs , args .buildtime )
455+ scanner .scan_cvebin (sbom_cdx_path )
390456 scanner .scan_grype (sbom_cdx_path )
391457 scanner .scan_osv (sbom_cdx_path )
392458 scanner .report (args .out , target_path , sbom_csv_path , args .buildtime , args .sbom )
0 commit comments