Skip to content

Commit 5d239bf

Browse files
authored
fix(cmd): add experimental config support to image command (#1547)
1 parent b870d62 commit 5d239bf

File tree

3 files changed

+125
-94
lines changed

3 files changed

+125
-94
lines changed

cmd/osv-scanner/internal/helper/helper.go

+82
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
package helper
22

33
import (
4+
"errors"
45
"fmt"
6+
"io"
57
"net/http"
8+
"os"
69
"os/exec"
710
"runtime"
811
"slices"
912
"strings"
1013
"time"
1114

15+
"github.com/google/osv-scanner/v2/internal/spdx"
16+
"github.com/google/osv-scanner/v2/pkg/osvscanner"
1217
"github.com/google/osv-scanner/v2/pkg/reporter"
1318
"github.com/urfave/cli/v2"
19+
"golang.org/x/term"
1420
)
1521

1622
// flags that require network access and values to disable them.
@@ -78,6 +84,14 @@ var GlobalScanFlags = []cli.Flag{
7884
// Disable the features requiring network access.
7985
for flag, value := range OfflineFlags {
8086
// TODO(michaelkedar): do something if the flag was already explicitly set.
87+
88+
// Skip setting the flag if the current command doesn't have it."
89+
if !slices.ContainsFunc(ctx.Command.Flags, func(f cli.Flag) bool {
90+
return slices.Contains(f.Names(), flag)
91+
}) {
92+
continue
93+
}
94+
8195
if err := ctx.Set(flag, value); err != nil {
8296
panic(fmt.Sprintf("failed setting offline flag %s to %s: %v", flag, value, err))
8397
}
@@ -155,3 +169,71 @@ func ServeHTML(r reporter.Reporter, outputPath string) {
155169
r.Errorf("Failed to start server: %v\n", err)
156170
}
157171
}
172+
173+
func GetScanLicensesAllowlist(context *cli.Context) ([]string, error) {
174+
if context.Bool("experimental-licenses-summary") && context.IsSet("experimental-licenses") {
175+
return nil, errors.New("--experimental-licenses-summary and --experimental-licenses flags cannot be set")
176+
}
177+
allowlist := context.StringSlice("experimental-licenses")
178+
if context.IsSet("experimental-licenses") {
179+
if len(allowlist) == 0 ||
180+
(len(allowlist) == 1 && allowlist[0] == "") {
181+
return nil, errors.New("--experimental-licenses requires at least one value")
182+
}
183+
if unrecognized := spdx.Unrecognized(allowlist); len(unrecognized) > 0 {
184+
return nil, fmt.Errorf("--experimental-licenses requires comma-separated spdx licenses. The following license(s) are not recognized as spdx: %s", strings.Join(unrecognized, ","))
185+
}
186+
}
187+
188+
scanLicensesAllowlist := context.StringSlice("experimental-licenses")
189+
if context.Bool("experimental-offline") {
190+
scanLicensesAllowlist = []string{}
191+
}
192+
193+
return scanLicensesAllowlist, nil
194+
}
195+
196+
func GetReporter(context *cli.Context, stdout, stderr io.Writer, outputPath, format string) (reporter.Reporter, error) {
197+
termWidth := 0
198+
var err error
199+
if outputPath != "" { // Output is definitely a file
200+
stdout, err = os.Create(outputPath)
201+
if err != nil {
202+
return nil, fmt.Errorf("failed to create output file: %w", err)
203+
}
204+
} else { // Output might be a terminal
205+
if stdoutAsFile, ok := stdout.(*os.File); ok {
206+
termWidth, _, err = term.GetSize(int(stdoutAsFile.Fd()))
207+
if err != nil { // If output is not a terminal,
208+
termWidth = 0
209+
}
210+
}
211+
}
212+
213+
verbosityLevel, err := reporter.ParseVerbosityLevel(context.String("verbosity"))
214+
if err != nil {
215+
return nil, err
216+
}
217+
r, err := reporter.New(format, stdout, stderr, verbosityLevel, termWidth)
218+
if err != nil {
219+
return r, err
220+
}
221+
222+
return r, nil
223+
}
224+
225+
func GetExperimentalScannerActions(context *cli.Context, scanLicensesAllowlist []string) osvscanner.ExperimentalScannerActions {
226+
return osvscanner.ExperimentalScannerActions{
227+
LocalDBPath: context.String("experimental-local-db-path"),
228+
DownloadDatabases: context.Bool("experimental-download-offline-databases"),
229+
CompareOffline: context.Bool("experimental-offline-vulnerabilities"),
230+
// License summary mode causes all
231+
// packages to appear in the json as
232+
// every package has a license - even
233+
// if it's just the UNKNOWN license.
234+
ShowAllPackages: context.Bool("experimental-all-packages") ||
235+
context.Bool("experimental-licenses-summary"),
236+
ScanLicensesSummary: context.Bool("experimental-licenses-summary"),
237+
ScanLicensesAllowlist: scanLicensesAllowlist,
238+
}
239+
}

cmd/osv-scanner/scan/image/main.go

+9-23
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"github.com/google/osv-scanner/v2/pkg/models"
1212
"github.com/google/osv-scanner/v2/pkg/osvscanner"
1313
"github.com/google/osv-scanner/v2/pkg/reporter"
14-
"golang.org/x/term"
1514

1615
"github.com/urfave/cli/v2"
1716
)
@@ -60,38 +59,25 @@ func action(context *cli.Context, stdout, stderr io.Writer) (reporter.Reporter,
6059
}
6160
}
6261

