Skip to content

Commit d0f5afc

Browse files
complete phase 4 scanning ability
1 parent e3ee78d commit d0f5afc

15 files changed

Lines changed: 4628 additions & 10 deletions

File tree

cmd/compliance/sbom.go

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

cmd/tools/install_tools.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,17 @@ Common tools:
4949
- honnef.co/go/tools/cmd/staticcheck (static analysis)
5050
- github.com/go-delve/delve/cmd/dlv (debugger)
5151
- gotest.tools/gotestsum (test runner with nice output)
52-
- mvdan.cc/gofumpt (stricter gofmt)`,
52+
- mvdan.cc/gofumpt (stricter gofmt)
53+
54+
Security & SBOM tools:
55+
- github.com/anchore/grype/cmd/grype (vulnerability scanner)
56+
- github.com/aquasecurity/trivy/cmd/trivy (security scanner)
57+
- github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod (SBOM generator)
58+
- github.com/anchore/syft/cmd/syft (SBOM generator)
59+
60+
Short aliases available:
61+
goenv tools install grype@latest (expands to full path)
62+
goenv tools install trivy@latest (expands to full path)`,
5363
Args: cobra.MinimumNArgs(1),
5464
RunE: runInstall,
5565
}

0 commit comments

Comments
 (0)