mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 15:50:15 -08:00
feat(cyclonedx): preserve SBOM structure when scanning SBOM files with vulnerability updates (#9439)
Signed-off-by: knqyf263 <knqyf263@gmail.com> Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io>
This commit is contained in:
22
integration/testdata/conda-spdx.json.golden
vendored
22
integration/testdata/conda-spdx.json.golden
vendored
@@ -3,7 +3,7 @@
|
|||||||
"dataLicense": "CC0-1.0",
|
"dataLicense": "CC0-1.0",
|
||||||
"SPDXID": "SPDXRef-DOCUMENT",
|
"SPDXID": "SPDXRef-DOCUMENT",
|
||||||
"name": "testdata/fixtures/repo/conda",
|
"name": "testdata/fixtures/repo/conda",
|
||||||
"documentNamespace": "http://trivy.dev/filesystem/testdata/fixtures/repo/conda-3ff14136-e09f-4df9-80ea-000000000004",
|
"documentNamespace": "http://trivy.dev/filesystem/testdata/fixtures/repo/conda-3ff14136-e09f-4df9-80ea-000000000005",
|
||||||
"creationInfo": {
|
"creationInfo": {
|
||||||
"creators": [
|
"creators": [
|
||||||
"Organization: aquasecurity",
|
"Organization: aquasecurity",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "openssl",
|
"name": "openssl",
|
||||||
"SPDXID": "SPDXRef-Package-22a178da112ac20a",
|
"SPDXID": "SPDXRef-Package-cb268df467bc826c",
|
||||||
"versionInfo": "1.1.1q",
|
"versionInfo": "1.1.1q",
|
||||||
"supplier": "NOASSERTION",
|
"supplier": "NOASSERTION",
|
||||||
"downloadLocation": "NONE",
|
"downloadLocation": "NONE",
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "pip",
|
"name": "pip",
|
||||||
"SPDXID": "SPDXRef-Package-c22b9ee9a601ba6",
|
"SPDXID": "SPDXRef-Package-1378bb10fcebba63",
|
||||||
"versionInfo": "22.2.2",
|
"versionInfo": "22.2.2",
|
||||||
"supplier": "NOASSERTION",
|
"supplier": "NOASSERTION",
|
||||||
"downloadLocation": "NONE",
|
"downloadLocation": "NONE",
|
||||||
@@ -118,23 +118,23 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spdxElementId": "SPDXRef-Filesystem-2e2426fd0f2580ef",
|
"spdxElementId": "SPDXRef-Filesystem-2e2426fd0f2580ef",
|
||||||
"relatedSpdxElement": "SPDXRef-Package-22a178da112ac20a",
|
"relatedSpdxElement": "SPDXRef-Package-1378bb10fcebba63",
|
||||||
"relationshipType": "CONTAINS"
|
"relationshipType": "CONTAINS"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spdxElementId": "SPDXRef-Filesystem-2e2426fd0f2580ef",
|
"spdxElementId": "SPDXRef-Filesystem-2e2426fd0f2580ef",
|
||||||
"relatedSpdxElement": "SPDXRef-Package-c22b9ee9a601ba6",
|
"relatedSpdxElement": "SPDXRef-Package-cb268df467bc826c",
|
||||||
"relationshipType": "CONTAINS"
|
"relationshipType": "CONTAINS"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spdxElementId": "SPDXRef-Package-22a178da112ac20a",
|
"spdxElementId": "SPDXRef-Package-1378bb10fcebba63",
|
||||||
"relatedSpdxElement": "SPDXRef-File-600e5e0110a84891",
|
|
||||||
"relationshipType": "CONTAINS"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"spdxElementId": "SPDXRef-Package-c22b9ee9a601ba6",
|
|
||||||
"relatedSpdxElement": "SPDXRef-File-7eb62e2a3edddc0a",
|
"relatedSpdxElement": "SPDXRef-File-7eb62e2a3edddc0a",
|
||||||
"relationshipType": "CONTAINS"
|
"relationshipType": "CONTAINS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"spdxElementId": "SPDXRef-Package-cb268df467bc826c",
|
||||||
|
"relatedSpdxElement": "SPDXRef-File-600e5e0110a84891",
|
||||||
|
"relationshipType": "CONTAINS"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
|
"$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
|
||||||
"bomFormat": "CycloneDX",
|
"bomFormat": "CycloneDX",
|
||||||
"specVersion": "1.6",
|
"specVersion": "1.6",
|
||||||
"serialNumber": "urn:uuid:3ff14136-e09f-4df9-80ea-000000000010",
|
"serialNumber": "urn:uuid:3ff14136-e09f-4df9-80ea-000000000006",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"timestamp": "2021-08-25T12:20:30+00:00",
|
"timestamp": "2021-08-25T12:20:30+00:00",
|
||||||
@@ -91,14 +91,6 @@
|
|||||||
"name": "aquasecurity:trivy:LayerDigest",
|
"name": "aquasecurity:trivy:LayerDigest",
|
||||||
"value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c"
|
"value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "aquasecurity:trivy:PkgID",
|
|
||||||
"value": "bash@5.0-4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "aquasecurity:trivy:PkgType",
|
|
||||||
"value": "debian"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "aquasecurity:trivy:SrcName",
|
"name": "aquasecurity:trivy:SrcName",
|
||||||
"value": "bash"
|
"value": "bash"
|
||||||
@@ -124,14 +116,6 @@
|
|||||||
"name": "aquasecurity:trivy:LayerDigest",
|
"name": "aquasecurity:trivy:LayerDigest",
|
||||||
"value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c"
|
"value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "aquasecurity:trivy:PkgID",
|
|
||||||
"value": "libidn2-0@2.0.5-1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "aquasecurity:trivy:PkgType",
|
|
||||||
"value": "debian"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "aquasecurity:trivy:SrcName",
|
"name": "aquasecurity:trivy:SrcName",
|
||||||
"value": "libidn2"
|
"value": "libidn2"
|
||||||
@@ -169,11 +153,7 @@
|
|||||||
"value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602"
|
"value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "aquasecurity:trivy:PkgID",
|
"name": "aquasecurity:trivy:Type",
|
||||||
"value": "activesupport@6.0.2.1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "aquasecurity:trivy:PkgType",
|
|
||||||
"value": "gemspec"
|
"value": "gemspec"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -193,18 +173,6 @@
|
|||||||
"353f2470-9c8b-4647-9d0d-96d893838dc8",
|
"353f2470-9c8b-4647-9d0d-96d893838dc8",
|
||||||
"pkg:gem/activesupport@6.0.2.1?file_path=var%2Flib%2Fgems%2F2.5.0%2Fspecifications%2Factivesupport-6.0.2.1.gemspec"
|
"pkg:gem/activesupport@6.0.2.1?file_path=var%2Flib%2Fgems%2F2.5.0%2Fspecifications%2Factivesupport-6.0.2.1.gemspec"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"ref": "pkg:deb/debian/bash@5.0-4?distro=debian-10.2",
|
|
||||||
"dependsOn": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ref": "pkg:deb/debian/libidn2-0@2.0.5-1?distro=debian-10.2",
|
|
||||||
"dependsOn": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ref": "pkg:gem/activesupport@6.0.2.1?file_path=var%2Flib%2Fgems%2F2.5.0%2Fspecifications%2Factivesupport-6.0.2.1.gemspec",
|
|
||||||
"dependsOn": []
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"vulnerabilities": [
|
"vulnerabilities": [
|
||||||
|
|||||||
22
integration/testdata/julia-spdx.json.golden
vendored
22
integration/testdata/julia-spdx.json.golden
vendored
@@ -14,7 +14,7 @@
|
|||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "Manifest.toml",
|
"name": "Manifest.toml",
|
||||||
"SPDXID": "SPDXRef-Application-18fc3597717a3e56",
|
"SPDXID": "SPDXRef-Application-c39d15beb6bdf085",
|
||||||
"downloadLocation": "NONE",
|
"downloadLocation": "NONE",
|
||||||
"filesAnalyzed": false,
|
"filesAnalyzed": false,
|
||||||
"primaryPackagePurpose": "APPLICATION",
|
"primaryPackagePurpose": "APPLICATION",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "A",
|
"name": "A",
|
||||||
"SPDXID": "SPDXRef-Package-761ce79b41d8f121",
|
"SPDXID": "SPDXRef-Package-3aea0b160c3af98d",
|
||||||
"versionInfo": "1.9.0",
|
"versionInfo": "1.9.0",
|
||||||
"supplier": "NOASSERTION",
|
"supplier": "NOASSERTION",
|
||||||
"downloadLocation": "NONE",
|
"downloadLocation": "NONE",
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "B",
|
"name": "B",
|
||||||
"SPDXID": "SPDXRef-Package-28f04edc422602a",
|
"SPDXID": "SPDXRef-Package-2264d5c424c073e7",
|
||||||
"versionInfo": "1.9.0",
|
"versionInfo": "1.9.0",
|
||||||
"supplier": "NOASSERTION",
|
"supplier": "NOASSERTION",
|
||||||
"downloadLocation": "NONE",
|
"downloadLocation": "NONE",
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "B",
|
"name": "B",
|
||||||
"SPDXID": "SPDXRef-Package-6e0b0d1825d8c02c",
|
"SPDXID": "SPDXRef-Package-e29bcba688483642",
|
||||||
"versionInfo": "1.9.0",
|
"versionInfo": "1.9.0",
|
||||||
"supplier": "NOASSERTION",
|
"supplier": "NOASSERTION",
|
||||||
"downloadLocation": "NONE",
|
"downloadLocation": "NONE",
|
||||||
@@ -150,13 +150,13 @@
|
|||||||
],
|
],
|
||||||
"relationships": [
|
"relationships": [
|
||||||
{
|
{
|
||||||
"spdxElementId": "SPDXRef-Application-18fc3597717a3e56",
|
"spdxElementId": "SPDXRef-Application-c39d15beb6bdf085",
|
||||||
"relatedSpdxElement": "SPDXRef-Package-6e0b0d1825d8c02c",
|
"relatedSpdxElement": "SPDXRef-Package-3aea0b160c3af98d",
|
||||||
"relationshipType": "CONTAINS"
|
"relationshipType": "CONTAINS"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spdxElementId": "SPDXRef-Application-18fc3597717a3e56",
|
"spdxElementId": "SPDXRef-Application-c39d15beb6bdf085",
|
||||||
"relatedSpdxElement": "SPDXRef-Package-761ce79b41d8f121",
|
"relatedSpdxElement": "SPDXRef-Package-e29bcba688483642",
|
||||||
"relationshipType": "CONTAINS"
|
"relationshipType": "CONTAINS"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -166,12 +166,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spdxElementId": "SPDXRef-Filesystem-1be792dd0077c431",
|
"spdxElementId": "SPDXRef-Filesystem-1be792dd0077c431",
|
||||||
"relatedSpdxElement": "SPDXRef-Application-18fc3597717a3e56",
|
"relatedSpdxElement": "SPDXRef-Application-c39d15beb6bdf085",
|
||||||
"relationshipType": "CONTAINS"
|
"relationshipType": "CONTAINS"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"spdxElementId": "SPDXRef-Package-761ce79b41d8f121",
|
"spdxElementId": "SPDXRef-Package-3aea0b160c3af98d",
|
||||||
"relatedSpdxElement": "SPDXRef-Package-28f04edc422602a",
|
"relatedSpdxElement": "SPDXRef-Package-2264d5c424c073e7",
|
||||||
"relationshipType": "DEPENDS_ON"
|
"relationshipType": "DEPENDS_ON"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -374,15 +374,6 @@ func Run(ctx context.Context, opts flag.Options, targetKind TargetKind) (err err
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if opts.ServerAddr != "" && opts.Scanners.AnyEnabled(types.MisconfigScanner, types.SecretScanner) {
|
|
||||||
log.WarnContext(ctx,
|
|
||||||
fmt.Sprintf(
|
|
||||||
"Trivy runs in client/server mode, but misconfiguration and license scanning will be done on the client side, see %s",
|
|
||||||
doc.URL("/docs/references/modes/client-server", ""),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.GenerateDefaultConfig {
|
if opts.GenerateDefaultConfig {
|
||||||
log.Info("Writing the default config to trivy-default.yaml...")
|
log.Info("Writing the default config to trivy-default.yaml...")
|
||||||
|
|
||||||
@@ -423,6 +414,9 @@ func Run(ctx context.Context, opts flag.Options, targetKind TargetKind) (err err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func run(ctx context.Context, opts flag.Options, targetKind TargetKind) (types.Report, error) {
|
func run(ctx context.Context, opts flag.Options, targetKind TargetKind) (types.Report, error) {
|
||||||
|
// Perform validation checks
|
||||||
|
checkOptions(ctx, opts, targetKind)
|
||||||
|
|
||||||
r, err := NewRunner(ctx, opts, targetKind)
|
r, err := NewRunner(ctx, opts, targetKind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, SkipScan) {
|
if errors.Is(err, SkipScan) {
|
||||||
@@ -466,6 +460,27 @@ func run(ctx context.Context, opts flag.Options, targetKind TargetKind) (types.R
|
|||||||
return report, nil
|
return report, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkOptions performs various checks on scan options and shows warnings
|
||||||
|
func checkOptions(ctx context.Context, opts flag.Options, targetKind TargetKind) {
|
||||||
|
// Check client/server mode with misconfiguration and secret scanning
|
||||||
|
if opts.ServerAddr != "" && opts.Scanners.AnyEnabled(types.MisconfigScanner, types.SecretScanner) {
|
||||||
|
log.WarnContext(ctx,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Trivy runs in client/server mode, but misconfiguration and license scanning will be done on the client side, see %s",
|
||||||
|
doc.URL("/docs/references/modes/client-server", ""),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check SBOM to SBOM scanning with package filtering flags
|
||||||
|
// For SBOM-to-SBOM scanning (for example, to add vulnerabilities to the SBOM file), we should not modify the scanned file.
|
||||||
|
// cf. https://github.com/aquasecurity/trivy/pull/9439#issuecomment-3295533665
|
||||||
|
if targetKind == TargetSBOM && slices.Contains(types.SupportedSBOMFormats, opts.Format) &&
|
||||||
|
(!slices.Equal(opts.PkgTypes, types.PkgTypes) || !slices.Equal(opts.PkgRelationships, ftypes.Relationships)) {
|
||||||
|
log.Warn("'--pkg-types' and '--pkg-relationships' options will be ignored when scanning SBOM and outputting SBOM format.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func disabledAnalyzers(opts flag.Options) []analyzer.Type {
|
func disabledAnalyzers(opts flag.Options) []analyzer.Type {
|
||||||
// Specified analyzers to be disabled depending on scanning modes
|
// Specified analyzers to be disabled depending on scanning modes
|
||||||
// e.g. The 'image' subcommand should disable the lock file scanning.
|
// e.g. The 'image' subcommand should disable the lock file scanning.
|
||||||
|
|||||||
@@ -367,6 +367,89 @@ func TestArtifact_Inspect(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "components with missing BOM-REF",
|
||||||
|
filePath: filepath.Join("testdata", "bom-missing-refs.json"),
|
||||||
|
wantBlobs: []cachetest.WantBlob{
|
||||||
|
{
|
||||||
|
ID: "sha256:512b9e999c9d7b4880c63ce55c2c74ea5c22b05cdbcb486097a16ec692c746a0",
|
||||||
|
BlobInfo: types.BlobInfo{
|
||||||
|
SchemaVersion: types.BlobJSONSchemaVersion,
|
||||||
|
OS: types.OS{
|
||||||
|
Family: "alpine",
|
||||||
|
Name: "3.16.0",
|
||||||
|
},
|
||||||
|
PackageInfos: []types.PackageInfo{
|
||||||
|
{
|
||||||
|
Packages: types.Packages{
|
||||||
|
{
|
||||||
|
ID: "musl@1.2.3-r0",
|
||||||
|
Name: "musl",
|
||||||
|
Version: "1.2.3-r0",
|
||||||
|
SrcName: "musl",
|
||||||
|
SrcVersion: "1.2.3-r0",
|
||||||
|
Licenses: []string{"MIT"},
|
||||||
|
Layer: types.Layer{
|
||||||
|
DiffID: "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3",
|
||||||
|
},
|
||||||
|
Identifier: types.PkgIdentifier{
|
||||||
|
PURL: &packageurl.PackageURL{
|
||||||
|
Type: packageurl.TypeApk,
|
||||||
|
Namespace: "alpine",
|
||||||
|
Name: "musl",
|
||||||
|
Version: "1.2.3-r0",
|
||||||
|
Qualifiers: packageurl.Qualifiers{
|
||||||
|
{
|
||||||
|
Key: "distro",
|
||||||
|
Value: "3.16.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// BOM-Ref should be auto-generated from PURL
|
||||||
|
BOMRef: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Applications: []types.Application{
|
||||||
|
{
|
||||||
|
Type: "composer",
|
||||||
|
FilePath: "",
|
||||||
|
Packages: types.Packages{
|
||||||
|
{
|
||||||
|
ID: "pear/log@1.13.1",
|
||||||
|
Name: "pear/log",
|
||||||
|
Version: "1.13.1",
|
||||||
|
Layer: types.Layer{
|
||||||
|
DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
|
||||||
|
},
|
||||||
|
Identifier: types.PkgIdentifier{
|
||||||
|
PURL: &packageurl.PackageURL{
|
||||||
|
Type: packageurl.TypeComposer,
|
||||||
|
Namespace: "pear",
|
||||||
|
Name: "log",
|
||||||
|
Version: "1.13.1",
|
||||||
|
},
|
||||||
|
// BOM-Ref should be auto-generated from PURL
|
||||||
|
BOMRef: "pkg:composer/pear/log@1.13.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: artifact.Reference{
|
||||||
|
Name: filepath.Join("testdata", "bom-missing-refs.json"),
|
||||||
|
Type: types.TypeCycloneDX,
|
||||||
|
ID: "sha256:512b9e999c9d7b4880c63ce55c2c74ea5c22b05cdbcb486097a16ec692c746a0",
|
||||||
|
BlobIDs: []string{
|
||||||
|
"sha256:512b9e999c9d7b4880c63ce55c2c74ea5c22b05cdbcb486097a16ec692c746a0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "sad path with no such directory",
|
name: "sad path with no such directory",
|
||||||
filePath: filepath.Join("testdata", "unknown.json"),
|
filePath: filepath.Join("testdata", "unknown.json"),
|
||||||
|
|||||||
89
pkg/fanal/artifact/sbom/testdata/bom-missing-refs.json
vendored
Normal file
89
pkg/fanal/artifact/sbom/testdata/bom-missing-refs.json
vendored
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
{
|
||||||
|
"bomFormat": "CycloneDX",
|
||||||
|
"specVersion": "1.5",
|
||||||
|
"serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b",
|
||||||
|
"version": 1,
|
||||||
|
"metadata": {
|
||||||
|
"timestamp": "2022-05-28T10:20:03.79527Z",
|
||||||
|
"tools": {
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "application",
|
||||||
|
"group": "aquasecurity",
|
||||||
|
"name": "trivy",
|
||||||
|
"version": "dev"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"component": {
|
||||||
|
"bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a",
|
||||||
|
"type": "container",
|
||||||
|
"name": "test-project",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:SchemaVersion",
|
||||||
|
"value": "2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "library",
|
||||||
|
"name": "musl",
|
||||||
|
"version": "1.2.3-r0",
|
||||||
|
"licenses": [
|
||||||
|
{
|
||||||
|
"expression": "MIT"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"purl": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:LayerDiffID",
|
||||||
|
"value": "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bom-ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182",
|
||||||
|
"type": "operating-system",
|
||||||
|
"name": "alpine",
|
||||||
|
"version": "3.16.0",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:Type",
|
||||||
|
"value": "alpine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:Class",
|
||||||
|
"value": "os-pkgs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "library",
|
||||||
|
"name": "pear/log",
|
||||||
|
"version": "1.13.1",
|
||||||
|
"purl": "pkg:composer/pear/log@1.13.1",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "aquasecurity:trivy:LayerDiffID",
|
||||||
|
"value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": [
|
||||||
|
{
|
||||||
|
"ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ref": "0f585d64-4815-4b72-92c5-97dae191fa4a",
|
||||||
|
"dependsOn": [
|
||||||
|
"60e9f57b-d4a6-4f71-ad14-0893ac609182"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vulnerabilities": []
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
|
|
||||||
dtypes "github.com/aquasecurity/trivy-db/pkg/types"
|
dtypes "github.com/aquasecurity/trivy-db/pkg/types"
|
||||||
"github.com/aquasecurity/trivy/pkg/digest"
|
"github.com/aquasecurity/trivy/pkg/digest"
|
||||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||||
@@ -164,6 +167,18 @@ func (c *Component) ID() uuid.UUID {
|
|||||||
return c.id
|
return c.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clone creates a deep copy of the Component
|
||||||
|
func (c *Component) Clone() *Component {
|
||||||
|
clone := *c // Shallow copy
|
||||||
|
|
||||||
|
// Deep copy slices using slices.Clone
|
||||||
|
clone.Licenses = slices.Clone(c.Licenses)
|
||||||
|
clone.Files = slices.Clone(c.Files)
|
||||||
|
clone.Properties = slices.Clone(c.Properties)
|
||||||
|
|
||||||
|
return &clone
|
||||||
|
}
|
||||||
|
|
||||||
type File struct {
|
type File struct {
|
||||||
// Path is a path of the file.
|
// Path is a path of the file.
|
||||||
// CycloneDX: N/A
|
// CycloneDX: N/A
|
||||||
@@ -181,6 +196,9 @@ type Property struct {
|
|||||||
Name string
|
Name string
|
||||||
Value string
|
Value string
|
||||||
Namespace string
|
Namespace string
|
||||||
|
// External indicates if this property came from external source (not generated by Trivy)
|
||||||
|
// When false (default), Trivy namespace prefix will be applied during marshaling
|
||||||
|
External bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Properties []Property
|
type Properties []Property
|
||||||
@@ -352,3 +370,43 @@ func (b *BOM) bomRef(c *Component) string {
|
|||||||
}
|
}
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clone creates a deep copy of the BOM, including all components, relationships, and vulnerabilities.
|
||||||
|
// This ensures that modifications to the cloned BOM do not affect the original BOM.
|
||||||
|
func (b *BOM) Clone() *BOM {
|
||||||
|
return &BOM{
|
||||||
|
SerialNumber: b.SerialNumber,
|
||||||
|
Version: b.Version,
|
||||||
|
rootID: b.rootID,
|
||||||
|
|
||||||
|
// Deep copy components
|
||||||
|
components: lo.MapValues(b.components, func(c *Component, _ uuid.UUID) *Component {
|
||||||
|
return c.Clone()
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Deep copy relationships
|
||||||
|
relationships: lo.MapValues(b.relationships, func(rels []Relationship, _ uuid.UUID) []Relationship {
|
||||||
|
return slices.Clone(rels)
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Deep copy vulnerabilities
|
||||||
|
vulnerabilities: lo.MapValues(b.vulnerabilities, func(vulns []Vulnerability, _ uuid.UUID) []Vulnerability {
|
||||||
|
return slices.Clone(vulns)
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Deep copy external references
|
||||||
|
externalReferences: slices.Clone(b.externalReferences),
|
||||||
|
|
||||||
|
// Deep copy purls
|
||||||
|
purls: lo.MapValues(b.purls, func(ids []uuid.UUID, _ string) []uuid.UUID {
|
||||||
|
return slices.Clone(ids)
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Deep copy parents
|
||||||
|
parents: lo.MapValues(b.parents, func(parentIds []uuid.UUID, _ uuid.UUID) []uuid.UUID {
|
||||||
|
return slices.Clone(parentIds)
|
||||||
|
}),
|
||||||
|
|
||||||
|
opts: b.opts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package cyclonedx
|
package cyclonedx
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -358,13 +359,13 @@ func (m *Marshaler) normalizeLicense(license string) expression.Expression {
|
|||||||
func (*Marshaler) Properties(properties []core.Property) *[]cdx.Property {
|
func (*Marshaler) Properties(properties []core.Property) *[]cdx.Property {
|
||||||
cdxProps := make([]cdx.Property, 0, len(properties))
|
cdxProps := make([]cdx.Property, 0, len(properties))
|
||||||
for _, property := range properties {
|
for _, property := range properties {
|
||||||
namespace := Namespace
|
namespace := cmp.Or(property.Namespace, Namespace)
|
||||||
if property.Namespace != "" {
|
|
||||||
namespace = property.Namespace
|
// External property preserves original name, Trivy property gets namespace prefix
|
||||||
}
|
name := lo.Ternary(property.External, property.Name, namespace+property.Name)
|
||||||
|
|
||||||
cdxProps = append(cdxProps, cdx.Property{
|
cdxProps = append(cdxProps, cdx.Property{
|
||||||
Name: namespace + property.Name,
|
Name: name,
|
||||||
Value: property.Value,
|
Value: property.Value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,9 @@ var (
|
|||||||
|
|
||||||
func TestMarshaler_MarshalReport(t *testing.T) {
|
func TestMarshaler_MarshalReport(t *testing.T) {
|
||||||
testSBOM := core.NewBOM(core.Options{GenerateBOMRef: true})
|
testSBOM := core.NewBOM(core.Options{GenerateBOMRef: true})
|
||||||
testSBOM.AddComponent(&core.Component{
|
|
||||||
|
// Add root component
|
||||||
|
rootComponent := &core.Component{
|
||||||
Root: true,
|
Root: true,
|
||||||
Type: core.TypeApplication,
|
Type: core.TypeApplication,
|
||||||
Name: "jackson-databind-2.13.4.1.jar",
|
Name: "jackson-databind-2.13.4.1.jar",
|
||||||
@@ -77,7 +79,40 @@ func TestMarshaler_MarshalReport(t *testing.T) {
|
|||||||
Value: "2",
|
Value: "2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
testSBOM.AddComponent(rootComponent)
|
||||||
|
|
||||||
|
// Add the jackson-databind component that matches scan results
|
||||||
|
jacksonComponent := &core.Component{
|
||||||
|
Type: core.TypeLibrary,
|
||||||
|
Name: "jackson-databind",
|
||||||
|
Group: "com.fasterxml.jackson.core",
|
||||||
|
Version: "2.13.4.1",
|
||||||
|
PkgIdentifier: ftypes.PkgIdentifier{
|
||||||
|
BOMRef: "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.4.1",
|
||||||
|
PURL: &packageurl.PackageURL{
|
||||||
|
Type: packageurl.TypeMaven,
|
||||||
|
Namespace: "com.fasterxml.jackson.core",
|
||||||
|
Name: "jackson-databind",
|
||||||
|
Version: "2.13.4.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: []core.Property{
|
||||||
|
{
|
||||||
|
Name: core.PropertyPkgType,
|
||||||
|
Value: "jar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: core.PropertyFilePath,
|
||||||
|
Value: "jackson-databind-2.13.4.1.jar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
testSBOM.AddComponent(jacksonComponent)
|
||||||
|
|
||||||
|
// Establish relationships
|
||||||
|
testSBOM.AddRelationship(rootComponent, jacksonComponent, core.RelationshipContains)
|
||||||
|
testSBOM.AddRelationship(jacksonComponent, nil, core.RelationshipDependsOn)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -1533,7 +1568,7 @@ func TestMarshaler_MarshalReport(t *testing.T) {
|
|||||||
BOMFormat: "CycloneDX",
|
BOMFormat: "CycloneDX",
|
||||||
SpecVersion: cdx.SpecVersion1_6,
|
SpecVersion: cdx.SpecVersion1_6,
|
||||||
JSONSchema: "http://cyclonedx.org/schema/bom-1.6.schema.json",
|
JSONSchema: "http://cyclonedx.org/schema/bom-1.6.schema.json",
|
||||||
SerialNumber: "urn:uuid:3ff14136-e09f-4df9-80ea-000000000002",
|
SerialNumber: "urn:uuid:3ff14136-e09f-4df9-80ea-000000000001",
|
||||||
Version: 1,
|
Version: 1,
|
||||||
Metadata: &cdx.Metadata{
|
Metadata: &cdx.Metadata{
|
||||||
Timestamp: "2021-08-25T12:20:30+00:00",
|
Timestamp: "2021-08-25T12:20:30+00:00",
|
||||||
|
|||||||
@@ -80,12 +80,20 @@ func (b *BOM) parseBOM(bom *cdx.BOM) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, depRef := range lo.FromPtr(dep.Dependencies) {
|
|
||||||
dependency, ok := components[depRef]
|
dependencies := lo.FromPtr(dep.Dependencies)
|
||||||
if !ok {
|
if len(dependencies) == 0 {
|
||||||
continue
|
// Empty dependsOn array - create empty relationship to preserve this information
|
||||||
|
b.BOM.AddRelationship(ref, nil, core.RelationshipDependsOn)
|
||||||
|
} else {
|
||||||
|
// Process actual dependencies
|
||||||
|
for _, depRef := range dependencies {
|
||||||
|
dependency, ok := components[depRef]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.BOM.AddRelationship(ref, dependency, core.RelationshipDependsOn)
|
||||||
}
|
}
|
||||||
b.BOM.AddRelationship(ref, dependency, core.RelationshipDependsOn)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,10 +289,21 @@ func (b *BOM) unmarshalSupplier(supplier *cdx.OrganizationalEntity) string {
|
|||||||
func (b *BOM) unmarshalProperties(properties *[]cdx.Property) []core.Property {
|
func (b *BOM) unmarshalProperties(properties *[]cdx.Property) []core.Property {
|
||||||
var props []core.Property
|
var props []core.Property
|
||||||
for _, p := range lo.FromPtr(properties) {
|
for _, p := range lo.FromPtr(properties) {
|
||||||
props = append(props, core.Property{
|
prop := core.Property{
|
||||||
Name: strings.TrimPrefix(p.Name, Namespace),
|
|
||||||
Value: p.Value,
|
Value: p.Value,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// If the property has the Trivy namespace prefix, it's a Trivy property
|
||||||
|
if name, found := strings.CutPrefix(p.Name, Namespace); found {
|
||||||
|
prop.Name = name
|
||||||
|
prop.External = false // Trivy property (default)
|
||||||
|
} else {
|
||||||
|
// External property - preserve the original name and mark as external
|
||||||
|
prop.Name = p.Name
|
||||||
|
prop.External = true
|
||||||
|
}
|
||||||
|
|
||||||
|
props = append(props, prop)
|
||||||
}
|
}
|
||||||
return props
|
return props
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/aquasecurity/trivy/pkg/digest"
|
"github.com/aquasecurity/trivy/pkg/digest"
|
||||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/log"
|
||||||
"github.com/aquasecurity/trivy/pkg/purl"
|
"github.com/aquasecurity/trivy/pkg/purl"
|
||||||
"github.com/aquasecurity/trivy/pkg/sbom/core"
|
"github.com/aquasecurity/trivy/pkg/sbom/core"
|
||||||
"github.com/aquasecurity/trivy/pkg/scan/utils"
|
"github.com/aquasecurity/trivy/pkg/scan/utils"
|
||||||
@@ -19,9 +20,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Encoder struct {
|
type Encoder struct {
|
||||||
bom *core.BOM
|
bom *core.BOM
|
||||||
opts core.Options
|
opts core.Options
|
||||||
components map[uuid.UUID]*core.Component
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEncoder(opts core.Options) *Encoder {
|
func NewEncoder(opts core.Options) *Encoder {
|
||||||
@@ -29,8 +29,12 @@ func NewEncoder(opts core.Options) *Encoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Encoder) Encode(report types.Report) (*core.BOM, error) {
|
func (e *Encoder) Encode(report types.Report) (*core.BOM, error) {
|
||||||
|
// When report.BOM is not nil, reuse the existing BOM structure.
|
||||||
|
// This happens in two scenarios:
|
||||||
|
// 1. SBOM scanning: When scanning an existing SBOM file to refresh vulnerabilities
|
||||||
|
// 2. Library usage: When using Trivy as a library with a custom BOM in the report
|
||||||
if report.BOM != nil {
|
if report.BOM != nil {
|
||||||
e.components = report.BOM.Components()
|
return e.reuseExistingBOM(report)
|
||||||
}
|
}
|
||||||
// Metadata component
|
// Metadata component
|
||||||
root, err := e.rootComponent(report)
|
root, err := e.rootComponent(report)
|
||||||
@@ -101,11 +105,6 @@ func (e *Encoder) rootComponent(r types.Report) (*core.Component, error) {
|
|||||||
case ftypes.TypeRepository:
|
case ftypes.TypeRepository:
|
||||||
root.Type = core.TypeRepository
|
root.Type = core.TypeRepository
|
||||||
case ftypes.TypeCycloneDX, ftypes.TypeSPDX:
|
case ftypes.TypeCycloneDX, ftypes.TypeSPDX:
|
||||||
// When we scan SBOM file
|
|
||||||
// If SBOM file doesn't contain root component - use filesystem
|
|
||||||
if r.BOM != nil && r.BOM.Root() != nil {
|
|
||||||
return r.BOM.Root(), nil
|
|
||||||
}
|
|
||||||
// When we scan a `json` file (meaning a file in `json` format) which was created from the SBOM file.
|
// When we scan a `json` file (meaning a file in `json` format) which was created from the SBOM file.
|
||||||
// e.g. for use in `convert` mode.
|
// e.g. for use in `convert` mode.
|
||||||
// See https://github.com/aquasecurity/trivy/issues/6780
|
// See https://github.com/aquasecurity/trivy/issues/6780
|
||||||
@@ -253,14 +252,50 @@ func (e *Encoder) encodePackages(parent *core.Component, result types.Result) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// existedPkgIdentifier tries to look for package identifier (BOM-ref, PURL) by component name and component type
|
// reuseExistingBOM preserves the original SBOM structure and only updates the vulnerabilities section
|
||||||
func (e *Encoder) existedPkgIdentifier(name string, componentType core.ComponentType) ftypes.PkgIdentifier {
|
// with newly detected vulnerabilities. This method handles two use cases:
|
||||||
for _, c := range e.components {
|
// 1. SBOM scanning (CycloneDX): When scanning an existing SBOM file to refresh vulnerability data while
|
||||||
if c.Name == name && c.Type == componentType {
|
// preserving the original structure, components, and relationships
|
||||||
return c.PkgIdentifier
|
// e.g. $ trivy sbom sbom.cdx.json --scanners vuln --format cyclonedx
|
||||||
|
// 2. Library usage: When using Trivy as a library with a pre-existing custom BOM that needs
|
||||||
|
// to be enriched with vulnerability information
|
||||||
|
//
|
||||||
|
// For SBOM scanning (case 1), this approach is CycloneDX-specific
|
||||||
|
// because: SPDX 2.3 does not include vulnerabilities in the SBOM specification.
|
||||||
|
// Therefore, the method uses BOM-Ref for component-vulnerability lookup rather than SPDX-ID.
|
||||||
|
func (e *Encoder) reuseExistingBOM(report types.Report) (*core.BOM, error) {
|
||||||
|
bom := report.BOM.Clone()
|
||||||
|
|
||||||
|
// Create a lookup map from BOM-Ref to component for efficient vulnerability assignment
|
||||||
|
// BOM-Ref is used as the key because it's the standard identifier in CycloneDX format
|
||||||
|
// and is guaranteed to be present in components from CycloneDX SBOMs
|
||||||
|
components := lo.MapKeys(report.BOM.Components(), func(v *core.Component, _ uuid.UUID) string {
|
||||||
|
return v.PkgIdentifier.BOMRef
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, result := range report.Results {
|
||||||
|
// Group newly detected vulnerabilities by their component's BOM-Ref
|
||||||
|
vulns := make(map[string][]core.Vulnerability)
|
||||||
|
for _, vuln := range result.Vulnerabilities {
|
||||||
|
vulns[vuln.PkgIdentifier.BOMRef] = append(vulns[vuln.PkgIdentifier.BOMRef], e.vulnerability(vuln))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Associate vulnerabilities with their corresponding components in the SBOM
|
||||||
|
for bomRef, componentVulns := range vulns {
|
||||||
|
c, ok := components[bomRef]
|
||||||
|
if !ok {
|
||||||
|
// This should never happen in proper SBOM rescanning because vulnerabilities
|
||||||
|
// should only be detected for components that exist in the original SBOM
|
||||||
|
log.Warn("Skipping vulnerabilities for component not found in SBOM",
|
||||||
|
log.String("bom-ref", bomRef),
|
||||||
|
log.Int("vulnerabilities", len(componentVulns)))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bom.AddVulnerabilities(c, componentVulns)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ftypes.PkgIdentifier{}
|
|
||||||
|
return bom, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Encoder) resultComponent(root *core.Component, r types.Result, osFound *ftypes.OS) *core.Component {
|
func (e *Encoder) resultComponent(root *core.Component, r types.Result, osFound *ftypes.OS) *core.Component {
|
||||||
@@ -285,10 +320,8 @@ func (e *Encoder) resultComponent(root *core.Component, r types.Result, osFound
|
|||||||
component.Version = osFound.Name
|
component.Version = osFound.Name
|
||||||
}
|
}
|
||||||
component.Type = core.TypeOS
|
component.Type = core.TypeOS
|
||||||
component.PkgIdentifier = e.existedPkgIdentifier(component.Name, component.Type)
|
|
||||||
case types.ClassLangPkg:
|
case types.ClassLangPkg:
|
||||||
component.Type = core.TypeApplication
|
component.Type = core.TypeApplication
|
||||||
component.PkgIdentifier = e.existedPkgIdentifier(component.Name, component.Type)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
e.bom.AddRelationship(root, component, core.RelationshipContains)
|
e.bom.AddRelationship(root, component, core.RelationshipContains)
|
||||||
|
|||||||
@@ -1000,44 +1000,20 @@ func TestEncoder_Encode(t *testing.T) {
|
|||||||
SchemaVersion: 2,
|
SchemaVersion: 2,
|
||||||
ArtifactName: "report.cdx.json",
|
ArtifactName: "report.cdx.json",
|
||||||
ArtifactType: ftypes.TypeCycloneDX,
|
ArtifactType: ftypes.TypeCycloneDX,
|
||||||
Results: []types.Result{
|
BOM: newTestBOM(t),
|
||||||
{
|
|
||||||
Target: "Java",
|
|
||||||
Type: ftypes.Jar,
|
|
||||||
Class: types.ClassLangPkg,
|
|
||||||
Packages: []ftypes.Package{
|
|
||||||
{
|
|
||||||
ID: "org.apache.logging.log4j:log4j-core:2.23.1",
|
|
||||||
Name: "org.apache.logging.log4j:log4j-core",
|
|
||||||
Version: "2.23.1",
|
|
||||||
Identifier: ftypes.PkgIdentifier{
|
|
||||||
UID: "6C0AE96901617503",
|
|
||||||
PURL: &packageurl.PackageURL{
|
|
||||||
Type: packageurl.TypeMaven,
|
|
||||||
Namespace: "org.apache.logging.log4j",
|
|
||||||
Name: "log4j-core",
|
|
||||||
Version: "2.23.1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
FilePath: "log4j-core-2.23.1.jar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
BOM: newTestBOM(t),
|
|
||||||
},
|
},
|
||||||
wantComponents: map[uuid.UUID]*core.Component{
|
wantComponents: map[uuid.UUID]*core.Component{
|
||||||
uuid.MustParse("2ff14136-e09f-4df9-80ea-000000000001"): appComponent,
|
uuid.MustParse("2ff14136-e09f-4df9-80ea-000000000001"): appComponent,
|
||||||
uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"): libComponent,
|
uuid.MustParse("2ff14136-e09f-4df9-80ea-000000000002"): libComponent,
|
||||||
},
|
},
|
||||||
wantRels: map[uuid.UUID][]core.Relationship{
|
wantRels: map[uuid.UUID][]core.Relationship{
|
||||||
uuid.MustParse("2ff14136-e09f-4df9-80ea-000000000001"): {
|
uuid.MustParse("2ff14136-e09f-4df9-80ea-000000000001"): {
|
||||||
{
|
{
|
||||||
Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"),
|
Dependency: uuid.MustParse("2ff14136-e09f-4df9-80ea-000000000002"),
|
||||||
Type: core.RelationshipContains,
|
Type: core.RelationshipContains,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"): nil,
|
uuid.MustParse("2ff14136-e09f-4df9-80ea-000000000002"): nil,
|
||||||
},
|
},
|
||||||
wantVulns: make(map[uuid.UUID][]core.Vulnerability),
|
wantVulns: make(map[uuid.UUID][]core.Vulnerability),
|
||||||
},
|
},
|
||||||
@@ -1047,44 +1023,13 @@ func TestEncoder_Encode(t *testing.T) {
|
|||||||
SchemaVersion: 2,
|
SchemaVersion: 2,
|
||||||
ArtifactName: "report.cdx.json",
|
ArtifactName: "report.cdx.json",
|
||||||
ArtifactType: ftypes.TypeCycloneDX,
|
ArtifactType: ftypes.TypeCycloneDX,
|
||||||
Results: []types.Result{
|
BOM: newTestBOM2(t),
|
||||||
{
|
|
||||||
Target: "Java",
|
|
||||||
Type: ftypes.Jar,
|
|
||||||
Class: types.ClassLangPkg,
|
|
||||||
Packages: []ftypes.Package{
|
|
||||||
{
|
|
||||||
ID: "org.apache.logging.log4j:log4j-core:2.23.1",
|
|
||||||
Name: "org.apache.logging.log4j:log4j-core",
|
|
||||||
Version: "2.23.1",
|
|
||||||
Identifier: ftypes.PkgIdentifier{
|
|
||||||
UID: "6C0AE96901617503",
|
|
||||||
PURL: &packageurl.PackageURL{
|
|
||||||
Type: packageurl.TypeMaven,
|
|
||||||
Namespace: "org.apache.logging.log4j",
|
|
||||||
Name: "log4j-core",
|
|
||||||
Version: "2.23.1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
FilePath: "log4j-core-2.23.1.jar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
BOM: newTestBOM2(t),
|
|
||||||
},
|
},
|
||||||
wantComponents: map[uuid.UUID]*core.Component{
|
wantComponents: map[uuid.UUID]*core.Component{
|
||||||
uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"): fsComponent,
|
uuid.MustParse("2ff14136-e09f-4df9-80ea-000000000001"): libComponent,
|
||||||
uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"): libComponent,
|
|
||||||
},
|
},
|
||||||
wantRels: map[uuid.UUID][]core.Relationship{
|
wantRels: map[uuid.UUID][]core.Relationship{
|
||||||
uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"): {
|
uuid.MustParse("2ff14136-e09f-4df9-80ea-000000000001"): nil,
|
||||||
{
|
|
||||||
Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"),
|
|
||||||
Type: core.RelationshipContains,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"): nil,
|
|
||||||
},
|
},
|
||||||
wantVulns: make(map[uuid.UUID][]core.Vulnerability),
|
wantVulns: make(map[uuid.UUID][]core.Vulnerability),
|
||||||
},
|
},
|
||||||
@@ -1600,7 +1545,17 @@ var (
|
|||||||
func newTestBOM(t *testing.T) *core.BOM {
|
func newTestBOM(t *testing.T) *core.BOM {
|
||||||
uuid.SetFakeUUID(t, "2ff14136-e09f-4df9-80ea-%012d")
|
uuid.SetFakeUUID(t, "2ff14136-e09f-4df9-80ea-%012d")
|
||||||
bom := core.NewBOM(core.Options{})
|
bom := core.NewBOM(core.Options{})
|
||||||
bom.AddComponent(appComponent)
|
|
||||||
|
// Copy components to avoid UUID conflicts between tests
|
||||||
|
appComp := appComponent.Clone()
|
||||||
|
libComp := libComponent.Clone()
|
||||||
|
|
||||||
|
bom.AddComponent(appComp)
|
||||||
|
bom.AddComponent(libComp)
|
||||||
|
// Add Contains relationship between appComponent and libComponent
|
||||||
|
bom.AddRelationship(appComp, libComp, core.RelationshipContains)
|
||||||
|
// Add empty relationship for libComponent to preserve structure for SBOM rescanning
|
||||||
|
bom.AddRelationship(libComp, nil, core.RelationshipDependsOn)
|
||||||
return bom
|
return bom
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1608,6 +1563,12 @@ func newTestBOM(t *testing.T) *core.BOM {
|
|||||||
func newTestBOM2(t *testing.T) *core.BOM {
|
func newTestBOM2(t *testing.T) *core.BOM {
|
||||||
uuid.SetFakeUUID(t, "2ff14136-e09f-4df9-80ea-%012d")
|
uuid.SetFakeUUID(t, "2ff14136-e09f-4df9-80ea-%012d")
|
||||||
bom := core.NewBOM(core.Options{})
|
bom := core.NewBOM(core.Options{})
|
||||||
bom.AddComponent(libComponent)
|
|
||||||
|
// Copy component to avoid UUID conflicts between tests
|
||||||
|
libComp := libComponent.Clone()
|
||||||
|
|
||||||
|
bom.AddComponent(libComp)
|
||||||
|
// Add empty relationship for libComponent to preserve structure for SBOM rescanning
|
||||||
|
bom.AddRelationship(libComp, nil, core.RelationshipDependsOn)
|
||||||
return bom
|
return bom
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,18 +184,20 @@ func decodeAttestCycloneDXJSONFormat(r io.ReadSeeker) (Format, bool) {
|
|||||||
func Decode(ctx context.Context, f io.Reader, format Format) (types.SBOM, error) {
|
func Decode(ctx context.Context, f io.Reader, format Format) (types.SBOM, error) {
|
||||||
var (
|
var (
|
||||||
v any
|
v any
|
||||||
bom = core.NewBOM(core.Options{})
|
bom *core.BOM
|
||||||
decoder interface{ Decode(any) error }
|
decoder interface{ Decode(any) error }
|
||||||
)
|
)
|
||||||
|
|
||||||
switch format {
|
switch format {
|
||||||
case FormatCycloneDXJSON:
|
case FormatCycloneDXJSON:
|
||||||
|
bom = core.NewBOM(core.Options{GenerateBOMRef: true})
|
||||||
v = &cyclonedx.BOM{BOM: bom}
|
v = &cyclonedx.BOM{BOM: bom}
|
||||||
decoder = json.NewDecoder(f)
|
decoder = json.NewDecoder(f)
|
||||||
case FormatAttestCycloneDXJSON:
|
case FormatAttestCycloneDXJSON:
|
||||||
// dsse envelope
|
// dsse envelope
|
||||||
// => in-toto attestation
|
// => in-toto attestation
|
||||||
// => CycloneDX JSON
|
// => CycloneDX JSON
|
||||||
|
bom = core.NewBOM(core.Options{GenerateBOMRef: true})
|
||||||
v = &attestation.Statement{
|
v = &attestation.Statement{
|
||||||
Predicate: &cyclonedx.BOM{BOM: bom},
|
Predicate: &cyclonedx.BOM{BOM: bom},
|
||||||
}
|
}
|
||||||
@@ -205,6 +207,7 @@ func Decode(ctx context.Context, f io.Reader, format Format) (types.SBOM, error)
|
|||||||
// => in-toto attestation
|
// => in-toto attestation
|
||||||
// => cosign predicate
|
// => cosign predicate
|
||||||
// => CycloneDX JSON
|
// => CycloneDX JSON
|
||||||
|
bom = core.NewBOM(core.Options{GenerateBOMRef: true})
|
||||||
v = &attestation.Statement{
|
v = &attestation.Statement{
|
||||||
Predicate: &attestation.CosignPredicate{
|
Predicate: &attestation.CosignPredicate{
|
||||||
Data: &cyclonedx.BOM{BOM: bom},
|
Data: &cyclonedx.BOM{BOM: bom},
|
||||||
@@ -212,9 +215,11 @@ func Decode(ctx context.Context, f io.Reader, format Format) (types.SBOM, error)
|
|||||||
}
|
}
|
||||||
decoder = json.NewDecoder(f)
|
decoder = json.NewDecoder(f)
|
||||||
case FormatSPDXJSON:
|
case FormatSPDXJSON:
|
||||||
|
bom = core.NewBOM(core.Options{})
|
||||||
v = &spdx.SPDX{BOM: bom}
|
v = &spdx.SPDX{BOM: bom}
|
||||||
decoder = json.NewDecoder(f)
|
decoder = json.NewDecoder(f)
|
||||||
case FormatSPDXTV:
|
case FormatSPDXTV:
|
||||||
|
bom = core.NewBOM(core.Options{})
|
||||||
v = &spdx.SPDX{BOM: bom}
|
v = &spdx.SPDX{BOM: bom}
|
||||||
decoder = spdx.NewTVDecoder(f)
|
decoder = spdx.NewTVDecoder(f)
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/aquasecurity/trivy/pkg/log"
|
"github.com/aquasecurity/trivy/pkg/log"
|
||||||
"github.com/aquasecurity/trivy/pkg/sbom/core"
|
"github.com/aquasecurity/trivy/pkg/sbom/core"
|
||||||
"github.com/aquasecurity/trivy/pkg/types"
|
"github.com/aquasecurity/trivy/pkg/types"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/uuid"
|
||||||
"github.com/aquasecurity/trivy/pkg/vex"
|
"github.com/aquasecurity/trivy/pkg/vex"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -156,6 +157,9 @@ func TestFilter(t *testing.T) {
|
|||||||
// Set up the OCI registry
|
// Set up the OCI registry
|
||||||
tr, d := setUpRegistry(t)
|
tr, d := setUpRegistry(t)
|
||||||
|
|
||||||
|
uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d")
|
||||||
|
testCycloneDXSBOM := createCycloneDXBOMWithSpringComponent()
|
||||||
|
|
||||||
type args struct {
|
type args struct {
|
||||||
report *types.Report
|
report *types.Report
|
||||||
opts vex.Options
|
opts vex.Options
|
||||||
@@ -329,10 +333,7 @@ func TestFilter(t *testing.T) {
|
|||||||
args: args{
|
args: args{
|
||||||
report: &types.Report{
|
report: &types.Report{
|
||||||
ArtifactType: ftypes.TypeCycloneDX,
|
ArtifactType: ftypes.TypeCycloneDX,
|
||||||
BOM: &core.BOM{
|
BOM: testCycloneDXSBOM,
|
||||||
SerialNumber: "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
|
|
||||||
Version: 1,
|
|
||||||
},
|
|
||||||
Results: []types.Result{
|
Results: []types.Result{
|
||||||
springResult(types.Result{
|
springResult(types.Result{
|
||||||
Vulnerabilities: []types.DetectedVulnerability{vuln1},
|
Vulnerabilities: []types.DetectedVulnerability{vuln1},
|
||||||
@@ -350,10 +351,7 @@ func TestFilter(t *testing.T) {
|
|||||||
},
|
},
|
||||||
want: &types.Report{
|
want: &types.Report{
|
||||||
ArtifactType: ftypes.TypeCycloneDX,
|
ArtifactType: ftypes.TypeCycloneDX,
|
||||||
BOM: &core.BOM{
|
BOM: testCycloneDXSBOM,
|
||||||
SerialNumber: "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
|
|
||||||
Version: 1,
|
|
||||||
},
|
|
||||||
Results: []types.Result{
|
Results: []types.Result{
|
||||||
springResult(types.Result{
|
springResult(types.Result{
|
||||||
Vulnerabilities: []types.DetectedVulnerability{},
|
Vulnerabilities: []types.DetectedVulnerability{},
|
||||||
@@ -617,6 +615,22 @@ func ociPURLString(ts *httptest.Server, d v1.Hash) string {
|
|||||||
return p.String()
|
return p.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createCycloneDXBOMWithSpringComponent() *core.BOM {
|
||||||
|
bom := core.NewBOM(core.Options{})
|
||||||
|
bom.SerialNumber = "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79"
|
||||||
|
bom.Version = 1
|
||||||
|
// Add the spring component to match vuln1's BOM-Ref
|
||||||
|
springComponent := &core.Component{
|
||||||
|
Type: core.TypeLibrary,
|
||||||
|
Name: springPackage.Identifier.PURL.Name,
|
||||||
|
Group: springPackage.Identifier.PURL.Namespace,
|
||||||
|
Version: springPackage.Version,
|
||||||
|
PkgIdentifier: springPackage.Identifier,
|
||||||
|
}
|
||||||
|
bom.AddComponent(springComponent)
|
||||||
|
return bom
|
||||||
|
}
|
||||||
|
|
||||||
func fsReport(results types.Results) *types.Report {
|
func fsReport(results types.Results) *types.Report {
|
||||||
return &types.Report{
|
return &types.Report{
|
||||||
ArtifactName: ".",
|
ArtifactName: ".",
|
||||||
|
|||||||
Reference in New Issue
Block a user