Skip to content

Commit 17b514f

Browse files
Add sbom scan
1 parent 2f4b11b commit 17b514f

File tree

2 files changed

+181
-11
lines changed

2 files changed

+181
-11
lines changed

cmd/sbom-scan.go

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package cmd
2+
3+
import (
4+
"os"
5+
6+
"github.com/gitpod-io/leeway/pkg/leeway"
7+
log "github.com/sirupsen/logrus"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
// sbomScanCmd represents the sbom scan command
12+
var sbomScanCmd = &cobra.Command{
13+
Use: "scan <package>",
14+
Short: "Scans a package's SBOM for vulnerabilities",
15+
Long: `Scans a package's SBOM for vulnerabilities and exports the results to a specified directory.
16+
17+
This command uses existing SBOM files from previously built packages. It checks if SBOM is enabled
18+
in the workspace settings. If not, it aborts. The scan results are exported to the directory
19+
specified by the --output-dir flag.
20+
21+
When used with --with-dependencies, it scans the package and all its dependencies for vulnerabilities.`,
22+
Args: cobra.MinimumNArgs(1),
23+
Run: func(cmd *cobra.Command, args []string) {
24+
// Get the package
25+
_, pkg, _, _ := getTarget(args, false)
26+
if pkg == nil {
27+
log.Fatal("sbom scan requires a package")
28+
}
29+
30+
// Check if SBOM is enabled in workspace settings
31+
if !pkg.C.W.SBOM.Enabled {
32+
log.Fatal("SBOM scanning requires sbom.enabled=true in workspace settings")
33+
}
34+
35+
// Check if vulnerability scanning is enabled
36+
if !pkg.C.W.SBOM.ScanVulnerabilities {
37+
log.Fatal("SBOM scanning requires sbom.scanVulnerabilities=true in workspace settings")
38+
}
39+
40+
// Get build options and cache
41+
_, cache := getBuildOpts(cmd)
42+
43+
// Get package location in cache
44+
_, ok := cache.Location(pkg)
45+
if !ok {
46+
log.Fatalf("%s is not built", pkg.FullName())
47+
}
48+
49+
// Get output directory
50+
outputDir, _ := cmd.Flags().GetString("output-dir")
51+
if outputDir == "" {
52+
log.Fatal("--output-dir is required")
53+
}
54+
55+
// Create output directory if it doesn't exist
56+
if err := os.MkdirAll(outputDir, 0755); err != nil {
57+
log.WithError(err).Fatalf("cannot create output directory %s", outputDir)
58+
}
59+
60+
// Get with-dependencies flag
61+
withDependencies, _ := cmd.Flags().GetBool("with-dependencies")
62+
63+
// Scan the package for vulnerabilities
64+
if withDependencies {
65+
log.Infof("Scanning package %s and its dependencies for vulnerabilities", pkg.FullName())
66+
} else {
67+
log.Infof("Scanning package %s for vulnerabilities", pkg.FullName())
68+
}
69+
70+
// Create a console reporter for logging
71+
reporter := leeway.NewConsoleReporter()
72+
73+
// Use the ScanPackageSBOM function to scan the package
74+
if err := leeway.ScanPackageSBOM(pkg, reporter, cache, outputDir, withDependencies); err != nil {
75+
log.WithError(err).Fatalf("Failed to scan package %s for vulnerabilities", pkg.FullName())
76+
}
77+
78+
if withDependencies {
79+
log.Infof("Vulnerability scan completed for package %s and its dependencies", pkg.FullName())
80+
} else {
81+
log.Infof("Vulnerability scan completed for package %s", pkg.FullName())
82+
}
83+
log.Infof("Scan results exported to %s", outputDir)
84+
85+
// If we have failOn configured, the ScanPackageForVulnerabilities function will have already
86+
// returned an error if vulnerabilities at those severity levels were found
87+
},
88+
}
89+
90+
func init() {
91+
sbomScanCmd.Flags().String("output-dir", "", "Directory to export scan results (required)")
92+
sbomScanCmd.MarkFlagRequired("output-dir")
93+
sbomScanCmd.Flags().Bool("with-dependencies", false, "Scan the package and all its dependencies")
94+
95+
sbomCmd.AddCommand(sbomScanCmd)
96+
addBuildFlags(sbomScanCmd)
97+
}

pkg/leeway/sbom.go

+84-11
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414

1515
"slices"
1616

17+
"github.com/gitpod-io/leeway/pkg/leeway/cache"
18+
1719
"github.com/anchore/clio"
1820
"github.com/anchore/grype/grype"
1921
"github.com/anchore/grype/grype/db/v6/distribution"
@@ -361,9 +363,38 @@ func ScanPackageForVulnerabilities(p *Package, buildctx *buildContext, sbomFile
361363
return nil
362364
}
363365

366+
// ScanPackageSBOM scans a package's SBOM for vulnerabilities and exports results to the specified directory
367+
// This is an exported wrapper around ScanAllPackagesForVulnerabilities for use by the sbom scan command
368+
// If withDependencies is true, it will also scan all dependencies of the package
369+
func ScanPackageSBOM(p *Package, reporter Reporter, localCache cache.LocalCache, outputDir string, withDependencies bool) error {
370+
// Create a minimal buildContext with just the required fields
371+
ctx := &buildContext{
372+
buildOptions: buildOptions{
373+
Reporter: reporter,
374+
LocalCache: localCache,
375+
},
376+
}
377+
378+
// If we need to scan dependencies as well
379+
if withDependencies {
380+
// Get all dependencies
381+
deps := getAllDependencies(p)
382+
log.Infof("Scanning %s and %d dependencies for vulnerabilities", p.FullName(), len(deps))
383+
384+
// Add the package itself to the list
385+
packagesToScan := append([]*Package{p}, deps...)
386+
387+
// Call the existing function with all packages and the custom output directory
388+
return ScanAllPackagesForVulnerabilities(ctx, packagesToScan, outputDir)
389+
}
390+
391+
// Just scan the single package
392+
return ScanAllPackagesForVulnerabilities(ctx, []*Package{p}, outputDir)
393+
}
394+
364395
// ScanAllPackagesForVulnerabilities scans all packages for vulnerabilities
365396
// This function is called after the build process completes
366-
func ScanAllPackagesForVulnerabilities(buildctx *buildContext, packages []*Package) error {
397+
func ScanAllPackagesForVulnerabilities(buildctx *buildContext, packages []*Package, customOutputDir ...string) error {
367398
// Skip if no packages to scan
368399
if len(packages) == 0 {
369400
return nil
@@ -382,14 +413,29 @@ func ScanAllPackagesForVulnerabilities(buildctx *buildContext, packages []*Packa
382413
continue
383414
}
384415

385-
// Get the location for this package's vulnerability reports
386-
reportLocation := GetVulnerabilityReportLocation(p, timestamp)
387-
388-
// Create the directory for this package's vulnerability reports
389-
if err := os.MkdirAll(reportLocation.PackageDir, 0755); err != nil {
390-
errMsg := fmt.Sprintf("failed to create vulnerability reports directory for package %s: %s", p.FullName(), err)
391-
buildctx.Reporter.PackageBuildLog(p, true, []byte(errMsg+"\n"))
392-
return xerrors.Errorf(errMsg)
416+
// Determine the output directory
417+
var outputDir string
418+
if len(customOutputDir) > 0 && customOutputDir[0] != "" {
419+
// Use custom output directory if provided
420+
outputDir = customOutputDir[0]
421+
422+
// Create the output directory if it doesn't exist
423+
if err := os.MkdirAll(outputDir, 0755); err != nil {
424+
errMsg := fmt.Sprintf("failed to create output directory %s: %s", outputDir, err)
425+
buildctx.Reporter.PackageBuildLog(p, true, []byte(errMsg+"\n"))
426+
return xerrors.Errorf(errMsg)
427+
}
428+
} else {
429+
// Use default timestamp-based directory structure
430+
reportLocation := GetVulnerabilityReportLocation(p, timestamp)
431+
outputDir = reportLocation.PackageDir
432+
433+
// Create the directory for this package's vulnerability reports
434+
if err := os.MkdirAll(outputDir, 0755); err != nil {
435+
errMsg := fmt.Sprintf("failed to create vulnerability reports directory for package %s: %s", p.FullName(), err)
436+
buildctx.Reporter.PackageBuildLog(p, true, []byte(errMsg+"\n"))
437+
return xerrors.Errorf(errMsg)
438+
}
393439
}
394440

395441
// Find the SBOM file for this package
@@ -458,14 +504,14 @@ func ScanAllPackagesForVulnerabilities(buildctx *buildContext, packages []*Packa
458504
sbomFile = tempFileName
459505

460506
// Scan the package for vulnerabilities
461-
if err := ScanPackageForVulnerabilities(p, buildctx, sbomFile, reportLocation.PackageDir); err != nil {
507+
if err := ScanPackageForVulnerabilities(p, buildctx, sbomFile, outputDir); err != nil {
462508
buildctx.Reporter.PackageBuildLog(p, false, fmt.Appendf(nil, "Failed to scan package %s for vulnerabilities: %s\n", p.FullName(), err.Error()))
463509
// Add to failed packages
464510
failedPackages = append(failedPackages, p.FullName())
465511
continue
466512
}
467513

468-
buildctx.Reporter.PackageBuildLog(p, false, fmt.Appendf(nil, "Vulnerability scan completed for package %s (reports: %s)\n", p.FullName(), reportLocation.PackageDir))
514+
buildctx.Reporter.PackageBuildLog(p, false, fmt.Appendf(nil, "Vulnerability scan completed for package %s (reports: %s)\n", p.FullName(), outputDir))
469515
}
470516

471517
// Return error if any packages failed due to vulnerabilities
@@ -675,6 +721,33 @@ func loadVulnerabilityDB(p *Package, buildctx *buildContext) (vulnerability.Prov
675721
return provider, status, nil
676722
}
677723

724+
// Helper function to get all dependencies of a package
725+
func getAllDependencies(pkg *Package) []*Package {
726+
// Use a map to avoid duplicates
727+
depsMap := make(map[string]*Package)
728+
729+
// Recursively collect dependencies
730+
var collectDeps func(p *Package)
731+
collectDeps = func(p *Package) {
732+
for _, dep := range p.GetDependencies() {
733+
if _, exists := depsMap[dep.FullName()]; !exists {
734+
depsMap[dep.FullName()] = dep
735+
collectDeps(dep)
736+
}
737+
}
738+
}
739+
740+
collectDeps(pkg)
741+
742+
// Convert map to slice
743+
deps := make([]*Package, 0, len(depsMap))
744+
for _, dep := range depsMap {
745+
deps = append(deps, dep)
746+
}
747+
748+
return deps
749+
}
750+
678751
// ErrNoSBOMFile is returned when no SBOM file is found in a cached archive
679752
var ErrNoSBOMFile = fmt.Errorf("no SBOM file found")
680753

0 commit comments

Comments
 (0)