diff --git a/syft/formats/common/spdxhelpers/to_syft_model.go b/syft/formats/common/spdxhelpers/to_syft_model.go index 9d7f05b3e..6eb2e5ca3 100644 --- a/syft/formats/common/spdxhelpers/to_syft_model.go +++ b/syft/formats/common/spdxhelpers/to_syft_model.go @@ -12,6 +12,7 @@ import ( "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/formats/common/util" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" @@ -293,6 +294,7 @@ func toSyftPackage(p *spdx.Package2_2) *pkg.Package { return &sP } +//nolint:funlen func extractMetadata(p *spdx.Package2_2, info pkgInfo) (pkg.MetadataType, interface{}) { arch := info.qualifierValue(pkg.PURLQualifierArch) upstreamValue := info.qualifierValue(pkg.PURLQualifierUpstream) @@ -352,6 +354,20 @@ func extractMetadata(p *spdx.Package2_2, info pkgInfo) (pkg.MetadataType, interf return pkg.JavaMetadataType, pkg.JavaMetadata{ ArchiveDigests: digests, } + case pkg.GoModulePkg: + var h1Digest string + for _, value := range p.PackageChecksums { + digest, err := util.HDigestFromSHA(string(value.Algorithm), value.Value) + if err != nil { + log.Debugf("invalid h1digest: %v %v", value, err) + continue + } + h1Digest = digest + break + } + return pkg.GolangBinMetadataType, pkg.GolangBinMetadata{ + H1Digest: h1Digest, + } } return pkg.UnknownMetadataType, nil } diff --git a/syft/formats/common/spdxhelpers/to_syft_model_test.go b/syft/formats/common/spdxhelpers/to_syft_model_test.go index 10d35bac6..4ca8bb61b 100644 --- a/syft/formats/common/spdxhelpers/to_syft_model_test.go +++ b/syft/formats/common/spdxhelpers/to_syft_model_test.go @@ -234,3 +234,84 @@ func TestExtractSourceFromNamespaces(t *testing.T) { require.Equal(t, tt.expected, extractSchemeFromNamespace(tt.namespace)) } } + +func TestH1Digest(t *testing.T) { + tests := []struct { + name string + pkg spdx.Package2_2 + expectedDigest string + }{ + { + name: "valid h1digest", + pkg: spdx.Package2_2{ + PackageName: "github.com/googleapis/gnostic", + PackageVersion: "v0.5.5", + PackageExternalReferences: []*spdx.PackageExternalReference2_2{ + { + Category: "PACKAGE_MANAGER", + Locator: "pkg:golang/github.com/googleapis/gnostic@v0.5.5", + RefType: "purl", + }, + }, + PackageChecksums: map[spdx.ChecksumAlgorithm]spdx.Checksum{ + spdx.SHA256: { + Algorithm: spdx.SHA256, + Value: "f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c", + }, + }, + }, + expectedDigest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=", + }, + { + name: "invalid h1digest algorithm", + pkg: spdx.Package2_2{ + PackageName: "github.com/googleapis/gnostic", + PackageVersion: "v0.5.5", + PackageExternalReferences: []*spdx.PackageExternalReference2_2{ + { + Category: "PACKAGE_MANAGER", + Locator: "pkg:golang/github.com/googleapis/gnostic@v0.5.5", + RefType: "purl", + }, + }, + PackageChecksums: map[spdx.ChecksumAlgorithm]spdx.Checksum{ + spdx.SHA256: { + Algorithm: spdx.SHA1, + Value: "f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c", + }, + }, + }, + expectedDigest: "", + }, + { + name: "invalid h1digest digest", + pkg: spdx.Package2_2{ + PackageName: "github.com/googleapis/gnostic", + PackageVersion: "v0.5.5", + PackageExternalReferences: []*spdx.PackageExternalReference2_2{ + { + Category: "PACKAGE_MANAGER", + Locator: "pkg:golang/github.com/googleapis/gnostic@v0.5.5", + RefType: "purl", + }, + }, + PackageChecksums: map[spdx.ChecksumAlgorithm]spdx.Checksum{ + spdx.SHA256: { + Algorithm: spdx.SHA256, + Value: "", + }, + }, + }, + expectedDigest: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + p := toSyftPackage(&test.pkg) + require.Equal(t, pkg.GolangBinMetadataType, p.MetadataType) + meta := p.Metadata.(pkg.GolangBinMetadata) + require.Equal(t, test.expectedDigest, meta.H1Digest) + }) + } +} diff --git a/syft/formats/common/util/h_digest.go b/syft/formats/common/util/h_digest.go new file mode 100644 index 000000000..a3c5d2048 --- /dev/null +++ b/syft/formats/common/util/h_digest.go @@ -0,0 +1,57 @@ +package util + +import ( + "encoding/base64" + "encoding/hex" + "fmt" + "strings" +) + +// HDigestToSHA converts a h# digest, such as h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= to an +// algorithm such as sha256 and a hex encoded digest +func HDigestToSHA(digest string) (string, string, error) { + // hash is base64, but we need hex encode + parts := strings.Split(digest, ":") + if len(parts) == 2 { + algo := parts[0] + hash := parts[1] + checksum, err := base64.StdEncoding.DecodeString(hash) + if err != nil { + return "", "", err + } + + hexStr := hex.EncodeToString(checksum) + + switch algo { + // golang h1 hash == sha256 + case "h1": + algo = "sha256" + default: + return "", "", fmt.Errorf("unsupported h#digest algorithm: %s", algo) + } + + return algo, hexStr, nil + } + + return "", "", fmt.Errorf("invalid h#digest: %s", digest) +} + +// HDigestFromSHA converts an algorithm, such sha256 with a hex encoded digest to a +// h# value such as h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +func HDigestFromSHA(algorithm string, digest string) (string, error) { + if digest == "" { + return "", fmt.Errorf("no digest value provided") + } + // digest is hex, but we need to base64 encode + algorithm = strings.ToLower(algorithm) + if algorithm == "sha256" { + checksum, err := hex.DecodeString(digest) + if err != nil { + return "", err + } + // hash is hex, but we need base64 + b64digest := base64.StdEncoding.EncodeToString(checksum) + return fmt.Sprintf("h1:%s", b64digest), nil + } + return "", fmt.Errorf("not a recognized h#digest algorithm: %s", algorithm) +} diff --git a/syft/formats/common/util/h_digest_test.go b/syft/formats/common/util/h_digest_test.go new file mode 100644 index 000000000..329fef372 --- /dev/null +++ b/syft/formats/common/util/h_digest_test.go @@ -0,0 +1,107 @@ +package util + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_HDigestToSHA(t *testing.T) { + tests := []struct { + name string + hDigest string + expected string + error bool + }{ + { + name: "valid h1digest", + hDigest: "h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=", + expected: "sha256:f10a9c0e0ceb52a9546ff1b63d04d68ae786a33b91d7cf3881d74ccb093a88b5", + error: false, + }, + { + name: "other valid h1digest", + hDigest: "h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=", + expected: "sha256:4933fc0ef0f273f748e5bf13e61b21b648d2f84e364e7cac34250f7637221a16", + error: false, + }, + { + name: "invalid h1digest", + hDigest: "h12:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=", + expected: "", + error: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + algo, digest, err := HDigestToSHA(test.hDigest) + if test.error { + require.Error(t, err) + return + } else { + require.NoError(t, err) + } + got := fmt.Sprintf("%s:%s", algo, digest) + require.Equal(t, test.expected, got) + }) + } +} + +func Test_HDigestFromSHA(t *testing.T) { + tests := []struct { + name string + sha string + expected string + error bool + }{ + { + name: "valid sha", + sha: "sha256:f10a9c0e0ceb52a9546ff1b63d04d68ae786a33b91d7cf3881d74ccb093a88b5", + expected: "h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=", + error: false, + }, + { + name: "other valid sha", + sha: "sha256:4933fc0ef0f273f748e5bf13e61b21b648d2f84e364e7cac34250f7637221a16", + expected: "h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=", + error: false, + }, + { + name: "invalid sha", + expected: "h12:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=", + sha: "sha256:f10a9c0e0zzzzceb52a99968ae786a33b91d7cf3881d74ccb093a88b5", + error: true, + }, + { + name: "invalid algorithm", + expected: "h12:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=", + sha: "sha1:f10a9c0e0ceb52a9546ff1b63d04d68ae786a33b91d7cf3881d74ccb093a88b5", + error: true, + }, + { + name: "empty sha", + expected: "", + sha: "sha256:", + error: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + parts := strings.Split(test.sha, ":") + algo := parts[0] + digest := parts[1] + got, err := HDigestFromSHA(algo, digest) + if test.error { + require.Error(t, err) + return + } else { + require.NoError(t, err) + } + require.Equal(t, test.expected, got) + }) + } +} diff --git a/syft/formats/spdx22json/to_format_model.go b/syft/formats/spdx22json/to_format_model.go index f55aee965..44a865a49 100644 --- a/syft/formats/spdx22json/to_format_model.go +++ b/syft/formats/spdx22json/to_format_model.go @@ -12,6 +12,7 @@ import ( "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/formats/common/spdxhelpers" + "github.com/anchore/syft/syft/formats/common/util" "github.com/anchore/syft/syft/formats/spdx22json/model" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" @@ -53,24 +54,8 @@ func toPackages(catalog *pkg.Catalog, relationships []artifact.Relationship) []m for _, p := range catalog.Sorted() { license := spdxhelpers.License(p) packageSpdxID := model.ElementID(p.ID()).String() - filesAnalyzed := false + checksums, filesAnalyzed := toPackageChecksums(p) - // we generate digest for some Java packages - // see page 33 of the spdx specification for 2.2 - // spdx.github.io/spdx-spec/package-information/#710-package-checksum-field - var checksums []model.Checksum - if p.MetadataType == pkg.JavaMetadataType { - javaMetadata := p.Metadata.(pkg.JavaMetadata) - if len(javaMetadata.ArchiveDigests) > 0 { - filesAnalyzed = true - for _, digest := range javaMetadata.ArchiveDigests { - checksums = append(checksums, model.Checksum{ - Algorithm: strings.ToUpper(digest.Algorithm), - ChecksumValue: digest.Value, - }) - } - } - } // note: the license concluded and declared should be the same since we are collecting license information // from the project data itself (the installed package files). packages = append(packages, model.Package{ @@ -100,6 +85,38 @@ func toPackages(catalog *pkg.Catalog, relationships []artifact.Relationship) []m return packages } +func toPackageChecksums(p pkg.Package) ([]model.Checksum, bool) { + filesAnalyzed := false + var checksums []model.Checksum + switch meta := p.Metadata.(type) { + // we generate digest for some Java packages + // see page 33 of the spdx specification for 2.2 + // spdx.github.io/spdx-spec/package-information/#710-package-checksum-field + case pkg.JavaMetadata: + if len(meta.ArchiveDigests) > 0 { + filesAnalyzed = true + for _, digest := range meta.ArchiveDigests { + checksums = append(checksums, model.Checksum{ + Algorithm: strings.ToUpper(digest.Algorithm), + ChecksumValue: digest.Value, + }) + } + } + case pkg.GolangBinMetadata: + algo, hexStr, err := util.HDigestToSHA(meta.H1Digest) + if err != nil { + log.Debugf("invalid h1digest: %s: %v", meta.H1Digest, err) + break + } + algo = strings.ToUpper(algo) + checksums = append(checksums, model.Checksum{ + Algorithm: strings.ToUpper(algo), + ChecksumValue: hexStr, + }) + } + return checksums, filesAnalyzed +} + func fileIDsForPackage(packageSpdxID string, relationships []artifact.Relationship) (fileIDs []string) { for _, relationship := range relationships { if relationship.Type != artifact.ContainsRelationship { diff --git a/syft/formats/spdx22json/to_format_model_test.go b/syft/formats/spdx22json/to_format_model_test.go index 2845568b9..254274a38 100644 --- a/syft/formats/spdx22json/to_format_model_test.go +++ b/syft/formats/spdx22json/to_format_model_test.go @@ -1,9 +1,11 @@ package spdx22json import ( + "fmt" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" @@ -253,3 +255,64 @@ func Test_fileIDsForPackage(t *testing.T) { }) } } + +func Test_H1Digest(t *testing.T) { + tests := []struct { + name string + pkg pkg.Package + expectedDigest string + }{ + { + name: "valid h1digest", + pkg: pkg.Package{ + Name: "github.com/googleapis/gnostic", + Version: "v0.5.5", + MetadataType: pkg.GolangBinMetadataType, + Metadata: pkg.GolangBinMetadata{ + H1Digest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=", + }, + }, + expectedDigest: "SHA256:f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c", + }, + { + name: "invalid h1digest", + pkg: pkg.Package{ + Name: "github.com/googleapis/gnostic", + Version: "v0.5.5", + MetadataType: pkg.GolangBinMetadataType, + Metadata: pkg.GolangBinMetadata{ + H1Digest: "h1:9fHAtK0uzzz", + }, + }, + expectedDigest: "", + }, + { + name: "unsupported h-digest", + pkg: pkg.Package{ + Name: "github.com/googleapis/gnostic", + Version: "v0.5.5", + MetadataType: pkg.GolangBinMetadataType, + Metadata: pkg.GolangBinMetadata{ + H1Digest: "h12:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=", + }, + }, + expectedDigest: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + catalog := pkg.NewCatalog(test.pkg) + pkgs := toPackages(catalog, nil) + require.Len(t, pkgs, 1) + p := pkgs[0] + if test.expectedDigest == "" { + require.Len(t, p.Checksums, 0) + } else { + require.Len(t, p.Checksums, 1) + c := p.Checksums[0] + require.Equal(t, test.expectedDigest, fmt.Sprintf("%s:%s", c.Algorithm, c.ChecksumValue)) + } + }) + } +} diff --git a/syft/formats/spdx22tagvalue/to_format_model.go b/syft/formats/spdx22tagvalue/to_format_model.go index ae4c00477..3dfd50484 100644 --- a/syft/formats/spdx22tagvalue/to_format_model.go +++ b/syft/formats/spdx22tagvalue/to_format_model.go @@ -2,13 +2,16 @@ package spdx22tagvalue import ( "fmt" + "strings" "time" "github.com/spdx/tools-golang/spdx" "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/spdxlicense" "github.com/anchore/syft/syft/formats/common/spdxhelpers" + "github.com/anchore/syft/syft/formats/common/util" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" ) @@ -101,24 +104,7 @@ func toFormatPackages(catalog *pkg.Catalog) map[spdx.ElementID]*spdx.Package2_2 // in the Comments on License field (section 3.16). With respect to NOASSERTION, a written explanation in // the Comments on License field (section 3.16) is preferred. license := spdxhelpers.License(p) - - filesAnalyzed := false - checksums := make(map[spdx.ChecksumAlgorithm]spdx.Checksum) - - // If the pkg type is Java we have attempted to generated a digest - // FilesAnalyzed should be true in this case - if p.MetadataType == pkg.JavaMetadataType { - javaMetadata := p.Metadata.(pkg.JavaMetadata) - if len(javaMetadata.ArchiveDigests) > 0 { - filesAnalyzed = true - for _, digest := range javaMetadata.ArchiveDigests { - checksums[spdx.ChecksumAlgorithm(digest.Algorithm)] = spdx.Checksum{ - Algorithm: spdx.ChecksumAlgorithm(digest.Algorithm), - Value: digest.Value, - } - } - } - } + checksums, filesAnalyzed := toPackageChecksums(p) results[spdx.ElementID(id)] = &spdx.Package2_2{ @@ -181,7 +167,7 @@ func toFormatPackages(catalog *pkg.Catalog) map[spdx.ElementID]*spdx.Package2_2 IsFilesAnalyzedTagPresent: true, // 3.9: Package Verification Code - // Cardinality: mandatory, one if filesAnalyzed is true / omitted; + // Cardinality: optional, one if filesAnalyzed is true / omitted; // zero (must be omitted) if filesAnalyzed is false PackageVerificationCode: "", // Spec also allows specifying a single file to exclude from the @@ -280,6 +266,38 @@ func toFormatPackages(catalog *pkg.Catalog) map[spdx.ElementID]*spdx.Package2_2 return results } +func toPackageChecksums(p pkg.Package) (map[spdx.ChecksumAlgorithm]spdx.Checksum, bool) { + filesAnalyzed := false + checksums := map[spdx.ChecksumAlgorithm]spdx.Checksum{} + switch meta := p.Metadata.(type) { + // we generate digest for some Java packages + // see page 33 of the spdx specification for 2.2 + // spdx.github.io/spdx-spec/package-information/#710-package-checksum-field + case pkg.JavaMetadata: + if len(meta.ArchiveDigests) > 0 { + filesAnalyzed = true + for _, digest := range meta.ArchiveDigests { + checksums[spdx.ChecksumAlgorithm(digest.Algorithm)] = spdx.Checksum{ + Algorithm: spdx.ChecksumAlgorithm(digest.Algorithm), + Value: digest.Value, + } + } + } + case pkg.GolangBinMetadata: + algo, hexStr, err := util.HDigestToSHA(meta.H1Digest) + if err != nil { + log.Debugf("invalid h1digest: %s: %v", meta.H1Digest, err) + break + } + algo = strings.ToUpper(algo) + checksums[spdx.ChecksumAlgorithm(algo)] = spdx.Checksum{ + Algorithm: spdx.ChecksumAlgorithm(algo), + Value: hexStr, + } + } + return checksums, filesAnalyzed +} + func formatSPDXExternalRefs(p pkg.Package) (refs []*spdx.PackageExternalReference2_2) { for _, ref := range spdxhelpers.ExternalRefs(p) { refs = append(refs, &spdx.PackageExternalReference2_2{ diff --git a/syft/formats/spdx22tagvalue/to_format_model_test.go b/syft/formats/spdx22tagvalue/to_format_model_test.go new file mode 100644 index 000000000..3b302b864 --- /dev/null +++ b/syft/formats/spdx22tagvalue/to_format_model_test.go @@ -0,0 +1,73 @@ +package spdx22tagvalue + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/pkg" +) + +func Test_H1Digest(t *testing.T) { + tests := []struct { + name string + pkg pkg.Package + expectedDigest string + }{ + { + name: "valid h1digest", + pkg: pkg.Package{ + Name: "github.com/googleapis/gnostic", + Version: "v0.5.5", + MetadataType: pkg.GolangBinMetadataType, + Metadata: pkg.GolangBinMetadata{ + H1Digest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=", + }, + }, + expectedDigest: "SHA256:f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c", + }, + { + name: "invalid h1digest", + pkg: pkg.Package{ + Name: "github.com/googleapis/gnostic", + Version: "v0.5.5", + MetadataType: pkg.GolangBinMetadataType, + Metadata: pkg.GolangBinMetadata{ + H1Digest: "h1:9fHAtK0uzzz", + }, + }, + expectedDigest: "", + }, + { + name: "unsupported h-digest", + pkg: pkg.Package{ + Name: "github.com/googleapis/gnostic", + Version: "v0.5.5", + MetadataType: pkg.GolangBinMetadataType, + Metadata: pkg.GolangBinMetadata{ + H1Digest: "h12:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=", + }, + }, + expectedDigest: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + catalog := pkg.NewCatalog(test.pkg) + pkgs := toFormatPackages(catalog) + require.Len(t, pkgs, 1) + for _, p := range pkgs { + if test.expectedDigest == "" { + require.Len(t, p.PackageChecksums, 0) + } else { + require.Len(t, p.PackageChecksums, 1) + for _, c := range p.PackageChecksums { + require.Equal(t, test.expectedDigest, fmt.Sprintf("%s:%s", c.Algorithm, c.Value)) + } + } + } + }) + } +}