diff --git a/go.mod b/go.mod index b2af4735..a02fffc2 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,11 @@ go 1.16 require ( github.com/BurntSushi/toml v1.2.1 github.com/Masterminds/semver/v3 v3.2.0 + github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501 - github.com/anchore/syft v0.70.0 + github.com/anchore/stereoscope v0.0.0-20230216143338-4b5ebf8c7f4b + github.com/anchore/syft v0.72.0 github.com/apex/log v1.9.0 github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 github.com/gabriel-vasile/mimetype v1.4.1 @@ -16,6 +18,7 @@ require ( github.com/pelletier/go-toml v1.9.5 github.com/sclevine/spec v1.4.0 github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e + github.com/sergi/go-diff v1.3.1 github.com/spdx/tools-golang v0.5.0-rc1 github.com/stretchr/testify v1.8.1 github.com/ulikunitz/xz v0.5.11 diff --git a/go.sum b/go.sum index e8eaadbc..0b83d17b 100644 --- a/go.sum +++ b/go.sum @@ -447,7 +447,7 @@ github.com/Microsoft/hcsshim v0.8.20/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwT github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg= github.com/Microsoft/hcsshim v0.9.2/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= -github.com/Microsoft/hcsshim v0.9.5/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= +github.com/Microsoft/hcsshim v0.9.6/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= @@ -482,10 +482,10 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501 h1:AV7qjwMcM4r8wFhJq3jLRztew3ywIyPTRapl2T1s9o8= github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4= -github.com/anchore/stereoscope v0.0.0-20221208011002-c5ff155d72f1 h1:DXUAm/H9chRTEzMfkFyduBIcCiJyFXhCmv3zH3C0HGs= -github.com/anchore/stereoscope v0.0.0-20221208011002-c5ff155d72f1/go.mod h1:/zjVnu2Jdl7xQCUtASegzeEg+IHKrM7SyMqdao3e+Nc= -github.com/anchore/syft v0.70.0 h1:vJMGyMhGaiSU9bm/5bmgQm/j0ZODAPCp4x3K5mDw/mc= -github.com/anchore/syft v0.70.0/go.mod h1:Mylc9ChS0RP8Jb5/OrmgEJPpGM7o4+1EibuwlN4q9Wc= +github.com/anchore/stereoscope v0.0.0-20230216143338-4b5ebf8c7f4b h1:vMEAfz91QLjJq2W8JPxpIC4dG4OeynTY4MisHnZ19F0= +github.com/anchore/stereoscope v0.0.0-20230216143338-4b5ebf8c7f4b/go.mod h1:6oSG43mzahqiktzXZDctqi1o66fwU2wDk3xki0KlnbA= +github.com/anchore/syft v0.72.0 h1:EpZMDitSElK/Qm1zgrL/3HM2Cw0hHo7hy9uwrhIXDGA= +github.com/anchore/syft v0.72.0/go.mod h1:T3ZSrApwb+jwI+vyTfE5R54Xej4NBoQ8c2t1LyJWGao= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= @@ -519,6 +519,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.6.0/go.mod h1:q7o0j7d7HrJk/vr9uUt3BV github.com/aws/smithy-go v1.6.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220517224237-e6f29200ae04/go.mod h1:Z+bXnIbhKJYSvxNwsNnwde7pDKxuqlEZCbUBoTwAqf0= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= +github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA= +github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4= github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -599,6 +601,7 @@ github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4S github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= github.com/containerd/cgroups v1.0.3/go.mod h1:/ofk34relqNjSGyqPrmEULrO4Sc8LJhvJmWbUCUKqj8= +github.com/containerd/cgroups v1.0.4/go.mod h1:nLNQtsF7Sl2HxNebu77i1R0oDlhiTG+kO4JTrUzo6IA= github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= @@ -622,8 +625,8 @@ github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTV github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s= github.com/containerd/containerd v1.6.1/go.mod h1:1nJz5xCZPusx6jJU8Frfct988y0NpumIq9ODB0kLtoE= -github.com/containerd/containerd v1.6.12 h1:kJ9b3mOFKf8yqo05Ob+tMoxvt1pbVWhnB0re9Y+k+8c= -github.com/containerd/containerd v1.6.12/go.mod h1:K4Bw7gjgh4TnkmQY+py/PYQGp4e7xgnHAeg87VeWb3A= +github.com/containerd/containerd v1.6.18 h1:qZbsLvmyu+Vlty0/Ex5xc0z2YtKpIsb5n45mAMI+2Ns= +github.com/containerd/containerd v1.6.18/go.mod h1:1RdCUu95+gc2v9t3IL+zIlpClSmew7/0YS8O5eQZrOw= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= @@ -747,8 +750,8 @@ github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r github.com/docker/docker v20.10.10+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v20.10.12+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v20.10.20+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v23.0.0+incompatible h1:L6c28tNyqZ4/ub9AZC9d5QUuunoHHfEH4/Ue+h/E5nE= -github.com/docker/docker v23.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v23.0.1+incompatible h1:vjgvJZxprTTE1A37nm+CLNAdwu6xZekyoiVlUZEINcY= +github.com/docker/docker v23.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= @@ -1688,8 +1691,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20220823124025-807a23277127 h1:S4NrSKDfihhl3+4jSTgwoIevKxX9p7Iv9x++OEIptDo= -golang.org/x/exp v0.0.0-20220823124025-807a23277127/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/exp v0.0.0-20230202163644-54bba9f4231b h1:EqBVA+nNsObCwQoBEHy4wLU0pi7i8a4AL3pbItPdPkE= +golang.org/x/exp v0.0.0-20230202163644-54bba9f4231b/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/sbom/formatted_reader.go b/sbom/formatted_reader.go index ba648d98..a4462328 100644 --- a/sbom/formatted_reader.go +++ b/sbom/formatted_reader.go @@ -63,7 +63,7 @@ func (f *FormattedReader) Read(b []byte) (int, error) { // Makes CycloneDX SBOM more reproducible, see // https://github.com/paketo-buildpacks/packit/issues/367 for more details. - if f.format.ID() == "cyclonedx-1.3-json" || f.format.ID() == "cyclonedx-1-json" { + if f.format.ID() == "cyclonedx-1.3-json" || f.format.ID() == "cyclonedx-json" { var cycloneDXOutput map[string]interface{} err = json.Unmarshal(output, &cycloneDXOutput) if err != nil { diff --git a/sbom/formatted_reader_test.go b/sbom/formatted_reader_test.go index 0f388542..bfd79b23 100644 --- a/sbom/formatted_reader_test.go +++ b/sbom/formatted_reader_test.go @@ -77,12 +77,6 @@ func testFormattedReader(t *testing.T, context spec.G, it spec.S) { format := syft.IdentifyFormat(buffer.Bytes()) Expect(format.ID()).To(Equal(syft.CycloneDxJSONFormatID)) - // Ensures pretty printing - Expect(buffer.String()).To(ContainSubstring(`{ - "bomFormat": "CycloneDX", - "components": [ - {`)) - var cdxOutput cdxOutput err = json.Unmarshal(buffer.Bytes(), &cdxOutput) diff --git a/sbom/internal/formats/common/testutils/utils.go b/sbom/internal/formats/common/testutils/utils.go new file mode 100644 index 00000000..f214c4f0 --- /dev/null +++ b/sbom/internal/formats/common/testutils/utils.go @@ -0,0 +1,309 @@ +package testutils + +import ( + "bytes" + "math/rand" + "strings" + "testing" + "time" + + "github.com/sergi/go-diff/diffmatchpatch" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/go-testutils" + "github.com/anchore/stereoscope/pkg/filetree" + "github.com/anchore/stereoscope/pkg/image" + "github.com/anchore/stereoscope/pkg/imagetest" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/linux" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" +) + +type redactor func(s []byte) []byte + +type imageCfg struct { + fromSnapshot bool +} + +type ImageOption func(cfg *imageCfg) + +func FromSnapshot() ImageOption { + return func(cfg *imageCfg) { + cfg.fromSnapshot = true + } +} + +func AssertEncoderAgainstGoldenImageSnapshot(t *testing.T, format sbom.Format, sbom sbom.SBOM, testImage string, updateSnapshot bool, json bool, redactors ...redactor) { + var buffer bytes.Buffer + + // grab the latest image contents and persist + if updateSnapshot { + imagetest.UpdateGoldenFixtureImage(t, testImage) + } + + err := format.Encode(&buffer, sbom) + assert.NoError(t, err) + actual := buffer.Bytes() + + // replace the expected snapshot contents with the current encoder contents + if updateSnapshot { + testutils.UpdateGoldenFileContents(t, actual) + } + + var expected = testutils.GetGoldenFileContents(t) + + // remove dynamic values, which should be tested independently + redactors = append(redactors, carriageRedactor) + for _, r := range redactors { + actual = r(actual) + expected = r(expected) + } + + if json { + require.JSONEq(t, string(expected), string(actual)) + } else if !bytes.Equal(expected, actual) { + // assert that the golden file snapshot matches the actual contents + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(string(expected), string(actual), true) + t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs)) + } +} + +func AssertEncoderAgainstGoldenSnapshot(t *testing.T, format sbom.Format, sbom sbom.SBOM, updateSnapshot bool, json bool, redactors ...redactor) { + var buffer bytes.Buffer + + err := format.Encode(&buffer, sbom) + assert.NoError(t, err) + actual := buffer.Bytes() + + // replace the expected snapshot contents with the current encoder contents + if updateSnapshot { + testutils.UpdateGoldenFileContents(t, actual) + } + + var expected = testutils.GetGoldenFileContents(t) + + // remove dynamic values, which should be tested independently + redactors = append(redactors, carriageRedactor) + for _, r := range redactors { + actual = r(actual) + expected = r(expected) + } + + if json { + require.JSONEq(t, string(expected), string(actual)) + } else if !bytes.Equal(expected, actual) { + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(string(expected), string(actual), true) + t.Logf("len: %d\nexpected: %s", len(expected), expected) + t.Logf("len: %d\nactual: %s", len(actual), actual) + t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs)) + } +} + +func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBOM { + t.Helper() + catalog := pkg.NewCatalog() + var cfg imageCfg + var img *image.Image + for _, opt := range options { + opt(&cfg) + } + + switch cfg.fromSnapshot { + case true: + img = imagetest.GetGoldenFixtureImage(t, testImage) + default: + img = imagetest.GetFixtureImage(t, "docker-archive", testImage) + } + + populateImageCatalog(catalog, img) + + // this is a hard coded value that is not given by the fixture helper and must be provided manually + img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368" + + src, err := source.NewFromImage(img, "user-image-input") + assert.NoError(t, err) + + return sbom.SBOM{ + Artifacts: sbom.Artifacts{ + PackageCatalog: catalog, + LinuxDistribution: &linux.Release{ + PrettyName: "debian", + Name: "debian", + ID: "debian", + IDLike: []string{"like!"}, + Version: "1.2.3", + VersionID: "1.2.3", + }, + }, + Source: src.Metadata, + Descriptor: sbom.Descriptor{ + Name: "syft", + Version: "v0.42.0-bogus", + // the application configuration should be persisted here, however, we do not want to import + // the application configuration in this package (it's reserved only for ingestion by the cmd package) + Configuration: map[string]string{ + "config-key": "config-value", + }, + }, + } +} + +func carriageRedactor(s []byte) []byte { + msg := strings.ReplaceAll(string(s), "\r\n", "\n") + return []byte(msg) +} + +func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) { + _, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks) + _, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks) + + // populate catalog with test data + catalog.Add(pkg.Package{ + Name: "package-1", + Version: "1.0.1", + Locations: source.NewLocationSet( + source.NewLocationFromImage(string(ref1.RealPath), *ref1.Reference, img), + ), + Type: pkg.PythonPkg, + FoundBy: "the-cataloger-1", + Language: pkg.Python, + MetadataType: pkg.PythonPackageMetadataType, + Licenses: []string{"MIT"}, + Metadata: pkg.PythonPackageMetadata{ + Name: "package-1", + Version: "1.0.1", + }, + PURL: "a-purl-1", // intentionally a bad pURL for test fixtures + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"), + }, + }) + catalog.Add(pkg.Package{ + Name: "package-2", + Version: "2.0.1", + Locations: source.NewLocationSet( + source.NewLocationFromImage(string(ref2.RealPath), *ref2.Reference, img), + ), + Type: pkg.DebPkg, + FoundBy: "the-cataloger-2", + MetadataType: pkg.DpkgMetadataType, + Metadata: pkg.DpkgMetadata{ + Package: "package-2", + Version: "2.0.1", + }, + PURL: "pkg:deb/debian/package-2@2.0.1", + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + }, + }) +} + +func DirectoryInput(t testing.TB) sbom.SBOM { + catalog := newDirectoryCatalog() + + src, err := source.NewFromDirectory("/some/path") + assert.NoError(t, err) + + return sbom.SBOM{ + Artifacts: sbom.Artifacts{ + PackageCatalog: catalog, + LinuxDistribution: &linux.Release{ + PrettyName: "debian", + Name: "debian", + ID: "debian", + IDLike: []string{"like!"}, + Version: "1.2.3", + VersionID: "1.2.3", + }, + }, + Source: src.Metadata, + Descriptor: sbom.Descriptor{ + Name: "syft", + Version: "v0.42.0-bogus", + // the application configuration should be persisted here, however, we do not want to import + // the application configuration in this package (it's reserved only for ingestion by the cmd package) + Configuration: map[string]string{ + "config-key": "config-value", + }, + }, + } +} + +func newDirectoryCatalog() *pkg.Catalog { + catalog := pkg.NewCatalog() + + // populate catalog with test data + catalog.Add(pkg.Package{ + Name: "package-1", + Version: "1.0.1", + Type: pkg.PythonPkg, + FoundBy: "the-cataloger-1", + Locations: source.NewLocationSet( + source.NewLocation("/some/path/pkg1"), + ), + Language: pkg.Python, + MetadataType: pkg.PythonPackageMetadataType, + Licenses: []string{"MIT"}, + Metadata: pkg.PythonPackageMetadata{ + Name: "package-1", + Version: "1.0.1", + Files: []pkg.PythonFileRecord{ + { + Path: "/some/path/pkg1/dependencies/foo", + }, + }, + }, + PURL: "a-purl-2", // intentionally a bad pURL for test fixtures + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + }, + }) + catalog.Add(pkg.Package{ + Name: "package-2", + Version: "2.0.1", + Type: pkg.DebPkg, + FoundBy: "the-cataloger-2", + Locations: source.NewLocationSet( + source.NewLocation("/some/path/pkg1"), + ), + MetadataType: pkg.DpkgMetadataType, + Metadata: pkg.DpkgMetadata{ + Package: "package-2", + Version: "2.0.1", + }, + PURL: "pkg:deb/debian/package-2@2.0.1", + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + }, + }) + + return catalog +} + +//nolint:gosec +func AddSampleFileRelationships(s *sbom.SBOM) { + catalog := s.Artifacts.PackageCatalog.Sorted() + s.Artifacts.FileMetadata = map[source.Coordinates]source.FileMetadata{} + + files := []string{"/f1", "/f2", "/d1/f3", "/d2/f4", "/z1/f5", "/a1/f6"} + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + rnd.Shuffle(len(files), func(i, j int) { files[i], files[j] = files[j], files[i] }) + + for _, f := range files { + meta := source.FileMetadata{} + coords := source.Coordinates{RealPath: f} + s.Artifacts.FileMetadata[coords] = meta + + s.Relationships = append(s.Relationships, artifact.Relationship{ + From: catalog[0], + To: coords, + Type: artifact.ContainsRelationship, + }) + } +} diff --git a/sbom/internal/formats/cyclonedx13/encoder_test.go b/sbom/internal/formats/cyclonedx13/encoder_test.go index d893d352..031c38bc 100644 --- a/sbom/internal/formats/cyclonedx13/encoder_test.go +++ b/sbom/internal/formats/cyclonedx13/encoder_test.go @@ -5,7 +5,7 @@ import ( "regexp" "testing" - "github.com/anchore/syft/syft/formats/common/testutils" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/common/testutils" ) var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx encoders") diff --git a/sbom/internal/formats/cyclonedx13/format.go b/sbom/internal/formats/cyclonedx13/format.go index aec773c7..b60c3b31 100644 --- a/sbom/internal/formats/cyclonedx13/format.go +++ b/sbom/internal/formats/cyclonedx13/format.go @@ -9,19 +9,12 @@ import ( // TODO: Decide version granularity of IDs const ID sbom.FormatID = "cyclonedx-1.3-json" -var dummyDecoder func(io.Reader) (*sbom.SBOM, error) = func(input io.Reader) (*sbom.SBOM, error) { - return nil, nil -} - -var dummyValidator func(io.Reader) error = func(input io.Reader) error { - return nil -} - func Format() sbom.Format { return sbom.NewFormat( - ID, + sbom.AnyVersion, encoder, - dummyDecoder, - dummyValidator, + func(input io.Reader) (*sbom.SBOM, error) { return nil, nil }, + func(input io.Reader) error { return nil }, + ID, ) } diff --git a/sbom/internal/formats/spdx22/encoder_test.go b/sbom/internal/formats/spdx22/encoder_test.go index a80910f0..56177343 100644 --- a/sbom/internal/formats/spdx22/encoder_test.go +++ b/sbom/internal/formats/spdx22/encoder_test.go @@ -5,7 +5,7 @@ import ( "regexp" "testing" - "github.com/anchore/syft/syft/formats/common/testutils" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/common/testutils" ) var updateSpdxJson = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json encoders") diff --git a/sbom/internal/formats/spdx22/format.go b/sbom/internal/formats/spdx22/format.go index 1fdf1131..95bee545 100644 --- a/sbom/internal/formats/spdx22/format.go +++ b/sbom/internal/formats/spdx22/format.go @@ -8,21 +8,12 @@ import ( const ID sbom.FormatID = "spdx-2-json" -// Decoder not implemented because it's not needed for buildpacks' SBOM generation -var dummyDecoder func(io.Reader) (*sbom.SBOM, error) = func(input io.Reader) (*sbom.SBOM, error) { - return nil, nil -} - -// Validator not implemented because it's not needed for buildpacks' SBOM generation -var dummyValidator func(io.Reader) error = func(input io.Reader) error { - return nil -} - func Format() sbom.Format { return sbom.NewFormat( - ID, + "2.2", encoder, - dummyDecoder, - dummyValidator, + func(input io.Reader) (*sbom.SBOM, error) { return nil, nil }, + func(input io.Reader) error { return nil }, + ID, ) } diff --git a/sbom/internal/formats/syft2/encoder_test.go b/sbom/internal/formats/syft2/encoder_test.go index b67573e2..32c6037a 100644 --- a/sbom/internal/formats/syft2/encoder_test.go +++ b/sbom/internal/formats/syft2/encoder_test.go @@ -4,17 +4,15 @@ import ( "flag" "testing" + stereoFile "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/linux" - - "github.com/anchore/syft/syft/artifact" - "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" - - "github.com/anchore/syft/syft/formats/common/testutils" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/common/testutils" ) // Source https://github.com/anchore/syft/blob/dfefd2ea4e9d44187b4f861bc202970247dd34c8/internal/formats/syftjson/encoder_test.go @@ -103,26 +101,26 @@ func TestEncodeFullJSONDocument(t *testing.T) { FileMetadata: map[source.Coordinates]source.FileMetadata{ source.NewLocation("/a/place").Coordinates: { Mode: 0775, - Type: "directory", + Type: stereoFile.TypeDirectory, UserID: 0, GroupID: 0, }, source.NewLocation("/a/place/a").Coordinates: { Mode: 0775, - Type: "regularFile", + Type: stereoFile.TypeRegular, UserID: 0, GroupID: 0, }, source.NewLocation("/b").Coordinates: { Mode: 0775, - Type: "symbolicLink", + Type: stereoFile.TypeSymLink, LinkDestination: "/c", UserID: 0, GroupID: 0, }, source.NewLocation("/b/place/b").Coordinates: { Mode: 0644, - Type: "regularFile", + Type: stereoFile.TypeRegular, UserID: 1, GroupID: 2, }, diff --git a/sbom/internal/formats/syft2/format.go b/sbom/internal/formats/syft2/format.go index 256c17f2..d1e33114 100644 --- a/sbom/internal/formats/syft2/format.go +++ b/sbom/internal/formats/syft2/format.go @@ -9,21 +9,12 @@ import ( const ID sbom.FormatID = "syft-2.0-json" const JSONSchemaVersion string = "2.0.2" -// Decoder not implemented because it's not needed for buildpacks' SBOM generation -var dummyDecoder func(io.Reader) (*sbom.SBOM, error) = func(input io.Reader) (*sbom.SBOM, error) { - return nil, nil -} - -// Validator not implemented because it's not needed for buildpacks' SBOM generation -var dummyValidator func(io.Reader) error = func(input io.Reader) error { - return nil -} - func Format() sbom.Format { return sbom.NewFormat( - ID, + "2.0.2", encoder, - dummyDecoder, - dummyValidator, + func(input io.Reader) (*sbom.SBOM, error) { return nil, nil }, + func(input io.Reader) error { return nil }, + ID, ) } diff --git a/sbom/internal/formats/syft2/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden b/sbom/internal/formats/syft2/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden index 620cb151..b3f23b6d 100644 --- a/sbom/internal/formats/syft2/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden +++ b/sbom/internal/formats/syft2/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden @@ -78,7 +78,7 @@ }, "metadata": { "mode": 775, - "type": "directory", + "type": "Directory", "userID": 0, "groupID": 0, "mimeType": "" @@ -91,7 +91,7 @@ }, "metadata": { "mode": 775, - "type": "regularFile", + "type": "RegularFile", "userID": 0, "groupID": 0, "mimeType": "" @@ -111,7 +111,7 @@ }, "metadata": { "mode": 775, - "type": "symbolicLink", + "type": "SymbolicLink", "linkDestination": "/c", "userID": 0, "groupID": 0, @@ -125,7 +125,7 @@ }, "metadata": { "mode": 644, - "type": "regularFile", + "type": "RegularFile", "userID": 1, "groupID": 2, "mimeType": "" diff --git a/sbom/internal/formats/syft2/to_format_model.go b/sbom/internal/formats/syft2/to_format_model.go index b48da3f3..eeeca129 100644 --- a/sbom/internal/formats/syft2/to_format_model.go +++ b/sbom/internal/formats/syft2/to_format_model.go @@ -5,16 +5,14 @@ import ( "sort" "strconv" + stereoscopeFile "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" - "github.com/anchore/syft/syft/linux" - - "github.com/anchore/syft/syft/artifact" - - "github.com/anchore/syft/syft/sbom" - "github.com/anchore/syft/syft/formats/syftjson/model" + "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" internalmodel "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/syft2/model" syft2source "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/syft2/source" @@ -117,7 +115,7 @@ func toFileMetadataEntry(coordinates source.Coordinates, metadata *source.FileMe return &model.FileMetadataEntry{ Mode: mode, - Type: metadata.Type, + Type: toFileType(metadata.Type), LinkDestination: metadata.LinkDestination, UserID: metadata.UserID, GroupID: metadata.GroupID, @@ -125,6 +123,31 @@ func toFileMetadataEntry(coordinates source.Coordinates, metadata *source.FileMe } } +func toFileType(ty stereoscopeFile.Type) string { + switch ty { + case stereoscopeFile.TypeSymLink: + return "SymbolicLink" + case stereoscopeFile.TypeHardLink: + return "HardLink" + case stereoscopeFile.TypeDirectory: + return "Directory" + case stereoscopeFile.TypeSocket: + return "Socket" + case stereoscopeFile.TypeBlockDevice: + return "BlockDevice" + case stereoscopeFile.TypeCharacterDevice: + return "CharacterDevice" + case stereoscopeFile.TypeFIFO: + return "FIFONode" + case stereoscopeFile.TypeRegular: + return "RegularFile" + case stereoscopeFile.TypeIrregular: + return "IrregularFile" + default: + return "Unknown" + } +} + func toPackageModels(catalog *pkg.Catalog) []internalmodel.Package { artifacts := make([]internalmodel.Package, 0) if catalog == nil { diff --git a/sbom/internal/formats/syft301/encoder_test.go b/sbom/internal/formats/syft301/encoder_test.go index f859a193..261aeca6 100644 --- a/sbom/internal/formats/syft301/encoder_test.go +++ b/sbom/internal/formats/syft301/encoder_test.go @@ -4,17 +4,15 @@ import ( "flag" "testing" + stereoFile "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" - - "github.com/anchore/syft/syft/artifact" - "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" - - "github.com/anchore/syft/syft/formats/common/testutils" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/common/testutils" ) var updateJson = flag.Bool("update-json", false, "update the *.golden files for json encoders") @@ -101,26 +99,26 @@ func TestEncodeFullJSONDocument(t *testing.T) { FileMetadata: map[source.Coordinates]source.FileMetadata{ source.NewLocation("/a/place").Coordinates: { Mode: 0775, - Type: "directory", + Type: stereoFile.TypeDirectory, UserID: 0, GroupID: 0, }, source.NewLocation("/a/place/a").Coordinates: { Mode: 0775, - Type: "regularFile", + Type: stereoFile.TypeRegular, UserID: 0, GroupID: 0, }, source.NewLocation("/b").Coordinates: { Mode: 0775, - Type: "symbolicLink", + Type: stereoFile.TypeSymLink, LinkDestination: "/c", UserID: 0, GroupID: 0, }, source.NewLocation("/b/place/b").Coordinates: { Mode: 0644, - Type: "regularFile", + Type: stereoFile.TypeRegular, UserID: 1, GroupID: 2, }, diff --git a/sbom/internal/formats/syft301/format.go b/sbom/internal/formats/syft301/format.go index 7b7ce8bd..6b3ef02b 100644 --- a/sbom/internal/formats/syft301/format.go +++ b/sbom/internal/formats/syft301/format.go @@ -9,21 +9,12 @@ import ( const ID sbom.FormatID = "syft-3.0.1-json" const JSONSchemaVersion string = "3.0.1" -// Decoder not implemented because it's not needed for buildpacks' SBOM generation -var dummyDecoder func(io.Reader) (*sbom.SBOM, error) = func(input io.Reader) (*sbom.SBOM, error) { - return nil, nil -} - -// Validator not implemented because it's not needed for buildpacks' SBOM generation -var dummyValidator func(io.Reader) error = func(input io.Reader) error { - return nil -} - func Format() sbom.Format { return sbom.NewFormat( - ID, + "3.0.1", encoder, - dummyDecoder, - dummyValidator, + func(input io.Reader) (*sbom.SBOM, error) { return nil, nil }, + func(input io.Reader) error { return nil }, + ID, ) } diff --git a/sbom/internal/formats/syft301/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden b/sbom/internal/formats/syft301/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden index e687e437..c1f898d7 100644 --- a/sbom/internal/formats/syft301/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden +++ b/sbom/internal/formats/syft301/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden @@ -78,7 +78,7 @@ }, "metadata": { "mode": 775, - "type": "directory", + "type": "Directory", "userID": 0, "groupID": 0, "mimeType": "" @@ -91,7 +91,7 @@ }, "metadata": { "mode": 775, - "type": "regularFile", + "type": "RegularFile", "userID": 0, "groupID": 0, "mimeType": "" @@ -111,7 +111,7 @@ }, "metadata": { "mode": 775, - "type": "symbolicLink", + "type": "SymbolicLink", "linkDestination": "/c", "userID": 0, "groupID": 0, @@ -125,7 +125,7 @@ }, "metadata": { "mode": 644, - "type": "regularFile", + "type": "RegularFile", "userID": 1, "groupID": 2, "mimeType": "" diff --git a/sbom/internal/formats/syft301/to_format_model.go b/sbom/internal/formats/syft301/to_format_model.go index 5b5ae8f5..d8e8a5fa 100644 --- a/sbom/internal/formats/syft301/to_format_model.go +++ b/sbom/internal/formats/syft301/to_format_model.go @@ -5,21 +5,16 @@ import ( "sort" "strconv" + stereoscopeFile "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" - "github.com/anchore/syft/syft/linux" - "github.com/anchore/syft/syft/file" - - "github.com/anchore/syft/syft/artifact" - - "github.com/anchore/syft/syft/sbom" - - internalmodel "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/syft301/model" - "github.com/anchore/syft/syft/formats/syftjson/model" - // "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" + internalmodel "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/syft301/model" ) // ToFormatModel transforms the sbom import a format-specific model. @@ -141,7 +136,7 @@ func toFileMetadataEntry(coordinates source.Coordinates, metadata *source.FileMe return &model.FileMetadataEntry{ Mode: mode, - Type: metadata.Type, + Type: toFileType(metadata.Type), LinkDestination: metadata.LinkDestination, UserID: metadata.UserID, GroupID: metadata.GroupID, @@ -149,6 +144,31 @@ func toFileMetadataEntry(coordinates source.Coordinates, metadata *source.FileMe } } +func toFileType(ty stereoscopeFile.Type) string { + switch ty { + case stereoscopeFile.TypeSymLink: + return "SymbolicLink" + case stereoscopeFile.TypeHardLink: + return "HardLink" + case stereoscopeFile.TypeDirectory: + return "Directory" + case stereoscopeFile.TypeSocket: + return "Socket" + case stereoscopeFile.TypeBlockDevice: + return "BlockDevice" + case stereoscopeFile.TypeCharacterDevice: + return "CharacterDevice" + case stereoscopeFile.TypeFIFO: + return "FIFONode" + case stereoscopeFile.TypeRegular: + return "RegularFile" + case stereoscopeFile.TypeIrregular: + return "IrregularFile" + default: + return "Unknown" + } +} + func toPackageModels(catalog *pkg.Catalog) []model.Package { artifacts := make([]model.Package, 0) if catalog == nil { diff --git a/sbom/sbom_format.go b/sbom/sbom_format.go index cb5ee5f7..ccc3b97e 100644 --- a/sbom/sbom_format.go +++ b/sbom/sbom_format.go @@ -57,7 +57,7 @@ func (f sbomFormat) Extension() string { switch f.ID() { case syft.CycloneDxJSONFormatID, cyclonedx13.ID: return "cdx.json" - case syft.SPDXJSONFormatID: + case syft.SPDXJSONFormatID, spdx22.ID: return "spdx.json" case syft.JSONFormatID, syft2.ID, syft301.ID: return "syft.json"