Skip to content

Commit

Permalink
feat: add SBOM analyzer (aquasecurity#4210)
Browse files Browse the repository at this point in the history
Co-authored-by: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com>
Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io>
  • Loading branch information
3 people committed Jun 2, 2023
1 parent dadd1e1 commit 9ef0113
Show file tree
Hide file tree
Showing 20 changed files with 660 additions and 46 deletions.
39 changes: 33 additions & 6 deletions docs/docs/supply-chain/sbom.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# SBOM generation
# SBOM

## Generating

Trivy can generate the following SBOM formats.

- [CycloneDX](#cyclonedx)
- [SPDX](#spdx)

## CLI commands
### CLI commands
To generate SBOM, you can use the `--format` option for each subcommand such as `image`, `fs` and `vm`.

```
Expand Down Expand Up @@ -177,7 +179,7 @@ $ trivy fs --format cyclonedx --output result.json /app/myproject

</details>

## Supported packages
### Supported packages
Trivy supports the following packages.

- [OS packages][os_packages]
Expand All @@ -196,8 +198,8 @@ In addition to the above packages, Trivy also supports the following packages fo
[^1]: Use `startline == 1 and endline == 1` for unsupported file types
[^2]: `envs/*/conda-meta/*.json`

## Formats
### CycloneDX
### Formats
#### CycloneDX
Trivy can generate SBOM in the [CycloneDX][cyclonedx] format.
Note that XML format is not supported at the moment.

Expand Down Expand Up @@ -442,7 +444,7 @@ If you want to include vulnerabilities, you can enable vulnerability scanning vi
$ trivy image --scanners vuln --format cyclonedx --output result.json alpine:3.15
```

### SPDX
#### SPDX
Trivy can generate SBOM in the [SPDX][spdx] format.

You can use the regular subcommands (like `image`, `fs` and `rootfs`) and specify `spdx` with the `--format` option.
Expand Down Expand Up @@ -737,6 +739,31 @@ $ cat result.spdx.json | jq .

</details>

## Scanning
Trivy can take SBOM documents as input for scanning.
See [here](../target/sbom.md) for more details.

Also, Trivy searches for SBOM files in container images.

```bash
$ trivy image bitnami/elasticsearch:8.7.1
```

For example, [Bitnami images](https://github.com/bitnami/containers) contain SBOM files in `/opt/bitnami` directory.
Trivy automatically detects the SBOM files and uses them for scanning.
It is enabled in the following targets.

| Target | Enabled |
|:---------------:|:-------:|
| Container Image ||
| Filesystem | |
| Rootfs ||
| Git Repository | |
| VM Image ||
| Kubernetes | |
| AWS | |
| SBOM | |


[spdx]: https://spdx.dev/wp-content/uploads/sites/41/2020/08/SPDX-specification-2-2.pdf

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@
{
"VulnerabilityID": "CVE-2020-8165",
"PkgName": "activesupport",
"PkgPath": "var/lib/gems/2.5.0/specifications/activesupport-6.0.2.1.gemspec",
"InstalledVersion": "6.0.2.1",
"FixedVersion": "6.0.3.1, 5.2.4.3",
"Layer": {},
Expand Down
17 changes: 9 additions & 8 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,14 @@ nav:
- Docs:
- Overview: docs/index.md
- Target:
Container Image: docs/target/container_image.md
Filesystem: docs/target/filesystem.md
Rootfs: docs/target/rootfs.md
Git Repository: docs/target/git-repository.md
Virtual Machine Image: docs/target/vm.md
Kubernetes: docs/target/kubernetes.md
AWS: docs/target/aws.md
SBOM: docs/target/sbom.md
- Container Image: docs/target/container_image.md
- Filesystem: docs/target/filesystem.md
- Rootfs: docs/target/rootfs.md
- Git Repository: docs/target/git-repository.md
- Virtual Machine Image: docs/target/vm.md
- Kubernetes: docs/target/kubernetes.md
- AWS: docs/target/aws.md
- SBOM: docs/target/sbom.md
- Scanner:
- Vulnerability:
- Overview: docs/scanner/vulnerability/index.md
Expand Down Expand Up @@ -110,6 +110,7 @@ nav:
- Overview: docs/references/configuration/cli/trivy.md
- AWS: docs/references/configuration/cli/trivy_aws.md
- Config: docs/references/configuration/cli/trivy_config.md
- Convert: docs/references/configuration/cli/trivy_convert.md
- Filesystem: docs/references/configuration/cli/trivy_filesystem.md
- Image: docs/references/configuration/cli/trivy_image.md
- Kubernetes: docs/references/configuration/cli/trivy_kubernetes.md
Expand Down
6 changes: 4 additions & 2 deletions pkg/commands/artifact/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,9 @@ func (r *runner) ScanImage(ctx context.Context, opts flag.Options) (types.Report
}

func (r *runner) ScanFilesystem(ctx context.Context, opts flag.Options) (types.Report, error) {
// Disable the individual package scanning
// Disable scanning of individual package and SBOM files
opts.DisabledAnalyzers = append(opts.DisabledAnalyzers, analyzer.TypeIndividualPkgs...)
opts.DisabledAnalyzers = append(opts.DisabledAnalyzers, analyzer.TypeSBOM)

return r.scanFS(ctx, opts)
}
Expand Down Expand Up @@ -217,8 +218,9 @@ func (r *runner) ScanRepository(ctx context.Context, opts flag.Options) (types.R
// Do not scan OS packages
opts.VulnType = []string{types.VulnTypeLibrary}

// Disable the OS analyzers and individual package analyzers
// Disable the OS analyzers, individual package analyzers and SBOM analyzer
opts.DisabledAnalyzers = append(analyzer.TypeIndividualPkgs, analyzer.TypeOSes...)
opts.DisabledAnalyzers = append(opts.DisabledAnalyzers, analyzer.TypeSBOM)

var s InitializeScanner
if opts.ServerAddr == "" {
Expand Down
1 change: 1 addition & 0 deletions pkg/fanal/analyzer/all/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,6 @@ import (
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/dpkg"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/rpm"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/repo/apk"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/sbom"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/secret"
)
15 changes: 14 additions & 1 deletion pkg/fanal/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,20 @@ func (ag AnalyzerGroup) PostAnalyze(ctx context.Context, files *syncx.Map[Type,
continue
}

filteredFS, err := fsys.Filter(result.SystemInstalledFiles)
skippedFiles := result.SystemInstalledFiles
for _, app := range result.Applications {
skippedFiles = append(skippedFiles, app.FilePath)
for _, lib := range app.Libraries {
// The analysis result could contain packages listed in SBOM.
// The files of those packages don't have to be analyzed.
// This is especially helpful for expensive post-analyzers such as the JAR analyzer.
if lib.FilePath != "" {
skippedFiles = append(skippedFiles, lib.FilePath)
}
}
}

filteredFS, err := fsys.Filter(skippedFiles)
if err != nil {
return xerrors.Errorf("unable to filter filesystem: %w", err)
}
Expand Down
1 change: 1 addition & 0 deletions pkg/fanal/analyzer/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ const (
// Non-packaged
// ============
TypeExecutable Type = "executable"
TypeSBOM Type = "sbom"

// ============
// Image Config
Expand Down
4 changes: 0 additions & 4 deletions pkg/fanal/analyzer/language/python/packaging/packaging.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"context"
"io"
"os"
"path/filepath"
"strings"

"golang.org/x/xerrors"
Expand Down Expand Up @@ -99,9 +98,6 @@ func (a packagingAnalyzer) open(file *zip.File) (dio.ReadSeekerAt, error) {
}

func (a packagingAnalyzer) Required(filePath string, _ os.FileInfo) bool {
// For Windows
filePath = filepath.ToSlash(filePath)

for _, r := range requiredFiles {
if strings.HasSuffix(filePath, r) {
return true
Expand Down
85 changes: 85 additions & 0 deletions pkg/fanal/analyzer/sbom/sbom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package sbom

import (
"context"
"os"
"path"
"strings"

"golang.org/x/xerrors"

"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/sbom"
)

func init() {
analyzer.RegisterAnalyzer(&sbomAnalyzer{})
}

const version = 1

var requiredSuffixes = []string{
".spdx",
".spdx.json",
".cdx",
".cdx.json",
}

type sbomAnalyzer struct{}

func (a sbomAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) {
// Format auto-detection
format, err := sbom.DetectFormat(input.Content)
if err != nil {
return nil, xerrors.Errorf("failed to detect SBOM format: %w", err)
}

bom, err := sbom.Decode(input.Content, format)
if err != nil {
return nil, xerrors.Errorf("SBOM decode error: %w", err)
}

// For Bitnami images
if strings.HasPrefix(input.FilePath, "opt/bitnami/") {
dir, file := path.Split(input.FilePath)
bin := strings.TrimPrefix(file, ".spdx-")
bin = strings.TrimSuffix(bin, ".spdx")
binPath := path.Join(input.FilePath, "../bin", bin)
for i, app := range bom.Applications {
// Replace the SBOM path with the binary path
bom.Applications[i].FilePath = binPath

for j, pkg := range app.Libraries {
if pkg.FilePath == "" {
continue
}
// Set the absolute path since SBOM in Bitnami images contain a relative path
// e.g. modules/apm/elastic-apm-agent-1.36.0.jar
// => opt/bitnami/elasticsearch/modules/apm/elastic-apm-agent-1.36.0.jar
bom.Applications[i].Libraries[j].FilePath = path.Join(dir, pkg.FilePath)
}
}
}

return &analyzer.AnalysisResult{
PackageInfos: bom.Packages,
Applications: bom.Applications,
}, nil
}

func (a sbomAnalyzer) Required(filePath string, _ os.FileInfo) bool {
for _, suffix := range requiredSuffixes {
if strings.HasSuffix(filePath, suffix) {
return true
}
}
return false
}

func (a sbomAnalyzer) Type() analyzer.Type {
return analyzer.TypeSBOM
}

func (a sbomAnalyzer) Version() int {
return version
}
Loading

0 comments on commit 9ef0113

Please sign in to comment.