diff --git a/cmd/packages.go b/cmd/packages.go index 25eef4ed3..a1cb71e82 100644 --- a/cmd/packages.go +++ b/cmd/packages.go @@ -283,7 +283,7 @@ func packagesExecWorker(userInput string) <-chan error { bus.Publish(partybus.Event{ Type: event.PresenterReady, - Value: f.Presenter(s), + Value: f.Presenter(s, appConfig), }) }() return errs diff --git a/cmd/power_user.go b/cmd/power_user.go index 401898942..c924f361d 100644 --- a/cmd/power_user.go +++ b/cmd/power_user.go @@ -4,6 +4,8 @@ import ( "fmt" "os" + "github.com/anchore/syft/internal/formats/syftjson" + "github.com/anchore/syft/syft/artifact" "github.com/gookit/color" @@ -13,7 +15,6 @@ import ( "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/bus" "github.com/anchore/syft/internal/log" - "github.com/anchore/syft/internal/presenter/poweruser" "github.com/anchore/syft/internal/ui" "github.com/anchore/syft/syft/event" "github.com/anchore/syft/syft/source" @@ -139,7 +140,7 @@ func powerUserExecWorker(userInput string) <-chan error { bus.Publish(partybus.Event{ Type: event.PresenterReady, - Value: poweruser.NewJSONPresenter(s, *appConfig), + Value: syftjson.Format().Presenter(s, *appConfig), }) }() diff --git a/internal/anchore/import_package_sbom.go b/internal/anchore/import_package_sbom.go index cafdf72c1..e4d9f3159 100644 --- a/internal/anchore/import_package_sbom.go +++ b/internal/anchore/import_package_sbom.go @@ -25,7 +25,7 @@ type packageSBOMImportAPI interface { func packageSbomModel(s sbom.SBOM) (*external.ImagePackageManifest, error) { var buf bytes.Buffer - err := syftjson.Format().Presenter(s).Present(&buf) + err := syftjson.Format().Presenter(s, nil).Present(&buf) if err != nil { return nil, fmt.Errorf("unable to serialize results: %w", err) } diff --git a/internal/anchore/import_package_sbom_test.go b/internal/anchore/import_package_sbom_test.go index 18af116a6..b5ed5bd66 100644 --- a/internal/anchore/import_package_sbom_test.go +++ b/internal/anchore/import_package_sbom_test.go @@ -105,7 +105,7 @@ func TestPackageSbomToModel(t *testing.T) { } var buf bytes.Buffer - pres := syftjson.Format().Presenter(s) + pres := syftjson.Format().Presenter(s, nil) if err := pres.Present(&buf); err != nil { t.Fatalf("unable to get expected json: %+v", err) } diff --git a/internal/config/application.go b/internal/config/application.go index 949978e82..cf844a9d5 100644 --- a/internal/config/application.go +++ b/internal/config/application.go @@ -163,6 +163,10 @@ func (cfg *Application) parseLogLevelOption() error { } } + if cfg.Log.Level == "" { + cfg.Log.Level = cfg.Log.LevelOpt.String() + } + return nil } diff --git a/internal/formats/cyclonedx12xml/encoder.go b/internal/formats/cyclonedx12xml/encoder.go index a371ce052..90dd72590 100644 --- a/internal/formats/cyclonedx12xml/encoder.go +++ b/internal/formats/cyclonedx12xml/encoder.go @@ -7,7 +7,7 @@ import ( "github.com/anchore/syft/syft/sbom" ) -func encoder(output io.Writer, s sbom.SBOM) error { +func encoder(output io.Writer, s sbom.SBOM, _ interface{}) error { enc := xml.NewEncoder(output) enc.Indent("", " ") diff --git a/internal/formats/cyclonedx12xml/encoder_test.go b/internal/formats/cyclonedx12xml/encoder_test.go index 5481208f1..5d07badad 100644 --- a/internal/formats/cyclonedx12xml/encoder_test.go +++ b/internal/formats/cyclonedx12xml/encoder_test.go @@ -12,7 +12,7 @@ var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden func TestCycloneDxDirectoryPresenter(t *testing.T) { testutils.AssertPresenterAgainstGoldenSnapshot(t, - Format().Presenter(testutils.DirectoryInput(t)), + Format().Presenter(testutils.DirectoryInput(t), nil), *updateCycloneDx, cycloneDxRedactor, ) @@ -21,7 +21,7 @@ func TestCycloneDxDirectoryPresenter(t *testing.T) { func TestCycloneDxImagePresenter(t *testing.T) { testImage := "image-simple" testutils.AssertPresenterAgainstGoldenImageSnapshot(t, - Format().Presenter(testutils.ImageInput(t, testImage)), + Format().Presenter(testutils.ImageInput(t, testImage), nil), testImage, *updateCycloneDx, cycloneDxRedactor, diff --git a/internal/formats/spdx22json/encoder.go b/internal/formats/spdx22json/encoder.go index 8159fe130..e02359645 100644 --- a/internal/formats/spdx22json/encoder.go +++ b/internal/formats/spdx22json/encoder.go @@ -9,7 +9,7 @@ import ( const anchoreNamespace = "https://anchore.com/syft" -func encoder(output io.Writer, s sbom.SBOM) error { +func encoder(output io.Writer, s sbom.SBOM, _ interface{}) error { doc := toFormatModel(s) enc := json.NewEncoder(output) diff --git a/internal/formats/spdx22json/encoder_test.go b/internal/formats/spdx22json/encoder_test.go index 59bb5eccd..f3f70ed81 100644 --- a/internal/formats/spdx22json/encoder_test.go +++ b/internal/formats/spdx22json/encoder_test.go @@ -12,7 +12,7 @@ var updateSpdxJson = flag.Bool("update-spdx-json", false, "update the *.golden f func TestSPDXJSONDirectoryPresenter(t *testing.T) { testutils.AssertPresenterAgainstGoldenSnapshot(t, - Format().Presenter(testutils.DirectoryInput(t)), + Format().Presenter(testutils.DirectoryInput(t), nil), *updateSpdxJson, spdxJsonRedactor, ) @@ -21,7 +21,7 @@ func TestSPDXJSONDirectoryPresenter(t *testing.T) { func TestSPDXJSONImagePresenter(t *testing.T) { testImage := "image-simple" testutils.AssertPresenterAgainstGoldenImageSnapshot(t, - Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())), + Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot()), nil), testImage, *updateSpdxJson, spdxJsonRedactor, diff --git a/internal/formats/spdx22json/test-fixtures/snapshot/TestSPDXJSONDirectoryPresenter.golden b/internal/formats/spdx22json/test-fixtures/snapshot/TestSPDXJSONDirectoryPresenter.golden index ac87f0f22..8469bc2e1 100644 --- a/internal/formats/spdx22json/test-fixtures/snapshot/TestSPDXJSONDirectoryPresenter.golden +++ b/internal/formats/spdx22json/test-fixtures/snapshot/TestSPDXJSONDirectoryPresenter.golden @@ -3,18 +3,18 @@ "name": "/some/path", "spdxVersion": "SPDX-2.2", "creationInfo": { - "created": "2021-10-29T16:26:08.995826Z", + "created": "2021-11-17T19:35:54.834877Z", "creators": [ "Organization: Anchore, Inc", "Tool: syft-[not provided]" ], - "licenseListVersion": "3.14" + "licenseListVersion": "3.15" }, "dataLicense": "CC0-1.0", - "documentNamespace": "https:/anchore.com/syft/dir/some/path-5362d380-914a-458f-b059-d8d27899574c", + "documentNamespace": "https:/anchore.com/syft/dir/some/path-65e2226e-a61e-4ed1-81bb-56022e1ff1eb", "packages": [ { - "SPDXID": "SPDXRef-Package-python-package-1-1.0.1", + "SPDXID": "SPDXRef-2a115ac97d018a0e", "name": "package-1", "licenseConcluded": "MIT", "downloadLocation": "NOASSERTION", @@ -31,15 +31,12 @@ } ], "filesAnalyzed": false, - "hasFiles": [ - "SPDXRef-File-package-1-efae7fecc76ca25da40f79d7ef5b8933510434914835832c7976f3e866aa756a" - ], "licenseDeclared": "MIT", "sourceInfo": "acquired package info from installed python package manifest file: /some/path/pkg1", "versionInfo": "1.0.1" }, { - "SPDXID": "SPDXRef-Package-deb-package-2-2.0.1", + "SPDXID": "SPDXRef-5e920b2bece2c3ae", "name": "package-2", "licenseConcluded": "NONE", "downloadLocation": "NOASSERTION", @@ -60,20 +57,5 @@ "sourceInfo": "acquired package info from DPKG DB: /some/path/pkg1", "versionInfo": "2.0.1" } - ], - "files": [ - { - "SPDXID": "SPDXRef-File-package-1-efae7fecc76ca25da40f79d7ef5b8933510434914835832c7976f3e866aa756a", - "name": "foo", - "licenseConcluded": "", - "fileName": "/some/path/pkg1/dependencies/foo" - } - ], - "relationships": [ - { - "spdxElementId": "SPDXRef-Package-python-package-1-1.0.1", - "relationshipType": "CONTAINS", - "relatedSpdxElement": "SPDXRef-File-package-1-efae7fecc76ca25da40f79d7ef5b8933510434914835832c7976f3e866aa756a" - } ] } diff --git a/internal/formats/spdx22json/test-fixtures/snapshot/TestSPDXJSONImagePresenter.golden b/internal/formats/spdx22json/test-fixtures/snapshot/TestSPDXJSONImagePresenter.golden index 07efff023..a52c91fac 100644 --- a/internal/formats/spdx22json/test-fixtures/snapshot/TestSPDXJSONImagePresenter.golden +++ b/internal/formats/spdx22json/test-fixtures/snapshot/TestSPDXJSONImagePresenter.golden @@ -3,18 +3,18 @@ "name": "user-image-input", "spdxVersion": "SPDX-2.2", "creationInfo": { - "created": "2021-10-29T16:26:09.001799Z", + "created": "2021-11-17T19:35:57.761372Z", "creators": [ "Organization: Anchore, Inc", "Tool: syft-[not provided]" ], - "licenseListVersion": "3.14" + "licenseListVersion": "3.15" }, "dataLicense": "CC0-1.0", - "documentNamespace": "https:/anchore.com/syft/image/user-image-input-3ad8571c-513f-4fce-944e-5125353c3186", + "documentNamespace": "https:/anchore.com/syft/image/user-image-input-5383918f-ec96-4aa9-b756-ad16e1ada31e", "packages": [ { - "SPDXID": "SPDXRef-Package-python-package-1-1.0.1", + "SPDXID": "SPDXRef-888661d4f0362f02", "name": "package-1", "licenseConcluded": "MIT", "downloadLocation": "NOASSERTION", @@ -36,7 +36,7 @@ "versionInfo": "1.0.1" }, { - "SPDXID": "SPDXRef-Package-deb-package-2-2.0.1", + "SPDXID": "SPDXRef-4068ff5e8926b305", "name": "package-2", "licenseConcluded": "NONE", "downloadLocation": "NOASSERTION", diff --git a/internal/formats/spdx22tagvalue/encoder.go b/internal/formats/spdx22tagvalue/encoder.go index b529a6859..68c9f3e62 100644 --- a/internal/formats/spdx22tagvalue/encoder.go +++ b/internal/formats/spdx22tagvalue/encoder.go @@ -7,7 +7,7 @@ import ( "github.com/spdx/tools-golang/tvsaver" ) -func encoder(output io.Writer, s sbom.SBOM) error { +func encoder(output io.Writer, s sbom.SBOM, _ interface{}) error { model := toFormatModel(s) return tvsaver.Save2_2(&model, output) } diff --git a/internal/formats/spdx22tagvalue/encoder_test.go b/internal/formats/spdx22tagvalue/encoder_test.go index fa6b4ceff..4ab25af9a 100644 --- a/internal/formats/spdx22tagvalue/encoder_test.go +++ b/internal/formats/spdx22tagvalue/encoder_test.go @@ -13,7 +13,7 @@ var updateSpdxTagValue = flag.Bool("update-spdx-tv", false, "update the *.golden func TestSPDXTagValueDirectoryPresenter(t *testing.T) { testutils.AssertPresenterAgainstGoldenSnapshot(t, - Format().Presenter(testutils.DirectoryInput(t)), + Format().Presenter(testutils.DirectoryInput(t), nil), *updateSpdxTagValue, spdxTagValueRedactor, ) @@ -22,7 +22,7 @@ func TestSPDXTagValueDirectoryPresenter(t *testing.T) { func TestSPDXTagValueImagePresenter(t *testing.T) { testImage := "image-simple" testutils.AssertPresenterAgainstGoldenImageSnapshot(t, - Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())), + Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot()), nil), testImage, *updateSpdxTagValue, spdxTagValueRedactor, diff --git a/internal/formats/syftjson/decoder_test.go b/internal/formats/syftjson/decoder_test.go index ea69ee2c7..3f383a75b 100644 --- a/internal/formats/syftjson/decoder_test.go +++ b/internal/formats/syftjson/decoder_test.go @@ -15,7 +15,7 @@ func TestEncodeDecodeCycle(t *testing.T) { originalSBOM := testutils.ImageInput(t, testImage) var buf bytes.Buffer - assert.NoError(t, encoder(&buf, originalSBOM)) + assert.NoError(t, encoder(&buf, originalSBOM, map[string]string{"config": "value"})) actualSBOM, err := decoder(bytes.NewReader(buf.Bytes())) assert.NoError(t, err) diff --git a/internal/formats/syftjson/encoder.go b/internal/formats/syftjson/encoder.go index 8e3141a61..d6c287494 100644 --- a/internal/formats/syftjson/encoder.go +++ b/internal/formats/syftjson/encoder.go @@ -7,9 +7,9 @@ import ( "github.com/anchore/syft/syft/sbom" ) -func encoder(output io.Writer, s sbom.SBOM) error { +func encoder(output io.Writer, s sbom.SBOM, appConfig interface{}) error { // TODO: application config not available yet - doc := ToFormatModel(s, nil) + doc := ToFormatModel(s, appConfig) enc := json.NewEncoder(output) // prevent > and < from being escaped in the payload diff --git a/internal/formats/syftjson/encoder_test.go b/internal/formats/syftjson/encoder_test.go index 3fc05c822..9dc4be48b 100644 --- a/internal/formats/syftjson/encoder_test.go +++ b/internal/formats/syftjson/encoder_test.go @@ -4,6 +4,14 @@ import ( "flag" "testing" + "github.com/anchore/syft/syft/artifact" + + "github.com/anchore/syft/syft/distro" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" + "github.com/anchore/syft/internal/formats/common/testutils" ) @@ -11,7 +19,7 @@ var updateJson = flag.Bool("update-json", false, "update the *.golden files for func TestDirectoryPresenter(t *testing.T) { testutils.AssertPresenterAgainstGoldenSnapshot(t, - Format().Presenter(testutils.DirectoryInput(t)), + Format().Presenter(testutils.DirectoryInput(t), nil), *updateJson, ) } @@ -19,8 +27,165 @@ func TestDirectoryPresenter(t *testing.T) { func TestImagePresenter(t *testing.T) { testImage := "image-simple" testutils.AssertPresenterAgainstGoldenImageSnapshot(t, - Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())), + Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot()), nil), testImage, *updateJson, ) } + +func TestFullJSONDocument(t *testing.T) { + catalog := pkg.NewCatalog() + + p1 := pkg.Package{ + Name: "package-1", + Version: "1.0.1", + Locations: []source.Location{ + { + Coordinates: source.Coordinates{ + RealPath: "/a/place/a", + }, + }, + }, + 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", + Files: []pkg.PythonFileRecord{}, + }, + PURL: "a-purl-1", + CPEs: []pkg.CPE{ + pkg.MustCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"), + }, + } + + p2 := pkg.Package{ + Name: "package-2", + Version: "2.0.1", + Locations: []source.Location{ + { + Coordinates: source.Coordinates{ + RealPath: "/b/place/b", + }, + }, + }, + Type: pkg.DebPkg, + FoundBy: "the-cataloger-2", + MetadataType: pkg.DpkgMetadataType, + Metadata: pkg.DpkgMetadata{ + Package: "package-2", + Version: "2.0.1", + Files: []pkg.DpkgFileRecord{}, + }, + PURL: "a-purl-2", + CPEs: []pkg.CPE{ + pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + }, + } + + catalog.Add(p1) + catalog.Add(p2) + + s := sbom.SBOM{ + Artifacts: sbom.Artifacts{ + PackageCatalog: catalog, + FileMetadata: map[source.Coordinates]source.FileMetadata{ + source.NewLocation("/a/place").Coordinates: { + Mode: 0775, + Type: "directory", + UserID: 0, + GroupID: 0, + }, + source.NewLocation("/a/place/a").Coordinates: { + Mode: 0775, + Type: "regularFile", + UserID: 0, + GroupID: 0, + }, + source.NewLocation("/b").Coordinates: { + Mode: 0775, + Type: "symbolicLink", + LinkDestination: "/c", + UserID: 0, + GroupID: 0, + }, + source.NewLocation("/b/place/b").Coordinates: { + Mode: 0644, + Type: "regularFile", + UserID: 1, + GroupID: 2, + }, + }, + FileDigests: map[source.Coordinates][]file.Digest{ + source.NewLocation("/a/place/a").Coordinates: { + { + Algorithm: "sha256", + Value: "366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703", + }, + }, + source.NewLocation("/b/place/b").Coordinates: { + { + Algorithm: "sha256", + Value: "1b3722da2a7d90d033b87581a2a3f12021647445653e34666ef041e3b4f3707c", + }, + }, + }, + FileContents: map[source.Coordinates]string{ + source.NewLocation("/a/place/a").Coordinates: "the-contents", + }, + Distro: &distro.Distro{ + Type: distro.RedHat, + RawVersion: "7", + IDLike: "rhel", + }, + }, + Relationships: []artifact.Relationship{ + { + From: p1, + To: p2, + Type: artifact.OwnershipByFileOverlapRelationship, + Data: map[string]string{ + "file": "path", + }, + }, + }, + Source: source.Metadata{ + Scheme: source.ImageScheme, + ImageMetadata: source.ImageMetadata{ + UserInput: "user-image-input", + ID: "sha256:c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0", + ManifestDigest: "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368", + MediaType: "application/vnd.docker.distribution.manifest.v2+json", + Tags: []string{ + "stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b", + }, + Size: 38, + Layers: []source.LayerMetadata{ + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:3de16c5b8659a2e8d888b8ded8427be7a5686a3c8c4e4dd30de20f362827285b", + Size: 22, + }, + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703", + Size: 16, + }, + }, + RawManifest: []byte("eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJh..."), + RawConfig: []byte("eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZp..."), + RepoDigests: []string{}, + }, + }, + } + + testutils.AssertPresenterAgainstGoldenSnapshot(t, + Format().Presenter(s, map[string]string{ + "app": "config", + }), + *updateJson, + ) +} diff --git a/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryPresenter.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryPresenter.golden index 87546b861..823f0282d 100644 --- a/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryPresenter.golden +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryPresenter.golden @@ -80,7 +80,7 @@ "version": "[not provided]" }, "schema": { - "version": "1.1.0", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json" + "version": "2.0.0", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.0.json" } } diff --git a/internal/presenter/poweruser/test-fixtures/snapshot/TestJSONPresenter.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestFullJSONDocument.golden similarity index 66% rename from internal/presenter/poweruser/test-fixtures/snapshot/TestJSONPresenter.golden rename to internal/formats/syftjson/test-fixtures/snapshot/TestFullJSONDocument.golden index 0e887a1de..bb45ed06e 100644 --- a/internal/presenter/poweruser/test-fixtures/snapshot/TestJSONPresenter.golden +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestFullJSONDocument.golden @@ -1,75 +1,4 @@ { - "fileContents": [ - { - "location": { - "path": "/a/place/a" - }, - "contents": "the-contents" - } - ], - "fileMetadata": [ - { - "location": { - "path": "/a/place" - }, - "metadata": { - "mode": 775, - "type": "directory", - "userID": 0, - "groupID": 0, - "mimeType": "" - } - }, - { - "location": { - "path": "/a/place/a" - }, - "metadata": { - "mode": 775, - "type": "regularFile", - "userID": 0, - "groupID": 0, - "digests": [ - { - "algorithm": "sha256", - "value": "366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703" - } - ], - "mimeType": "" - } - }, - { - "location": { - "path": "/b" - }, - "metadata": { - "mode": 775, - "type": "symbolicLink", - "linkDestination": "/c", - "userID": 0, - "groupID": 0, - "mimeType": "" - } - }, - { - "location": { - "path": "/b/place/b" - }, - "metadata": { - "mode": 644, - "type": "regularFile", - "userID": 1, - "groupID": 2, - "digests": [ - { - "algorithm": "sha256", - "value": "1b3722da2a7d90d033b87581a2a3f12021647445653e34666ef041e3b4f3707c" - } - ], - "mimeType": "" - } - } - ], "artifacts": [ { "id": "962403cfb7be50d7", @@ -131,7 +60,84 @@ } } ], - "artifactRelationships": [], + "artifactRelationships": [ + { + "parent": "962403cfb7be50d7", + "child": "b11f44847bba0ed1", + "type": "ownership-by-file-overlap", + "metadata": { + "file": "path" + } + } + ], + "files": [ + { + "id": "913b4592e2c2ebdf", + "location": { + "path": "/a/place" + }, + "metadata": { + "mode": 775, + "type": "directory", + "userID": 0, + "groupID": 0, + "mimeType": "" + } + }, + { + "id": "e7c88bd18e11b0b", + "location": { + "path": "/a/place/a" + }, + "metadata": { + "mode": 775, + "type": "regularFile", + "userID": 0, + "groupID": 0, + "mimeType": "" + }, + "contents": "the-contents", + "digests": [ + { + "algorithm": "sha256", + "value": "366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703" + } + ] + }, + { + "id": "5c3dc6885f48b5a1", + "location": { + "path": "/b" + }, + "metadata": { + "mode": 775, + "type": "symbolicLink", + "linkDestination": "/c", + "userID": 0, + "groupID": 0, + "mimeType": "" + } + }, + { + "id": "799d2f12da0bcec4", + "location": { + "path": "/b/place/b" + }, + "metadata": { + "mode": 644, + "type": "regularFile", + "userID": 1, + "groupID": 2, + "mimeType": "" + }, + "digests": [ + { + "algorithm": "sha256", + "value": "1b3722da2a7d90d033b87581a2a3f12021647445653e34666ef041e3b4f3707c" + } + ] + } + ], "source": { "type": "image", "target": { @@ -169,75 +175,11 @@ "name": "syft", "version": "[not provided]", "configuration": { - "configPath": "", - "output": "", - "file": "", - "quiet": false, - "check-for-app-update": false, - "anchore": { - "host": "", - "path": "", - "dockerfile": "", - "overwrite-existing-image": false, - "import-timeout": 0 - }, - "dev": { - "profile-cpu": false, - "profile-mem": false - }, - "log": { - "structured": false, - "level": "", - "file-location": "" - }, - "package": { - "cataloger": { - "enabled": false, - "scope": "" - } - }, - "file-metadata": { - "cataloger": { - "enabled": false, - "scope": "" - }, - "digests": [ - "sha256" - ] - }, - "file-classification": { - "cataloger": { - "enabled": false, - "scope": "" - } - }, - "file-contents": { - "cataloger": { - "enabled": false, - "scope": "" - }, - "skip-files-above-size": 0, - "globs": null - }, - "secrets": { - "cataloger": { - "enabled": false, - "scope": "" - }, - "additional-patterns": null, - "exclude-pattern-names": null, - "reveal-values": false, - "skip-files-above-size": 0 - }, - "registry": { - "insecure-skip-tls-verify": false, - "insecure-use-http": false, - "auth": null - } + "app": "config" } }, "schema": { - "version": "1.1.0", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json" + "version": "2.0.0", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.0.json" } } diff --git a/internal/formats/syftjson/test-fixtures/snapshot/TestImagePresenter.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestImagePresenter.golden index 46808e6ce..a9957e70b 100644 --- a/internal/formats/syftjson/test-fixtures/snapshot/TestImagePresenter.golden +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestImagePresenter.golden @@ -101,7 +101,7 @@ "version": "[not provided]" }, "schema": { - "version": "1.1.0", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json" + "version": "2.0.0", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.0.json" } } diff --git a/internal/formats/table/encoder.go b/internal/formats/table/encoder.go index e651674c5..e1404b50b 100644 --- a/internal/formats/table/encoder.go +++ b/internal/formats/table/encoder.go @@ -11,7 +11,7 @@ import ( "github.com/olekukonko/tablewriter" ) -func encoder(output io.Writer, s sbom.SBOM) error { +func encoder(output io.Writer, s sbom.SBOM, _ interface{}) error { var rows [][]string columns := []string{"Name", "Version", "Type"} diff --git a/internal/formats/table/encoder_test.go b/internal/formats/table/encoder_test.go index 7bcd62b4e..f3ab877a3 100644 --- a/internal/formats/table/encoder_test.go +++ b/internal/formats/table/encoder_test.go @@ -12,7 +12,7 @@ var updateTableGoldenFiles = flag.Bool("update-table", false, "update the *.gold func TestTablePresenter(t *testing.T) { testutils.AssertPresenterAgainstGoldenSnapshot(t, - Format().Presenter(testutils.DirectoryInput(t)), + Format().Presenter(testutils.DirectoryInput(t), nil), *updateTableGoldenFiles, ) } diff --git a/internal/formats/text/encoder.go b/internal/formats/text/encoder.go index 5360a4101..1519b92cb 100644 --- a/internal/formats/text/encoder.go +++ b/internal/formats/text/encoder.go @@ -10,7 +10,7 @@ import ( "github.com/anchore/syft/syft/source" ) -func encoder(output io.Writer, s sbom.SBOM) error { +func encoder(output io.Writer, s sbom.SBOM, _ interface{}) error { // init the tabular writer w := new(tabwriter.Writer) w.Init(output, 0, 8, 0, '\t', tabwriter.AlignRight) diff --git a/internal/formats/text/encoder_test.go b/internal/formats/text/encoder_test.go index 3ae7e87e4..5f1986d1d 100644 --- a/internal/formats/text/encoder_test.go +++ b/internal/formats/text/encoder_test.go @@ -11,7 +11,7 @@ var updateTextPresenterGoldenFiles = flag.Bool("update-text", false, "update the func TestTextDirectoryPresenter(t *testing.T) { testutils.AssertPresenterAgainstGoldenSnapshot(t, - Format().Presenter(testutils.DirectoryInput(t)), + Format().Presenter(testutils.DirectoryInput(t), nil), *updateTextPresenterGoldenFiles, ) } @@ -19,7 +19,7 @@ func TestTextDirectoryPresenter(t *testing.T) { func TestTextImagePresenter(t *testing.T) { testImage := "image-simple" testutils.AssertPresenterAgainstGoldenImageSnapshot(t, - Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())), + Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot()), nil), testImage, *updateTextPresenterGoldenFiles, ) diff --git a/internal/presenter/poweruser/json_presenter.go b/internal/presenter/poweruser/json_presenter.go deleted file mode 100644 index 1bd5d4bcc..000000000 --- a/internal/presenter/poweruser/json_presenter.go +++ /dev/null @@ -1,34 +0,0 @@ -package poweruser - -import ( - "encoding/json" - "io" - - "github.com/anchore/syft/internal/formats/syftjson" - - "github.com/anchore/syft/syft/sbom" -) - -// JSONPresenter is a JSON presentation object for the syft results -type JSONPresenter struct { - sbom sbom.SBOM - config interface{} -} - -// NewJSONPresenter creates a new JSON presenter object for the given cataloging results. -func NewJSONPresenter(s sbom.SBOM, appConfig interface{}) *JSONPresenter { - return &JSONPresenter{ - sbom: s, - config: appConfig, - } -} - -// Present the PackageCatalog results to the given writer. -func (p *JSONPresenter) Present(output io.Writer) error { - doc := syftjson.ToFormatModel(p.sbom, p.config) - enc := json.NewEncoder(output) - // prevent > and < from being escaped in the payload - enc.SetEscapeHTML(false) - enc.SetIndent("", " ") - return enc.Encode(&doc) -} diff --git a/internal/presenter/poweruser/json_presenter_test.go b/internal/presenter/poweruser/json_presenter_test.go deleted file mode 100644 index 735e08cad..000000000 --- a/internal/presenter/poweruser/json_presenter_test.go +++ /dev/null @@ -1,189 +0,0 @@ -package poweruser - -import ( - "bytes" - "flag" - "testing" - - "github.com/anchore/syft/syft/sbom" - - "github.com/sergi/go-diff/diffmatchpatch" - - "github.com/anchore/syft/syft/file" - - "github.com/anchore/go-testutils" - "github.com/anchore/syft/internal/config" - "github.com/anchore/syft/syft/distro" - "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/source" -) - -var updateJSONGoldenFiles = flag.Bool("update-json", false, "update the *.golden files for json presenters") - -func must(c pkg.CPE, e error) pkg.CPE { - if e != nil { - panic(e) - } - return c -} - -func TestJSONPresenter(t *testing.T) { - var buffer bytes.Buffer - - catalog := pkg.NewCatalog() - - catalog.Add(pkg.Package{ - Name: "package-1", - Version: "1.0.1", - Locations: []source.Location{ - { - Coordinates: source.Coordinates{ - RealPath: "/a/place/a", - }, - }, - }, - 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", - Files: []pkg.PythonFileRecord{}, - }, - PURL: "a-purl-1", - CPEs: []pkg.CPE{ - must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")), - }, - }) - catalog.Add(pkg.Package{ - Name: "package-2", - Version: "2.0.1", - Locations: []source.Location{ - { - Coordinates: source.Coordinates{ - RealPath: "/b/place/b", - }, - }, - }, - Type: pkg.DebPkg, - FoundBy: "the-cataloger-2", - MetadataType: pkg.DpkgMetadataType, - Metadata: pkg.DpkgMetadata{ - Package: "package-2", - Version: "2.0.1", - Files: []pkg.DpkgFileRecord{}, - }, - PURL: "a-purl-2", - CPEs: []pkg.CPE{ - must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")), - }, - }) - - appConfig := config.Application{ - FileMetadata: config.FileMetadata{ - Digests: []string{"sha256"}, - }, - } - - cfg := sbom.SBOM{ - Artifacts: sbom.Artifacts{ - PackageCatalog: catalog, - FileMetadata: map[source.Coordinates]source.FileMetadata{ - source.NewLocation("/a/place").Coordinates: { - Mode: 0775, - Type: "directory", - UserID: 0, - GroupID: 0, - }, - source.NewLocation("/a/place/a").Coordinates: { - Mode: 0775, - Type: "regularFile", - UserID: 0, - GroupID: 0, - }, - source.NewLocation("/b").Coordinates: { - Mode: 0775, - Type: "symbolicLink", - LinkDestination: "/c", - UserID: 0, - GroupID: 0, - }, - source.NewLocation("/b/place/b").Coordinates: { - Mode: 0644, - Type: "regularFile", - UserID: 1, - GroupID: 2, - }, - }, - FileDigests: map[source.Coordinates][]file.Digest{ - source.NewLocation("/a/place/a").Coordinates: { - { - Algorithm: "sha256", - Value: "366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703", - }, - }, - source.NewLocation("/b/place/b").Coordinates: { - { - Algorithm: "sha256", - Value: "1b3722da2a7d90d033b87581a2a3f12021647445653e34666ef041e3b4f3707c", - }, - }, - }, - FileContents: map[source.Coordinates]string{ - source.NewLocation("/a/place/a").Coordinates: "the-contents", - }, - Distro: &distro.Distro{ - Type: distro.RedHat, - RawVersion: "7", - IDLike: "rhel", - }, - }, - Source: source.Metadata{ - Scheme: source.ImageScheme, - ImageMetadata: source.ImageMetadata{ - UserInput: "user-image-input", - ID: "sha256:c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0", - ManifestDigest: "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368", - MediaType: "application/vnd.docker.distribution.manifest.v2+json", - Tags: []string{ - "stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b", - }, - Size: 38, - Layers: []source.LayerMetadata{ - { - MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", - Digest: "sha256:3de16c5b8659a2e8d888b8ded8427be7a5686a3c8c4e4dd30de20f362827285b", - Size: 22, - }, - { - MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", - Digest: "sha256:366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703", - Size: 16, - }, - }, - RawManifest: []byte("eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJh..."), - RawConfig: []byte("eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZp..."), - RepoDigests: []string{}, - }, - }, - } - - if err := NewJSONPresenter(cfg, appConfig).Present(&buffer); err != nil { - t.Fatal(err) - } - actual := buffer.Bytes() - - if *updateJSONGoldenFiles { - testutils.UpdateGoldenFileContents(t, actual) - } - - expected := testutils.GetGoldenFileContents(t) - - if !bytes.Equal(expected, actual) { - dmp := diffmatchpatch.New() - diffs := dmp.DiffMain(string(expected), string(actual), true) - t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs)) - } -} diff --git a/syft/encode_decode.go b/syft/encode_decode.go index 3d996a999..c28207c91 100644 --- a/syft/encode_decode.go +++ b/syft/encode_decode.go @@ -12,14 +12,14 @@ import ( ) // Encode takes all SBOM elements and a format option and encodes an SBOM document. -func Encode(s sbom.SBOM, option format.Option) ([]byte, error) { +func Encode(s sbom.SBOM, appConfig interface{}, option format.Option) ([]byte, error) { f := formats.ByOption(option) if f == nil { return nil, fmt.Errorf("unsupported format: %+v", option) } buff := bytes.Buffer{} - if err := f.Encode(&buff, s); err != nil { + if err := f.Encode(&buff, s, appConfig); err != nil { return nil, fmt.Errorf("unable to encode sbom: %w", err) } diff --git a/syft/format/encoder.go b/syft/format/encoder.go index 9c1c874de..3c5ed3c26 100644 --- a/syft/format/encoder.go +++ b/syft/format/encoder.go @@ -7,4 +7,4 @@ import ( ) // Encoder is a function that can transform Syft native objects into an SBOM document of a specific format written to the given writer. -type Encoder func(io.Writer, sbom.SBOM) error +type Encoder func(io.Writer, sbom.SBOM, interface{}) error diff --git a/syft/format/format.go b/syft/format/format.go index a7dacb15a..65bf18288 100644 --- a/syft/format/format.go +++ b/syft/format/format.go @@ -29,11 +29,11 @@ func NewFormat(option Option, encoder Encoder, decoder Decoder, validator Valida } } -func (f Format) Encode(output io.Writer, s sbom.SBOM) error { +func (f Format) Encode(output io.Writer, s sbom.SBOM, appConfig interface{}) error { if f.encoder == nil { return ErrEncodingNotSupported } - return f.encoder(output, s) + return f.encoder(output, s, appConfig) } func (f Format) Decode(reader io.Reader) (*sbom.SBOM, error) { @@ -51,9 +51,9 @@ func (f Format) Validate(reader io.Reader) error { return f.validator(reader) } -func (f Format) Presenter(s sbom.SBOM) *Presenter { +func (f Format) Presenter(s sbom.SBOM, appConfig interface{}) *Presenter { if f.encoder == nil { return nil } - return NewPresenter(f.encoder, s) + return NewPresenter(f.encoder, s, appConfig) } diff --git a/syft/format/presenter.go b/syft/format/presenter.go index 1ec018991..fc920c8d8 100644 --- a/syft/format/presenter.go +++ b/syft/format/presenter.go @@ -7,17 +7,19 @@ import ( ) type Presenter struct { - sbom sbom.SBOM - encoder Encoder + sbom sbom.SBOM + appConfig interface{} + encoder Encoder } -func NewPresenter(encoder Encoder, s sbom.SBOM) *Presenter { +func NewPresenter(encoder Encoder, s sbom.SBOM, appConfig interface{}) *Presenter { return &Presenter{ - sbom: s, - encoder: encoder, + sbom: s, + appConfig: appConfig, + encoder: encoder, } } func (pres *Presenter) Present(output io.Writer) error { - return pres.encoder(output, pres.sbom) + return pres.encoder(output, pres.sbom, pres.appConfig) } diff --git a/syft/pkg/cataloger/catalog.go b/syft/pkg/cataloger/catalog.go index 93678370b..9d7c1da65 100644 --- a/syft/pkg/cataloger/catalog.go +++ b/syft/pkg/cataloger/catalog.go @@ -1,6 +1,8 @@ package cataloger import ( + "fmt" + "github.com/anchore/syft/internal/bus" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" @@ -43,6 +45,7 @@ func Catalog(resolver source.FileResolver, theDistro *distro.Distro, catalogers catalog := pkg.NewCatalog() var allRelationships []artifact.Relationship + // TODO: update to show relationships filesProcessed, packagesDiscovered := newMonitor() // perform analysis, accumulating errors for each failed analysis @@ -57,6 +60,7 @@ func Catalog(resolver source.FileResolver, theDistro *distro.Distro, catalogers catalogedPackages := len(packages) + // TODO: update to show relationships and files log.Debugf("package cataloger %q discovered %d packages", theCataloger.Name(), catalogedPackages) packagesDiscovered.N += int64(catalogedPackages) @@ -67,6 +71,15 @@ func Catalog(resolver source.FileResolver, theDistro *distro.Distro, catalogers // generate PURL p.PURL = generatePackageURL(p, theDistro) + // TODO: break out into another function (refactor this function) + // create file-to-package relationships for files owned by the package + owningRelationships, err := packageFileOwnershipRelationships(p, resolver) + if err != nil { + log.Warnf("unable to create any package-file relationships for package name=%q: %w", p.Name, err) + } else { + allRelationships = append(allRelationships, owningRelationships...) + } + // add to catalog catalog.Add(p) } @@ -85,3 +98,33 @@ func Catalog(resolver source.FileResolver, theDistro *distro.Distro, catalogers return catalog, allRelationships, nil } + +func packageFileOwnershipRelationships(p pkg.Package, resolver source.FilePathResolver) ([]artifact.Relationship, error) { + fileOwner, ok := p.Metadata.(pkg.FileOwner) + if !ok { + return nil, nil + } + + var relationships []artifact.Relationship + + for _, path := range fileOwner.OwnedFiles() { + locations, err := resolver.FilesByPath(path) + if err != nil { + return nil, fmt.Errorf("unable to find path for path=%q: %w", path, err) + } + + //if len(locations) == 0 { + // // TODO: this is notable, we should at least log it(?)... however, ideally there is something in the SBOM about this + //} + + for _, l := range locations { + relationships = append(relationships, artifact.Relationship{ + From: l.Coordinates, + To: p, + Type: artifact.PackageOfRelationship, + }) + } + } + + return relationships, nil +} diff --git a/syft/source/coordinates_test.go b/syft/source/coordinates_test.go new file mode 100644 index 000000000..e9f8a4a30 --- /dev/null +++ b/syft/source/coordinates_test.go @@ -0,0 +1,51 @@ +package source + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCoordinateSet(t *testing.T) { + + binA := Coordinates{ + RealPath: "/bin", + FileSystemID: "a", + } + + binB := Coordinates{ + RealPath: "/bin", + FileSystemID: "b", + } + + tests := []struct { + name string + input []Coordinates + expected []Coordinates + }{ + { + name: "de-dup same location", + input: []Coordinates{ + binA, binA, binA, + }, + expected: []Coordinates{ + binA, + }, + }, + { + name: "dont de-dup different filesystem", + input: []Coordinates{ + binB, binA, + }, + expected: []Coordinates{ + binA, binB, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, NewCoordinateSet(test.input...).ToSlice()) + }) + } +} diff --git a/test/integration/encode_decode_cycle_test.go b/test/integration/encode_decode_cycle_test.go index 7c520b076..8f51a5b42 100644 --- a/test/integration/encode_decode_cycle_test.go +++ b/test/integration/encode_decode_cycle_test.go @@ -31,14 +31,18 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) { originalSBOM, _ := catalogFixtureImage(t, "image-pkg-coverage") - by1, err := syft.Encode(originalSBOM, test.format) + appConfig := map[string]string{ + "config": "value", + } + + by1, err := syft.Encode(originalSBOM, appConfig, test.format) assert.NoError(t, err) newSBOM, newFormat, err := syft.Decode(bytes.NewReader(by1)) assert.NoError(t, err) assert.Equal(t, test.format, newFormat) - by2, err := syft.Encode(*newSBOM, test.format) + by2, err := syft.Encode(*newSBOM, appConfig, test.format) assert.NoError(t, err) if !assert.True(t, bytes.Equal(by1, by2)) {