@@ -112,6 +112,7 @@ func init() {
112112 sbomCmd .AddCommand (sbomSignCmd )
113113 sbomCmd .AddCommand (sbomVerifySignatureCmd )
114114 sbomCmd .AddCommand (sbomAttestCmd )
115+ sbomCmd .AddCommand (sbomScanCmd )
115116 cmdpkg .RootCmd .AddCommand (sbomCmd )
116117}
117118
@@ -1005,3 +1006,284 @@ func buildSyftCommand(toolPath string, cfg *config.Config) (*exec.Cmd, error) {
10051006
10061007 return exec .Command (toolPath , args ... ), nil
10071008}
1009+
1010+ var sbomScanCmd = & cobra.Command {
1011+ Use : "scan <sbom-file>" ,
1012+ Short : "Scan SBOM for vulnerabilities using security scanners" ,
1013+ Long : `Scan an SBOM file for known vulnerabilities using security scanners.
1014+
1015+ Supported scanners:
1016+
1017+ Open Source (Phase 4A):
1018+ - Grype (Anchore): Fast, offline vulnerability scanning
1019+ - Trivy (Aqua Security): Kubernetes-native, container scanning
1020+
1021+ Commercial/Enterprise (Phase 4B):
1022+ - Snyk: Developer-first security with prioritized fixes
1023+ - Veracode: Enterprise compliance and policy enforcement
1024+
1025+ The scan command reads an SBOM file (CycloneDX or SPDX format) and checks all
1026+ components against vulnerability databases to identify security issues.
1027+
1028+ Results include:
1029+ - Vulnerability ID (CVE-2023-xxxxx, GHSA-xxxx-yyyy-zzzz)
1030+ - Affected package and version
1031+ - Severity level (Critical, High, Medium, Low)
1032+ - Fix information (available version with patch)
1033+ - CVSS scores and descriptions
1034+
1035+ Examples:
1036+ # Scan with Grype (default)
1037+ goenv sbom scan sbom.json
1038+
1039+ # Scan with Trivy
1040+ goenv sbom scan sbom.json --scanner=trivy
1041+
1042+ # Scan with Snyk (requires SNYK_TOKEN)
1043+ goenv sbom scan sbom.json --scanner=snyk
1044+
1045+ # Scan with Veracode (requires API credentials)
1046+ goenv sbom scan sbom.json --scanner=veracode
1047+
1048+ # Show only high and critical vulnerabilities
1049+ goenv sbom scan sbom.json --severity=high
1050+
1051+ # Show only vulnerabilities with available fixes
1052+ goenv sbom scan sbom.json --only-fixed
1053+
1054+ # Save results to file
1055+ goenv sbom scan sbom.json --output=scan-results.json
1056+
1057+ # Fail build if any vulnerabilities found
1058+ goenv sbom scan sbom.json --fail-on=any
1059+
1060+ Phase 4A/4B: Scanner Integration (v3.4+)
1061+ Supports both open-source and commercial scanners for comprehensive vulnerability detection.` ,
1062+ Args : cobra .ExactArgs (1 ),
1063+ RunE : runSBOMScan ,
1064+ }
1065+
1066+ var (
1067+ scanScanner string
1068+ scanFormat string
1069+ scanOutputFormat string
1070+ scanOutput string
1071+ scanSeverity string
1072+ scanFailOn string
1073+ scanOnlyFixed bool
1074+ scanOffline bool
1075+ scanVerbose bool
1076+ scanListScanners bool
1077+ )
1078+
1079+ func init () {
1080+ sbomScanCmd .Flags ().StringVar (& scanScanner , "scanner" , "grype" , "Scanner to use (grype, trivy)" )
1081+ sbomScanCmd .Flags ().StringVar (& scanFormat , "format" , "cyclonedx-json" , "SBOM format (cyclonedx-json, spdx-json)" )
1082+ sbomScanCmd .Flags ().StringVar (& scanOutputFormat , "output-format" , "json" , "Output format (json, table, sarif)" )
1083+ sbomScanCmd .Flags ().StringVarP (& scanOutput , "output" , "o" , "" , "Output file (default: stdout)" )
1084+ sbomScanCmd .Flags ().StringVar (& scanSeverity , "severity" , "" , "Minimum severity to report (low, medium, high, critical)" )
1085+ sbomScanCmd .Flags ().StringVar (& scanFailOn , "fail-on" , "" , "Exit with error if vulnerabilities found (any, high, critical)" )
1086+ sbomScanCmd .Flags ().BoolVar (& scanOnlyFixed , "only-fixed" , false , "Show only vulnerabilities with available fixes" )
1087+ sbomScanCmd .Flags ().BoolVar (& scanOffline , "offline" , false , "Offline mode - skip vulnerability database updates" )
1088+ sbomScanCmd .Flags ().BoolVar (& scanVerbose , "verbose" , false , "Verbose output" )
1089+ sbomScanCmd .Flags ().BoolVar (& scanListScanners , "list-scanners" , false , "List available scanners and exit" )
1090+ }
1091+
1092+ func runSBOMScan (cmd * cobra.Command , args []string ) error {
1093+ // Handle --list-scanners flag
1094+ if scanListScanners {
1095+ return listScanners ()
1096+ }
1097+
1098+ sbomPath := args [0 ]
1099+
1100+ // Get scanner
1101+ scanner , err := sbom .GetScanner (scanScanner )
1102+ if err != nil {
1103+ return err
1104+ }
1105+
1106+ // Check if scanner is installed
1107+ if ! scanner .IsInstalled () {
1108+ fmt .Fprintf (os .Stderr , "Error: %s is not installed\n \n " , scanner .Name ())
1109+ fmt .Fprintf (os .Stderr , "%s\n " , scanner .InstallationInstructions ())
1110+ return fmt .Errorf ("%s not found" , scanner .Name ())
1111+ }
1112+
1113+ // Check if scanner supports the format
1114+ if ! scanner .SupportsFormat (scanFormat ) {
1115+ return fmt .Errorf ("%s does not support format: %s" , scanner .Name (), scanFormat )
1116+ }
1117+
1118+ // Prepare scan options
1119+ opts := & sbom.ScanOptions {
1120+ SBOMPath : sbomPath ,
1121+ Format : scanFormat ,
1122+ OutputFormat : scanOutputFormat ,
1123+ OutputPath : scanOutput ,
1124+ SeverityThreshold : scanSeverity ,
1125+ FailOn : scanFailOn ,
1126+ OnlyFixed : scanOnlyFixed ,
1127+ Offline : scanOffline ,
1128+ Verbose : scanVerbose ,
1129+ }
1130+
1131+ // Run scan
1132+ fmt .Printf ("Scanning %s with %s...\n " , sbomPath , scanner .Name ())
1133+
1134+ ctx := cmd .Context ()
1135+ result , err := scanner .Scan (ctx , opts )
1136+ if err != nil {
1137+ return fmt .Errorf ("scan failed: %w" , err )
1138+ }
1139+
1140+ // Display results
1141+ if scanOutput == "" {
1142+ // Print to stdout
1143+ return displayScanResults (result , scanOutputFormat )
1144+ }
1145+
1146+ fmt .Printf ("✅ Scan complete: %d vulnerabilities found\n " , result .Summary .Total )
1147+ fmt .Printf (" Critical: %d, High: %d, Medium: %d, Low: %d\n " ,
1148+ result .Summary .Critical , result .Summary .High ,
1149+ result .Summary .Medium , result .Summary .Low )
1150+ fmt .Printf (" Results saved to: %s\n " , scanOutput )
1151+
1152+ // Apply fail-on logic
1153+ return checkFailOnCondition (result , scanFailOn )
1154+ }
1155+
1156+ func listScanners () error {
1157+ fmt .Println ("Available vulnerability scanners:" )
1158+ fmt .Println ()
1159+
1160+ scanners := sbom .ListAvailableScanners ()
1161+ for _ , scanner := range scanners {
1162+ installed := "❌ Not installed"
1163+ if scanner .IsInstalled () {
1164+ version , _ := scanner .Version ()
1165+ installed = fmt .Sprintf ("✅ Installed (v%s)" , version )
1166+ }
1167+
1168+ fmt .Printf (" %s - %s\n " , scanner .Name (), installed )
1169+ }
1170+
1171+ fmt .Println ()
1172+ fmt .Println ("To install a scanner:" )
1173+ fmt .Println (" goenv tools install grype" )
1174+ fmt .Println (" goenv tools install trivy" )
1175+
1176+ return nil
1177+ }
1178+
1179+ func displayScanResults (result * sbom.ScanResult , format string ) error {
1180+ switch format {
1181+ case "json" :
1182+ data , err := json .MarshalIndent (result , "" , " " )
1183+ if err != nil {
1184+ return fmt .Errorf ("failed to marshal results: %w" , err )
1185+ }
1186+ fmt .Println (string (data ))
1187+
1188+ case "table" :
1189+ displayTableResults (result )
1190+
1191+ default :
1192+ return fmt .Errorf ("unsupported output format: %s" , format )
1193+ }
1194+
1195+ return nil
1196+ }
1197+
1198+ func displayTableResults (result * sbom.ScanResult ) {
1199+ fmt .Printf ("\n 🔍 Scan Results (%s v%s)\n " , result .Scanner , result .ScannerVersion )
1200+ fmt .Println (strings .Repeat ("=" , 80 ))
1201+
1202+ fmt .Printf ("\n 📊 Summary:\n " )
1203+ fmt .Printf (" Total: %d vulnerabilities\n " , result .Summary .Total )
1204+ fmt .Printf (" Critical: %d | High: %d | Medium: %d | Low: %d\n " ,
1205+ result .Summary .Critical , result .Summary .High ,
1206+ result .Summary .Medium , result .Summary .Low )
1207+ fmt .Printf (" With Fix: %d | Without Fix: %d\n " ,
1208+ result .Summary .WithFix , result .Summary .WithoutFix )
1209+
1210+ if len (result .Vulnerabilities ) == 0 {
1211+ fmt .Printf ("\n ✅ No vulnerabilities found!\n " )
1212+ return
1213+ }
1214+
1215+ fmt .Printf ("\n 🚨 Vulnerabilities:\n " )
1216+ fmt .Println ()
1217+
1218+ for i , vuln := range result .Vulnerabilities {
1219+ // Severity indicator
1220+ indicator := getSeverityIndicator (vuln .Severity )
1221+
1222+ fmt .Printf ("%d. %s %s [%s]\n " , i + 1 , indicator , vuln .ID , vuln .Severity )
1223+ fmt .Printf (" Package: %s@%s\n " , vuln .PackageName , vuln .PackageVersion )
1224+
1225+ if vuln .FixAvailable {
1226+ fmt .Printf (" ✅ Fix: Upgrade to %s\n " , vuln .FixedInVersion )
1227+ } else {
1228+ fmt .Printf (" ⚠️ No fix available\n " )
1229+ }
1230+
1231+ if vuln .CVSS > 0 {
1232+ fmt .Printf (" CVSS: %.1f\n " , vuln .CVSS )
1233+ }
1234+
1235+ if vuln .Description != "" {
1236+ // Truncate long descriptions
1237+ desc := vuln .Description
1238+ if len (desc ) > 100 {
1239+ desc = desc [:97 ] + "..."
1240+ }
1241+ fmt .Printf (" %s\n " , desc )
1242+ }
1243+
1244+ if len (vuln .URLs ) > 0 {
1245+ fmt .Printf (" 🔗 %s\n " , vuln .URLs [0 ])
1246+ }
1247+
1248+ fmt .Println ()
1249+ }
1250+ }
1251+
1252+ func getSeverityIndicator (severity string ) string {
1253+ switch severity {
1254+ case "Critical" :
1255+ return "🔴"
1256+ case "High" :
1257+ return "🟠"
1258+ case "Medium" :
1259+ return "🟡"
1260+ case "Low" :
1261+ return "🔵"
1262+ default :
1263+ return "⚪"
1264+ }
1265+ }
1266+
1267+ func checkFailOnCondition (result * sbom.ScanResult , failOn string ) error {
1268+ if failOn == "" {
1269+ return nil
1270+ }
1271+
1272+ switch failOn {
1273+ case "any" :
1274+ if result .Summary .Total > 0 {
1275+ return fmt .Errorf ("found %d vulnerabilities (--fail-on=any)" , result .Summary .Total )
1276+ }
1277+ case "critical" :
1278+ if result .Summary .Critical > 0 {
1279+ return fmt .Errorf ("found %d critical vulnerabilities" , result .Summary .Critical )
1280+ }
1281+ case "high" :
1282+ if result .Summary .Critical > 0 || result .Summary .High > 0 {
1283+ total := result .Summary .Critical + result .Summary .High
1284+ return fmt .Errorf ("found %d high/critical vulnerabilities" , total )
1285+ }
1286+ }
1287+
1288+ return nil
1289+ }
0 commit comments