Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(sbom): add checksum to files #3888

Merged
merged 13 commits into from
Mar 30, 2023
50 changes: 31 additions & 19 deletions integration/testdata/conda-spdx.json.golden
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"SPDXID": "SPDXRef-DOCUMENT",
"creationInfo": {
"created": "2023-01-08T23:58:16.700785648Z",
"created": "2023-03-29T19:07:18Z",
"creators": [
"Tool: trivy-dev",
"Organization: aquasecurity"
Expand All @@ -11,14 +11,26 @@
"documentDescribes": [
"SPDXRef-Filesystem-6e0ac6a0fab50ab4"
],
"documentNamespace": "http://aquasecurity.github.io/trivy/filesystem/testdata/fixtures/fs/conda-3be0d21e-5711-451e-8b1b-2ac8775a3abb",
"documentNamespace": "http://aquasecurity.github.io/trivy/filesystem/testdata/fixtures/fs/conda-d872c7e3-4c6c-4fa1-a9b6-3e69dc71ff3b",
"files": [
{
"SPDXID": "SPDXRef-File-600e5e0110a84891",
"checksums": [
{
"algorithm": "SHA1",
"checksumValue": "237db0da53131e4548cb1181337fa0f420299e1f"
}
],
"fileName": "miniconda3/envs/testenv/conda-meta/openssl-1.1.1q-h7f8727e_0.json"
},
{
"SPDXID": "SPDXRef-File-7eb62e2a3edddc0a",
"checksums": [
{
"algorithm": "SHA1",
"checksumValue": "a6a2db7668f1ad541d704369fc66c96a4415aa24"
}
],
"fileName": "miniconda3/envs/testenv/conda-meta/pip-22.2.2-py38h06a4308_0.json"
}
],
Expand All @@ -33,50 +45,50 @@
},
{
"SPDXID": "SPDXRef-Filesystem-6e0ac6a0fab50ab4",
"downloadLocation": "NONE",
"attributionTexts": [
"SchemaVersion: 2"
],
"downloadLocation": "NONE",
"filesAnalyzed": false,
"name": "testdata/fixtures/fs/conda"
},
{
"SPDXID": "SPDXRef-Package-2984084f02572600",
"SPDXID": "SPDXRef-Package-6b677e82217fb5bd",
"downloadLocation": "NONE",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:conda/[email protected]",
"referenceLocator": "pkg:conda/[email protected]",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"hasFiles": [
"SPDXRef-File-600e5e0110a84891"
"SPDXRef-File-7eb62e2a3edddc0a"
],
"licenseConcluded": "OpenSSL",
"licenseDeclared": "OpenSSL",
"name": "openssl",
"versionInfo": "1.1.1q"
"licenseConcluded": "MIT",
"licenseDeclared": "MIT",
"name": "pip",
"versionInfo": "22.2.2"
},
{
"SPDXID": "SPDXRef-Package-ac33eb699b3aa81d",
"SPDXID": "SPDXRef-Package-b1088cb4090e3a55",
"downloadLocation": "NONE",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:conda/[email protected]",
"referenceLocator": "pkg:conda/[email protected]",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"hasFiles": [
"SPDXRef-File-7eb62e2a3edddc0a"
"SPDXRef-File-600e5e0110a84891"
],
"licenseConcluded": "MIT",
"licenseDeclared": "MIT",
"name": "pip",
"versionInfo": "22.2.2"
"licenseConcluded": "OpenSSL",
"licenseDeclared": "OpenSSL",
"name": "openssl",
"versionInfo": "1.1.1q"
}
],
"relationships": [
Expand All @@ -91,12 +103,12 @@
"spdxElementId": "SPDXRef-Filesystem-6e0ac6a0fab50ab4"
},
{
"relatedSpdxElement": "SPDXRef-Package-2984084f02572600",
"relatedSpdxElement": "SPDXRef-Package-b1088cb4090e3a55",
"relationshipType": "CONTAINS",
"spdxElementId": "SPDXRef-Application-ee5ef1aa4ac89125"
},
{
"relatedSpdxElement": "SPDXRef-Package-ac33eb699b3aa81d",
"relatedSpdxElement": "SPDXRef-Package-6b677e82217fb5bd",
"relationshipType": "CONTAINS",
"spdxElementId": "SPDXRef-Application-ee5ef1aa4ac89125"
}
Expand Down
7 changes: 7 additions & 0 deletions pkg/commands/artifact/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,12 @@ func initScannerConfig(opts flag.Options, cacheClient cache.Cache) (ScannerConfi
}
}