63-
termWidth := 0
64-
var err error
65-
if outputPath != "" { // Output is definitely a file
66-
stdout, err = os.Create(outputPath)
67-
if err != nil {
68-
return nil, fmt.Errorf("failed to create output file: %w", err)
69-
}
70-
} else { // Output might be a terminal
71-
if stdoutAsFile, ok := stdout.(*os.File); ok {
72-
termWidth, _, err = term.GetSize(int(stdoutAsFile.Fd()))
73-
if err != nil { // If output is not a terminal,
74-
termWidth = 0
75-
}
76-
}
77-
}
78-
79-
verbosityLevel, err := reporter.ParseVerbosityLevel(context.String("verbosity"))
62+
r, err := helper.GetReporter(context, stdout, stderr, outputPath, format)
8063
if err != nil {
8164
return nil, err
8265
}
83-
r, err := reporter.New(format, stdout, stderr, verbosityLevel, termWidth)
66+
67+
scanLicensesAllowlist, err := helper.GetScanLicensesAllowlist(context)
8468
if err != nil {
85-
return r, err
69+
return nil, err
8670
}
8771

8872
if context.Args().Len() == 0 {
8973
return r, errors.New("please provide an image name or see the help document")
9074
}
9175
scannerAction := osvscanner.ScannerActions{
92-
Image: context.Args().First(),
93-
ConfigOverridePath: context.String("config"),
94-
IsImageArchive: context.Bool("archive"),
76+
Image: context.Args().First(),
77+
ConfigOverridePath: context.String("config"),
78+
IsImageArchive: context.Bool("archive"),
79+
SkipGit: context.Bool("skip-git"),
80+
ExperimentalScannerActions: helper.GetExperimentalScannerActions(context, scanLicensesAllowlist),
9581
}
9682

9783
var vulnResult models.VulnerabilityResults

cmd/osv-scanner/scan/source/main.go

+34-71
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,12 @@ import (
66
"io"
77
"os"
88
"path/filepath"
9-
"strings"
109

1110
"github.com/google/osv-scanner/v2/cmd/osv-scanner/internal/helper"
12-
"github.com/google/osv-scanner/v2/internal/spdx"
1311
"github.com/google/osv-scanner/v2/pkg/models"
1412
"github.com/google/osv-scanner/v2/pkg/osvscanner"
1513
"github.com/google/osv-scanner/v2/pkg/reporter"
1614
"github.com/urfave/cli/v2"
17-
"golang.org/x/term"
1815
)
1916

