mirror of
https://github.com/anchore/grype
synced 2024-11-10 06:34:13 +00:00
Support for SBOMs with incomplete linux distribution or CPE information (#606)
This commit is contained in:
parent
ad9918a681
commit
fc8e13f5b8
13 changed files with 196 additions and 51 deletions
40
README.md
40
README.md
|
@ -91,19 +91,6 @@ grype path/to/image.tar
|
|||
grype dir:path/to/dir
|
||||
```
|
||||
|
||||
Use [Syft](https://github.com/anchore/syft) SBOMs for even faster vulnerability scanning in Grype:
|
||||
|
||||
```
|
||||
# Just need to generate the SBOM once
|
||||
syft <image> -o json > ./image-sbom.json
|
||||
|
||||
# Then scan for new vulnerabilities as frequently as needed
|
||||
grype sbom:./image-sbom.json
|
||||
|
||||
# (You can also pipe the SBOM into Grype)
|
||||
cat ./image-sbom.json | grype
|
||||
```
|
||||
|
||||
Sources can be explicitly provided with a scheme:
|
||||
```
|
||||
docker:yourrepo/yourimage:tag use images from the Docker daemon
|
||||
|
@ -114,6 +101,27 @@ dir:path/to/yourproject read directly from a path on disk (any di
|
|||
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||
```
|
||||
|
||||
Use SBOMs for even faster vulnerability scanning in Grype:
|
||||
|
||||
```
|
||||
# Then scan for new vulnerabilities as frequently as needed
|
||||
grype sbom:./sbom.json
|
||||
|
||||
# (You can also pipe the SBOM into Grype)
|
||||
cat ./sbom.json | grype
|
||||
```
|
||||
|
||||
Grype supports input of [Syft](https://github.com/anchore/syft), [SPDX](https://spdx.dev/), and [CycloneDX](https://cyclonedx.org/)
|
||||
SBOM formats. If Syft has generated any of these file types, they should have the appropriate information to work properly with Grype.
|
||||
It is also possible to use SBOMs generated by other tools with varying degrees of success. Two things that make Grype matching
|
||||
more successful are inclusion of CPE and Linux distribution information. If an SBOM does not include any CPE information, it
|
||||
is possible to generate these based on package information using the `--add-cpes-if-none` flag. To specify a distribution,
|
||||
use the `--distro <distro>:<version>` flag. A full example is:
|
||||
|
||||
```
|
||||
grype --add-cpes-if-none --distro alpine:3.10 sbom:some-apline-3.10.spdx.json
|
||||
```
|
||||
|
||||
### Vulnerability Summary
|
||||
|
||||
#### Basic Grype Vulnerability Data Shape
|
||||
|
@ -485,6 +493,11 @@ file: ""
|
|||
# same as --exclude ; GRYPE_EXCLUDE env var
|
||||
exclude:
|
||||
|
||||
# If using SBOM input, automatically generate CPEs when packages have none
|
||||
add-cpes-if-none: false
|
||||
|
||||
# Explicitly specify a linux distribution to use as <distro>:<version> like alpine:3.10
|
||||
distro:
|
||||
|
||||
db:
|
||||
# check for database updates on execution
|
||||
|
@ -562,4 +575,3 @@ log:
|
|||
The following areas of potential development are currently being investigated:
|
||||
|
||||
- Support for allowlist, package mapping
|
||||
- Accept alternative SBOM formats (CycloneDX, SPDX) as input
|
||||
|
|
65
cmd/root.go
65
cmd/root.go
|
@ -3,6 +3,7 @@ package cmd
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/profile"
|
||||
|
@ -28,6 +29,7 @@ import (
|
|||
"github.com/anchore/grype/internal/ui"
|
||||
"github.com/anchore/grype/internal/version"
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
|
@ -118,6 +120,16 @@ func setRootFlags(flags *pflag.FlagSet) {
|
|||
"file to write the report output to (default is STDOUT)",
|
||||
)
|
||||
|
||||
flags.StringP(
|
||||
"distro", "", "",
|
||||
"distro to match against in the format: <distro>:<version>",
|
||||
)
|
||||
|
||||
flags.BoolP(
|
||||
"add-cpes-if-none", "", false,
|
||||
"generate CPEs for packages with no CPE data",
|
||||
)
|
||||
|
||||
flags.StringP("template", "t", "", "specify the path to a Go template file ("+
|
||||
"requires 'template' output to be selected)")
|
||||
|
||||
|
@ -150,6 +162,14 @@ func bindRootConfigOptions(flags *pflag.FlagSet) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("distro", flags.Lookup("distro")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("add-cpes-if-none", flags.Lookup("add-cpes-if-none")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("output-template-file", flags.Lookup("template")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -260,12 +280,7 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
|
|||
go func() {
|
||||
defer wg.Done()
|
||||
log.Debugf("gathering packages")
|
||||
providerConfig := pkg.ProviderConfig{
|
||||
RegistryOptions: appConfig.Registry.ToOptions(),
|
||||
Exclusions: appConfig.Exclusions,
|
||||
CatalogingOptions: appConfig.Search.ToConfig(),
|
||||
}
|
||||
packages, context, err = pkg.Provide(userInput, providerConfig)
|
||||
packages, context, err = pkg.Provide(userInput, getProviderConfig())
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to catalog: %w", err)
|
||||
return
|
||||
|
@ -282,6 +297,8 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
|
|||
appConfig.Ignore = append(appConfig.Ignore, ignoreNonFixedMatches...)
|
||||
}
|
||||
|
||||
applyDistroHint(&context, appConfig)
|
||||
|
||||
allMatches := grype.FindVulnerabilitiesForPackage(provider, context.Distro, packages...)
|
||||
remainingMatches, ignoredMatches := match.ApplyIgnoreRules(allMatches, appConfig.Ignore)
|
||||
|
||||
|
@ -304,6 +321,42 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
|
|||
return errs
|
||||
}
|
||||
|
||||
func applyDistroHint(context *pkg.Context, appConfig *config.Application) {
|
||||
if appConfig.Distro != "" {
|
||||
log.Infof("using distro: %s", appConfig.Distro)
|
||||
|
||||
split := strings.Split(appConfig.Distro, ":")
|
||||
d := split[0]
|
||||
v := ""
|
||||
if len(split) > 1 {
|
||||
v = split[1]
|
||||
}
|
||||
context.Distro = &linux.Release{
|
||||
PrettyName: d,
|
||||
Name: d,
|
||||
ID: d,
|
||||
IDLike: []string{
|
||||
d,
|
||||
},
|
||||
Version: v,
|
||||
VersionID: v,
|
||||
}
|
||||
}
|
||||
|
||||
if context.Distro == nil {
|
||||
log.Warnf("Unable to determine the OS distribution. This may result in missing vulnerabilities. You may specify a distro using: --distro <distro>:<version>")
|
||||
}
|
||||
}
|
||||
|
||||
func getProviderConfig() pkg.ProviderConfig {
|
||||
return pkg.ProviderConfig{
|
||||
RegistryOptions: appConfig.Registry.ToOptions(),
|
||||
Exclusions: appConfig.Exclusions,
|
||||
CatalogingOptions: appConfig.Search.ToConfig(),
|
||||
GenerateMissingCPEs: appConfig.GenerateMissingCPEs,
|
||||
}
|
||||
}
|
||||
|
||||
func validateDBLoad(loadErr error, status *db.Status) error {
|
||||
if loadErr != nil {
|
||||
return fmt.Errorf("failed to load vulnerability db: %w", loadErr)
|
||||
|
|
|
@ -4,12 +4,14 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/grype/grype/db"
|
||||
grypeDB "github.com/anchore/grype/grype/db/v3"
|
||||
"github.com/anchore/grype/grype/match"
|
||||
"github.com/anchore/grype/grype/pkg"
|
||||
"github.com/anchore/grype/grype/vulnerability"
|
||||
"github.com/anchore/grype/internal/config"
|
||||
syftPkg "github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
|
@ -112,3 +114,35 @@ func TestAboveAllowableSeverity(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_applyDistroHint(t *testing.T) {
|
||||
ctx := pkg.Context{}
|
||||
cfg := config.Application{}
|
||||
|
||||
applyDistroHint(&ctx, &cfg)
|
||||
assert.Nil(t, ctx.Distro)
|
||||
|
||||
// works when distro is nil
|
||||
cfg.Distro = "alpine:3.10"
|
||||
applyDistroHint(&ctx, &cfg)
|
||||
assert.NotNil(t, ctx.Distro)
|
||||
|
||||
assert.Equal(t, "alpine", ctx.Distro.Name)
|
||||
assert.Equal(t, "3.10", ctx.Distro.Version)
|
||||
|
||||
// does override an existing distro
|
||||
cfg.Distro = "ubuntu:latest"
|
||||
applyDistroHint(&ctx, &cfg)
|
||||
assert.NotNil(t, ctx.Distro)
|
||||
|
||||
assert.Equal(t, "ubuntu", ctx.Distro.Name)
|
||||
assert.Equal(t, "latest", ctx.Distro.Version)
|
||||
|
||||
// doesn't remove an existing distro when empty
|
||||
cfg.Distro = ""
|
||||
applyDistroHint(&ctx, &cfg)
|
||||
assert.NotNil(t, ctx.Distro)
|
||||
|
||||
assert.Equal(t, "ubuntu", ctx.Distro.Name)
|
||||
assert.Equal(t, "latest", ctx.Distro.Version)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/log"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/common/cpe"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
|
@ -56,11 +57,24 @@ func New(p pkg.Package) Package {
|
|||
}
|
||||
}
|
||||
|
||||
func FromCatalog(catalog *pkg.Catalog) []Package {
|
||||
func FromCatalog(catalog *pkg.Catalog, config ProviderConfig) []Package {
|
||||
result := make([]Package, 0, catalog.PackageCount())
|
||||
missingCPEs := false
|
||||
for _, p := range catalog.Sorted() {
|
||||
if len(p.CPEs) == 0 {
|
||||
// For SPDX (or any format, really) we may have no CPEs
|
||||
if config.GenerateMissingCPEs {
|
||||
p.CPEs = cpe.Generate(p)
|
||||
} else {
|
||||
log.Debugf("no CPEs for package: %s", p)
|
||||
missingCPEs = true
|
||||
}
|
||||
}
|
||||
result = append(result, New(p))
|
||||
}
|
||||
if missingCPEs {
|
||||
log.Warnf("some package(s) are missing CPEs. This may result in missing vulnerabilities. You may autogenerate these using: --add-cpes-if-none")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
|
|
@ -288,10 +288,39 @@ func TestFromCatalog_DoesNotPanic(t *testing.T) {
|
|||
catalog.Add(examplePackage)
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
_ = FromCatalog(catalog)
|
||||
_ = FromCatalog(catalog, ProviderConfig{})
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromCatalog_GeneratesCPEs(t *testing.T) {
|
||||
catalog := syftPkg.NewCatalog()
|
||||
|
||||
catalog.Add(syftPkg.Package{
|
||||
Name: "first",
|
||||
Version: "1",
|
||||
CPEs: []syftPkg.CPE{
|
||||
{},
|
||||
},
|
||||
})
|
||||
|
||||
catalog.Add(syftPkg.Package{
|
||||
Name: "second",
|
||||
Version: "2",
|
||||
})
|
||||
|
||||
// doesn't generate cpes when no flag
|
||||
pkgs := FromCatalog(catalog, ProviderConfig{})
|
||||
assert.Len(t, pkgs[0].CPEs, 1)
|
||||
assert.Len(t, pkgs[1].CPEs, 0)
|
||||
|
||||
// does generate cpes with the flag
|
||||
pkgs = FromCatalog(catalog, ProviderConfig{
|
||||
GenerateMissingCPEs: true,
|
||||
})
|
||||
assert.Len(t, pkgs[0].CPEs, 1)
|
||||
assert.Len(t, pkgs[1].CPEs, 1)
|
||||
}
|
||||
|
||||
func Test_getNameAndELVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
@ -13,7 +13,7 @@ var errDoesNotProvide = fmt.Errorf("cannot provide packages from the given sourc
|
|||
|
||||
// Provide a set of packages and context metadata describing where they were sourced from.
|
||||
func Provide(userInput string, config ProviderConfig) ([]Package, Context, error) {
|
||||
packages, ctx, err := syftSBOMProvider(userInput)
|
||||
packages, ctx, err := syftSBOMProvider(userInput, config)
|
||||
if !errors.Is(err, errDoesNotProvide) {
|
||||
if len(config.Exclusions) > 0 {
|
||||
packages, err = filterPackageExclusions(packages, config.Exclusions)
|
||||
|
|
|
@ -6,7 +6,8 @@ import (
|
|||
)
|
||||
|
||||
type ProviderConfig struct {
|
||||
RegistryOptions *image.RegistryOptions
|
||||
Exclusions []string
|
||||
CatalogingOptions cataloger.Config
|
||||
RegistryOptions *image.RegistryOptions
|
||||
Exclusions []string
|
||||
CatalogingOptions cataloger.Config
|
||||
GenerateMissingCPEs bool
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ func syftProvider(userInput string, config ProviderConfig) ([]Package, Context,
|
|||
return nil, Context{}, err
|
||||
}
|
||||
|
||||
return FromCatalog(catalog), Context{
|
||||
return FromCatalog(catalog, config), Context{
|
||||
Source: &src.Metadata,
|
||||
Distro: theDistro,
|
||||
}, nil
|
||||
|
|
|
@ -15,7 +15,7 @@ import (
|
|||
"github.com/anchore/syft/syft/format"
|
||||
)
|
||||
|
||||
func syftSBOMProvider(userInput string) ([]Package, Context, error) {
|
||||
func syftSBOMProvider(userInput string, config ProviderConfig) ([]Package, Context, error) {
|
||||
reader, err := getSBOMReader(userInput)
|
||||
if err != nil {
|
||||
return nil, Context{}, err
|
||||
|
@ -29,7 +29,7 @@ func syftSBOMProvider(userInput string) ([]Package, Context, error) {
|
|||
return nil, Context{}, errDoesNotProvide
|
||||
}
|
||||
|
||||
return FromCatalog(sbom.Artifacts.PackageCatalog), Context{
|
||||
return FromCatalog(sbom.Artifacts.PackageCatalog, config), Context{
|
||||
Source: &sbom.Source,
|
||||
Distro: sbom.Artifacts.LinuxDistribution,
|
||||
}, nil
|
||||
|
|
|
@ -140,7 +140,7 @@ func TestParseSyftJSON(t *testing.T) {
|
|||
|
||||
for _, test := range tests {
|
||||
t.Run(test.Fixture, func(t *testing.T) {
|
||||
pkgs, context, err := syftSBOMProvider(test.Fixture)
|
||||
pkgs, context, err := syftSBOMProvider(test.Fixture, ProviderConfig{})
|
||||
if err != nil {
|
||||
t.Fatalf("unable to parse: %+v", err)
|
||||
}
|
||||
|
@ -169,7 +169,7 @@ func TestParseSyftJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseSyftJSON_BadCPEs(t *testing.T) {
|
||||
pkgs, _, err := syftSBOMProvider("test-fixtures/syft-java-bad-cpes.json")
|
||||
pkgs, _, err := syftSBOMProvider("test-fixtures/syft-java-bad-cpes.json", ProviderConfig{})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, pkgs, 1)
|
||||
}
|
||||
|
|
|
@ -311,7 +311,7 @@ func TestJsonDirsPresenter(t *testing.T) {
|
|||
Version: "8.0",
|
||||
},
|
||||
}
|
||||
pres := NewPresenter(matches, nil, pkg.FromCatalog(catalog), ctx, models.NewMetadataMock(), nil, nil)
|
||||
pres := NewPresenter(matches, nil, pkg.FromCatalog(catalog, pkg.ProviderConfig{}), ctx, models.NewMetadataMock(), nil, nil)
|
||||
|
||||
// TODO: add a constructor for a match.Match when the data is better shaped
|
||||
|
||||
|
|
|
@ -29,23 +29,25 @@ type parser interface {
|
|||
}
|
||||
|
||||
type Application struct {
|
||||
ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading)
|
||||
Output string `yaml:"output" json:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting
|
||||
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
|
||||
OutputTemplateFile string `yaml:"output-template-file" json:"output-template-file" mapstructure:"output-template-file"` // -t, the template file to use for formatting the final report
|
||||
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI)
|
||||
CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
|
||||
OnlyFixed bool `yaml:"only-fixed" json:"only-fixed" mapstructure:"only-fixed"` // only fail if detected vulns have a fix
|
||||
CliOptions CliOnlyOptions `yaml:"-" json:"-"`
|
||||
Search search `yaml:"search" json:"search" mapstructure:"search"`
|
||||
Ignore []match.IgnoreRule `yaml:"ignore" json:"ignore" mapstructure:"ignore"`
|
||||
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
|
||||
DB database `yaml:"db" json:"db" mapstructure:"db"`
|
||||
Dev development `yaml:"dev" json:"dev" mapstructure:"dev"`
|
||||
FailOn string `yaml:"fail-on-severity" json:"fail-on-severity" mapstructure:"fail-on-severity"`
|
||||
FailOnSeverity *vulnerability.Severity `yaml:"-" json:"-"`
|
||||
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
|
||||
Log logging `yaml:"log" json:"log" mapstructure:"log"`
|
||||
ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading)
|
||||
Output string `yaml:"output" json:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting
|
||||
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
|
||||
Distro string `yaml:"distro" json:"distro" mapstructure:"distro"` // --distro, specify a distro to explicitly use
|
||||
GenerateMissingCPEs bool `yaml:"add-cpes-if-none" json:"add-cpes-if-none" mapstructure:"add-cpes-if-none"` // --add-cpes-if-none, automatically generate CPEs if they are not present in import (e.g. from a 3rd party SPDX document)
|
||||
OutputTemplateFile string `yaml:"output-template-file" json:"output-template-file" mapstructure:"output-template-file"` // -t, the template file to use for formatting the final report
|
||||
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI)
|
||||
CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
|
||||
OnlyFixed bool `yaml:"only-fixed" json:"only-fixed" mapstructure:"only-fixed"` // only fail if detected vulns have a fix
|
||||
CliOptions CliOnlyOptions `yaml:"-" json:"-"`
|
||||
Search search `yaml:"search" json:"search" mapstructure:"search"`
|
||||
Ignore []match.IgnoreRule `yaml:"ignore" json:"ignore" mapstructure:"ignore"`
|
||||
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
|
||||
DB database `yaml:"db" json:"db" mapstructure:"db"`
|
||||
Dev development `yaml:"dev" json:"dev" mapstructure:"dev"`
|
||||
FailOn string `yaml:"fail-on-severity" json:"fail-on-severity" mapstructure:"fail-on-severity"`
|
||||
FailOnSeverity *vulnerability.Severity `yaml:"-" json:"-"`
|
||||
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
|
||||
Log logging `yaml:"log" json:"log" mapstructure:"log"`
|
||||
}
|
||||
|
||||
func newApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) *Application {
|
||||
|
@ -150,7 +152,7 @@ func (cfg *Application) parseLogLevelOption() error {
|
|||
case v >= 2:
|
||||
cfg.Log.LevelOpt = logrus.DebugLevel
|
||||
default:
|
||||
cfg.Log.LevelOpt = logrus.ErrorLevel
|
||||
cfg.Log.LevelOpt = logrus.WarnLevel
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -376,7 +376,7 @@ func TestMatchByImage(t *testing.T) {
|
|||
actualResults := grype.FindVulnerabilitiesForPackage(
|
||||
db.NewVulnerabilityProvider(theStore),
|
||||
theDistro,
|
||||
pkg.FromCatalog(theCatalog)...,
|
||||
pkg.FromCatalog(theCatalog, pkg.ProviderConfig{})...,
|
||||
)
|
||||
|
||||
// build expected matches from what's discovered from the catalog
|
||||
|
|
Loading…
Reference in a new issue