-
Notifications
You must be signed in to change notification settings - Fork 24
Expand file tree
/
Copy pathsbom-export.go
More file actions
211 lines (178 loc) · 6.98 KB
/
sbom-export.go
File metadata and controls
211 lines (178 loc) · 6.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
package cmd
import (
"context"
"io"
"os"
"path/filepath"
"strings"
"github.com/gitpod-io/leeway/pkg/leeway"
"github.com/gitpod-io/leeway/pkg/leeway/cache"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// sbomExportCmd represents the sbom export command
var sbomExportCmd = &cobra.Command{
Use: "export [package]",
Short: "Exports the SBOM of a (previously built) package",
Long: `Exports the SBOM of a (previously built) package.
When used with --with-dependencies, it exports SBOMs for the package and all its dependencies
to the specified output directory.
If no package is specified, the workspace's default target is used.`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// Get the package
_, pkg, _, _ := getTarget(args, false)
if pkg == nil {
log.Fatal("sbom export requires a package or a default target in the workspace")
}
// Get build options and cache
_, localCache := getBuildOpts(cmd)
// Get output format and file
format, _ := cmd.Flags().GetString("format")
outputFile, _ := cmd.Flags().GetString("output")
withDependencies, _ := cmd.Flags().GetBool("with-dependencies")
outputDir, _ := cmd.Flags().GetString("output-dir")
// Validate format using the utility function
formatValid, validFormats := leeway.ValidateSBOMFormat(format)
if !formatValid {
log.Fatalf("Unsupported format: %s. Supported formats are: %s", format, strings.Join(validFormats, ", "))
}
// Validate flags for dependency export
if withDependencies {
if outputDir == "" {
log.Fatal("--output-dir is required when using --with-dependencies")
}
if outputFile != "" {
log.Fatal("--output and --output-dir cannot be used together")
}
}
var allpkg []*leeway.Package
allpkg = append(allpkg, pkg)
if withDependencies {
// Get all dependencies
deps := pkg.GetTransitiveDependencies()
// Skip ephemeral packages as they're not meant to be cached
for _, p := range deps {
if p.Ephemeral {
log.Infof("Skipping vulnerability scan for ephemeral package %s\n", p.FullName())
continue
}
allpkg = append(allpkg, p)
}
log.Infof("Exporting SBOMs for %s and %d dependencies to %s", pkg.FullName(), len(allpkg)-1, outputDir)
}
for _, p := range allpkg {
var outputPath string
if outputFile == "" {
safeFilename := p.FilesystemSafeName()
outputPath = filepath.Join(outputDir, safeFilename+leeway.GetSBOMFileExtension(format))
} else {
outputPath = outputFile
}
exportSBOM(p, localCache, outputPath, format)
}
},
}
func init() {
sbomExportCmd.Flags().String("format", "cyclonedx", "SBOM format to export (cyclonedx, spdx, syft)")
sbomExportCmd.Flags().StringP("output", "o", "", "Output file (defaults to stdout)")
sbomExportCmd.Flags().Bool("with-dependencies", false, "Export SBOMs for the package and all its dependencies")
sbomExportCmd.Flags().String("output-dir", "", "Output directory for exporting multiple SBOMs (required with --with-dependencies)")
sbomCmd.AddCommand(sbomExportCmd)
addBuildFlags(sbomExportCmd)
}
// exportSBOM extracts and writes an SBOM from a package's cached archive.
// It retrieves the package from the cache, creates the output file if needed,
// and extracts the SBOM in the specified format. If outputFile is empty,
// the SBOM is written to stdout.
func exportSBOM(pkg *leeway.Package, localCache cache.LocalCache, outputFile string, format string) {
pkgFN := GetPackagePath(pkg, localCache)
var output io.Writer = os.Stdout
// Create directory if it doesn't exist
if dir := filepath.Dir(outputFile); dir != "" {
if err := os.MkdirAll(dir, 0755); err != nil {
log.WithError(err).Fatalf("cannot create output directory %s", dir)
}
}
file, err := os.Create(outputFile)
if err != nil {
log.WithError(err).Fatalf("cannot create output file %s", outputFile)
}
defer file.Close()
output = file
// Extract and output the SBOM
err = leeway.AccessSBOMInCachedArchive(pkgFN, format, func(sbomReader io.Reader) error {
log.Infof("Exporting SBOM in %s format", format)
_, err := io.Copy(output, sbomReader)
return err
})
if err != nil {
if err == leeway.ErrNoSBOMFile {
log.Fatalf("no SBOM file found in package %s", pkg.FullName())
}
log.WithError(err).Fatal("cannot extract SBOM")
}
if outputFile != "" {
log.Infof("SBOM exported to %s", outputFile)
}
}
// GetPackagePath retrieves the filesystem path to a package's cached archive.
// It first checks the local cache, and if not found, attempts to download
// the package from the remote cache. This function verifies that SBOM is enabled
// in the workspace settings and returns the path to the package archive.
// If the package cannot be found in either cache, it exits with a fatal error.
func GetPackagePath(pkg *leeway.Package, localCache cache.LocalCache) (packagePath string) {
// Check if SBOM is enabled in workspace settings
if !pkg.C.W.SBOM.Enabled {
log.Fatal("SBOM export/scan requires sbom.enabled=true in workspace settings")
}
if log.IsLevelEnabled(log.DebugLevel) {
v, err := pkg.Version()
if err != nil {
log.WithError(err).Fatal("error getting version")
}
log.Debugf("Exporting SBOM of package %s (version %s)", pkg.FullName(), v)
}
// Get package location in local cache
pkgFN, ok := localCache.Location(pkg)
if !ok {
// Package not found in local cache, check if it's in the remote cache
log.Debugf("Package %s not found in local cache, checking remote cache", pkg.FullName())
remoteCache := getRemoteCacheFromEnv()
remoteCache = &pullOnlyRemoteCache{C: remoteCache}
// Convert to cache.Package interface
pkgsToCheck := []cache.Package{pkg}
if log.IsLevelEnabled(log.DebugLevel) {
v, err := pkgsToCheck[0].Version()
if err != nil {
log.WithError(err).Fatal("error getting version")
}
log.Debugf("Checking remote of package %s (version %s)", pkgsToCheck[0].FullName(), v)
}
// Check if the package exists in the remote cache
existingPkgs, err := remoteCache.ExistingPackages(context.Background(), pkgsToCheck)
if err != nil {
log.WithError(err).Warnf("Failed to check if package %s exists in remote cache", pkg.FullName())
log.Fatalf("%s is not built", pkg.FullName())
} else {
_, existsInRemote := existingPkgs[pkg]
if existsInRemote {
log.Infof("Package %s found in remote cache, downloading...", pkg.FullName())
// Download the package from the remote cache
results := remoteCache.Download(context.Background(), localCache, pkgsToCheck)
if result, ok := results[pkg.FullName()]; ok && result.Status == cache.DownloadStatusFailed {
log.WithError(result.Err).Fatalf("Failed to download package %s from remote cache", pkg.FullName())
}
// Check if the download was successful
pkgFN, ok = localCache.Location(pkg)
if !ok {
log.Fatalf("Failed to download package %s from remote cache", pkg.FullName())
}
log.Infof("Successfully downloaded package %s from remote cache", pkg.FullName())
} else {
log.Fatalf("%s is not built", pkg.FullName())
}
}
}
return pkgFN
}