// SPDX needs to calculate digests for package files
var fileChecksum bool
if opts.Format == report.FormatSPDXJSON || opts.Format == report.FormatSPDX {
fileChecksum = true
}

remoteOpts := opts.Remote()

return ScannerConfig{
Expand All @@ -635,6 +641,7 @@ func initScannerConfig(opts flag.Options, cacheClient cache.Cache) (ScannerConfi
Platform: opts.Platform,
Slow: opts.Slow,
AWSRegion: opts.Region,
FileChecksum: fileChecksum,

// For OCI registries
RemoteOptions: remoteOpts,
Expand Down
78 changes: 78 additions & 0 deletions pkg/digest/digest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package digest

import (
"crypto/sha1" // nolint
"crypto/sha256"
"fmt"
"hash"
"io"
"strings"

"golang.org/x/xerrors"
)

type Algorithm string

func (a Algorithm) String() string {
return string(a)
}

// supported digest types
const (
SHA1 Algorithm = "sha1" // sha1 with hex encoding (lower case only)
SHA256 Algorithm = "sha256" // sha256 with hex encoding (lower case only)
)

// Digest allows simple protection of hex formatted digest strings, prefixed by their algorithm.
//
// The following is an example of the contents of Digest types:
//
// sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc
type Digest string

// NewDigest returns a Digest from alg and a hash.Hash object.
func NewDigest(alg Algorithm, h hash.Hash) Digest {
return Digest(fmt.Sprintf("%s:%x", alg, h.Sum(nil)))
}

func (d Digest) Algorithm() Algorithm {
return Algorithm(d[:d.sepIndex()])
}

func (d Digest) Encoded() string {
return string(d[d.sepIndex()+1:])
}

func (d Digest) String() string {
return string(d)
}

func (d Digest) sepIndex() int {
i := strings.Index(string(d), ":")
if i < 0 {
i = 0
}
return i
}

func CalcSHA1(r io.ReadSeeker) (Digest, error) {
defer r.Seek(0, io.SeekStart)

h := sha1.New() // nolint
if _, err := io.Copy(h, r); err != nil {
return "", xerrors.Errorf("unable to calculate sha1 digest: %w", err)
}

return NewDigest(SHA1, h), nil
}

func CalcSHA256(r io.ReadSeeker) (Digest, error) {
defer r.Seek(0, io.SeekStart)

h := sha256.New()
if _, err := io.Copy(h, r); err != nil {
return "", xerrors.Errorf("unable to calculate sha256 digest: %w", err)
}

return NewDigest(SHA256, h), nil
}
3 changes: 2 additions & 1 deletion pkg/fanal/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ type PostAnalysisInput struct {
}

type AnalysisOptions struct {
Offline bool
Offline bool
FileChecksum bool
}

type AnalysisResult struct {
Expand Down
11 changes: 4 additions & 7 deletions pkg/fanal/analyzer/executable/executable.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ package executable

import (
"context"
"crypto/sha256"
"encoding/hex"
"io"
"os"

"golang.org/x/xerrors"

"github.com/aquasecurity/trivy/pkg/digest"
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/fanal/utils"
)
Expand All @@ -30,15 +28,14 @@ func (a executableAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisIn
return nil, nil
}

h := sha256.New()
if _, err = io.Copy(h, input.Content); err != nil {
dig, err := digest.CalcSHA256(input.Content)
if err != nil {
return nil, xerrors.Errorf("sha256 error: %w", err)
}
s := hex.EncodeToString(h.Sum(nil))

return &analyzer.AnalysisResult{
Digests: map[string]string{
input.FilePath: "sha256:" + s,
input.FilePath: dig.String(),
},
}, nil
}
Expand Down
73 changes: 66 additions & 7 deletions pkg/fanal/analyzer/language/analyze.go
Original file line number Diff line number Diff line change
@@ -1,33 +1,88 @@
package language

import (
"io"
"strings"

"golang.org/x/xerrors"

dio "github.com/aquasecurity/go-dep-parser/pkg/io"
godeptypes "github.com/aquasecurity/go-dep-parser/pkg/types"
"github.com/aquasecurity/trivy/pkg/digest"
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/licensing"
"github.com/aquasecurity/trivy/pkg/log"
)

// Analyze returns an analysis result of the lock file
func Analyze(fileType, filePath string, r dio.ReadSeekerAt, parser godeptypes.Parser) (*analyzer.AnalysisResult, error) {
app, err := Parse(fileType, filePath, r, parser)
if err != nil {
return nil, xerrors.Errorf("failed to parse %s: %w", filePath, err)
}

if app == nil {
return nil, nil
}

return &analyzer.AnalysisResult{Applications: []types.Application{*app}}, nil
}

// AnalyzePackage returns an analysis result of the package file other than lock files
func AnalyzePackage(fileType, filePath string, r dio.ReadSeekerAt, parser godeptypes.Parser, checksum bool) (*analyzer.AnalysisResult, error) {
app, err := ParsePackage(fileType, filePath, r, parser, checksum)
if err != nil {
return nil, xerrors.Errorf("failed to parse %s: %w", filePath, err)
}

if app == nil {
return nil, nil
}

return &analyzer.AnalysisResult{Applications: []types.Application{*app}}, nil
}

// Parse returns a parsed result of the lock file
func Parse(fileType, filePath string, r dio.ReadSeekerAt, parser godeptypes.Parser) (*types.Application, error) {
parsedLibs, parsedDependencies, err := parser.Parse(r)
if err != nil {
return nil, xerrors.Errorf("failed to parse %s: %w", filePath, err)
}

// The file path of each library should be empty in case of dependency list such as lock file
// since they all will be the same path.
return ToAnalysisResult(fileType, filePath, "", parsedLibs, parsedDependencies), nil
return toApplication(fileType, filePath, "", nil, parsedLibs, parsedDependencies), nil
}

func ToApplication(fileType, filePath, libFilePath string, libs []godeptypes.Library, depGraph []godeptypes.Dependency) *types.Application {
// ParsePackage returns a parsed result of the package file
func ParsePackage(fileType, filePath string, r dio.ReadSeekerAt, parser godeptypes.Parser, checksum bool) (*types.Application, error) {
parsedLibs, parsedDependencies, err := parser.Parse(r)
if err != nil {
return nil, xerrors.Errorf("failed to parse %s: %w", filePath, err)
}

// The reader is not passed if the checksum is not necessarily calculated.
if !checksum {
r = nil
}

// The file path of each library should be empty in case of dependency list such as lock file
// since they all will be the same path.
return toApplication(fileType, filePath, filePath, r, parsedLibs, parsedDependencies), nil
}

func toApplication(fileType, filePath, libFilePath string, r dio.ReadSeekerAt, libs []godeptypes.Library, depGraph []godeptypes.Dependency) *types.Application {
if len(libs) == 0 {
return nil
}

// Calculate the file digest when one of `spdx` formats is selected
d, err := calculateDigest(r)
if err != nil {
log.Logger.Warnf("Unable to get checksum for %s: %s", filePath, err)
}

deps := make(map[string][]string)
for _, dep := range depGraph {
deps[dep.ID] = dep.DependsOn
Expand Down Expand Up @@ -59,6 +114,7 @@ func ToApplication(fileType, filePath, libFilePath string, libs []godeptypes.Lib
Licenses: licenses,
DependsOn: deps[lib.ID],
Locations: locs,
Digest: d,
})
}

Expand All @@ -69,11 +125,14 @@ func ToApplication(fileType, filePath, libFilePath string, libs []godeptypes.Lib
}
}

func ToAnalysisResult(fileType, filePath, libFilePath string, libs []godeptypes.Library, depGraph []godeptypes.Dependency) *analyzer.AnalysisResult {
app := ToApplication(fileType, filePath, libFilePath, libs, depGraph)
if app == nil {
return nil
func calculateDigest(r dio.ReadSeekerAt) (digest.Digest, error) {
if r == nil {
return "", nil
}
// return reader to start after it has been read in analyzer
if _, err := r.Seek(0, io.SeekStart); err != nil {
return "", xerrors.Errorf("unable to seek: %w", err)
}

return &analyzer.AnalysisResult{Applications: []types.Application{*app}}
return digest.CalcSHA1(r)
}
Loading