2017
var projectScanFlags = []cli.Flag{
@@ -45,11 +42,6 @@ var projectScanFlags = []cli.Flag{
4542
Usage: "check subdirectories",
4643
Value: false,
4744
},
48-
&cli.BoolFlag{
49-
Name: "experimental-call-analysis",
50-
Usage: "[Deprecated] attempt call analysis on code to detect only active vulnerabilities",
51-
Value: false,
52-
},
5345
&cli.BoolFlag{
5446
Name: "no-ignore",
5547
Usage: "also scan files that would be ignored by .gitignore",
@@ -63,6 +55,9 @@ var projectScanFlags = []cli.Flag{
6355
Name: "no-call-analysis",
6456
Usage: "disables call graph analysis",
6557
},
58+
}
59+
60+
var projectScanExperimentalFlags = []cli.Flag{
6661
&cli.StringFlag{
6762
Name: "experimental-resolution-data-source",
6863
Usage: "source to fetch package information from; value can be: deps.dev, native",
@@ -79,14 +74,25 @@ var projectScanFlags = []cli.Flag{
7974
Name: "experimental-maven-registry",
8075
Usage: "URL of the default registry to fetch Maven metadata",
8176
},
77+
&cli.BoolFlag{
78+
Name: "experimental-call-analysis",
79+
Usage: "[Deprecated] attempt call analysis on code to detect only active vulnerabilities",
80+
Value: false,
81+
},
8282
}
8383

8484
func Command(stdout, stderr io.Writer, r *reporter.Reporter) *cli.Command {
85+
flags := make([]cli.Flag, 0, len(projectScanFlags)+len(helper.GlobalScanFlags)+len(projectScanExperimentalFlags))
86+
flags = append(flags, projectScanFlags...)
87+
flags = append(flags, helper.GlobalScanFlags...)
88+
// Make sure all experimental flags show after regular flags
89+
flags = append(flags, projectScanExperimentalFlags...)
90+
8591
return &cli.Command{
8692
Name: "source",
8793
Usage: "scans a source project's dependencies for known vulnerabilities using the OSV database.",
8894
Description: "scans a source project's dependencies for known vulnerabilities using the OSV database.",
89-
Flags: append(projectScanFlags, helper.GlobalScanFlags...),
95+
Flags: flags,
9096
ArgsUsage: "[directory1 directory2...]",
9197
Action: func(c *cli.Context) error {
9298
var err error
@@ -122,43 +128,14 @@ func Action(context *cli.Context, stdout, stderr io.Writer) (reporter.Reporter,
122128
}
123129
}
124130

125-
termWidth := 0
126-
var err error
127-
if outputPath != "" { // Output is definitely a file
128-
stdout, err = os.Create(outputPath)
129-
if err != nil {
130-
return nil, fmt.Errorf("failed to create output file: %w", err)
131-
}
132-
} else { // Output might be a terminal
133-
if stdoutAsFile, ok := stdout.(*os.File); ok {
134-
termWidth, _, err = term.GetSize(int(stdoutAsFile.Fd()))
135-
if err != nil { // If output is not a terminal,
136-
termWidth = 0
137-
}
138-
}
139-
}
140-
141-
if context.Bool("experimental-licenses-summary") && context.IsSet("experimental-licenses") {
142-
return nil, errors.New("--experimental-licenses-summary and --experimental-licenses flags cannot be set")
143-
}
144-
allowlist := context.StringSlice("experimental-licenses")
145-
if context.IsSet("experimental-licenses") {
146-
if len(allowlist) == 0 ||
147-
(len(allowlist) == 1 && allowlist[0] == "") {
148-
return nil, errors.New("--experimental-licenses requires at least one value")
149-
}
150-
if unrecognized := spdx.Unrecognized(allowlist); len(unrecognized) > 0 {
151-
return nil, fmt.Errorf("--experimental-licenses requires comma-separated spdx licenses. The following license(s) are not recognized as spdx: %s", strings.Join(unrecognized, ","))
152-
}
153-
}
154-
155-
verbosityLevel, err := reporter.ParseVerbosityLevel(context.String("verbosity"))
131+
r, err := helper.GetReporter(context, stdout, stderr, outputPath, format)
156132
if err != nil {
157133
return nil, err
158134
}
159-
r, err := reporter.New(format, stdout, stderr, verbosityLevel, termWidth)
135+
136+
scanLicensesAllowlist, err := helper.GetScanLicensesAllowlist(context)
160137
if err != nil {
161-
return r, err
138+
return nil, err
162139
}
163140

164141
var callAnalysisStates map[string]bool
@@ -169,38 +146,24 @@ func Action(context *cli.Context, stdout, stderr io.Writer) (reporter.Reporter,
169146
callAnalysisStates = helper.CreateCallAnalysisStates(context.StringSlice("call-analysis"), context.StringSlice("no-call-analysis"))
170147
}
171148

172-
scanLicensesAllowlist := context.StringSlice("experimental-licenses")
173-
if context.Bool("experimental-offline") {
174-
scanLicensesAllowlist = []string{}
149+
experimentalScannerActions := helper.GetExperimentalScannerActions(context, scanLicensesAllowlist)
150+
// Add `source` specific experimental configs
151+
experimentalScannerActions.TransitiveScanningActions = osvscanner.TransitiveScanningActions{
152+
Disabled: context.Bool("experimental-no-resolve"),
153+
NativeDataSource: context.String("experimental-resolution-data-source") == "native",
154+
MavenRegistry: context.String("experimental-maven-registry"),
175155
}
176156

177157
scannerAction := osvscanner.ScannerActions{
178-
LockfilePaths: context.StringSlice("lockfile"),
179-
SBOMPaths: context.StringSlice("sbom"),
180-
Recursive: context.Bool("recursive"),
181-
SkipGit: context.Bool("skip-git"),
182-
NoIgnore: context.Bool("no-ignore"),
183-
ConfigOverridePath: context.String("config"),
184-
DirectoryPaths: context.Args().Slice(),
185-
CallAnalysisStates: callAnalysisStates,
186-
ExperimentalScannerActions: osvscanner.ExperimentalScannerActions{
187-
LocalDBPath: context.String("experimental-local-db-path"),
188-
DownloadDatabases: context.Bool("experimental-download-offline-databases"),
189-
CompareOffline: context.Bool("experimental-offline-vulnerabilities"),
190-
// License summary mode causes all
191-
// packages to appear in the json as
192-
// every package has a license - even
193-
// if it's just the UNKNOWN license.
194-
ShowAllPackages: context.Bool("experimental-all-packages") ||
195-
context.Bool("experimental-licenses-summary"),
196-
ScanLicensesSummary: context.Bool("experimental-licenses-summary"),
197-
ScanLicensesAllowlist: scanLicensesAllowlist,
198-
TransitiveScanningActions: osvscanner.TransitiveScanningActions{
199-
Disabled: context.Bool("experimental-no-resolve"),
200-
NativeDataSource: context.String("experimental-resolution-data-source") == "native",
201-
MavenRegistry: context.String("experimental-maven-registry"),
202-
},
203-
},
158+
LockfilePaths: context.StringSlice("lockfile"),
159+
SBOMPaths: context.StringSlice("sbom"),
160+
Recursive: context.Bool("recursive"),
161+
SkipGit: context.Bool("skip-git"),
162+
NoIgnore: context.Bool("no-ignore"),
163+
ConfigOverridePath: context.String("config"),
164+
DirectoryPaths: context.Args().Slice(),
165+
CallAnalysisStates: callAnalysisStates,
166+
ExperimentalScannerActions: experimentalScannerActions,
204167
}
205168

206169
var vulnResult models.VulnerabilityResults

0 commit comments

Comments
 (0)