diff --git a/cmd/packages.go b/cmd/packages.go index 47d55eb3ca4..81af8daca91 100644 --- a/cmd/packages.go +++ b/cmd/packages.go @@ -6,6 +6,8 @@ import ( "io/ioutil" "os" + "github.com/anchore/syft/syft/format" + "github.com/anchore/stereoscope" "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/anchore" @@ -48,7 +50,7 @@ const ( ) var ( - packagesPresenterOpt packages.PresenterOption + packagesPresenterOpt format.Option packagesCmd = &cobra.Command{ Use: "packages [SOURCE]", Short: "Generate a package SBOM", @@ -62,8 +64,8 @@ var ( SilenceErrors: true, PreRunE: func(cmd *cobra.Command, args []string) error { // set the presenter - presenterOption := packages.ParsePresenterOption(appConfig.Output) - if presenterOption == packages.UnknownPresenterOption { + presenterOption := format.ParseOption(appConfig.Output) + if presenterOption == format.UnknownFormatOption { return fmt.Errorf("bad --output value '%s'", appConfig.Output) } packagesPresenterOpt = presenterOption @@ -100,8 +102,8 @@ func setPackageFlags(flags *pflag.FlagSet) { fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes)) flags.StringP( - "output", "o", string(packages.TablePresenterOption), - fmt.Sprintf("report output formatter, options=%v", packages.AllPresenters), + "output", "o", string(format.TableOption), + fmt.Sprintf("report output formatter, options=%v", format.AllPresenters), ) flags.StringP( diff --git a/internal/anchore/import_package_sbom.go b/internal/anchore/import_package_sbom.go index 02b832e7eaa..5fb014b0f20 100644 --- a/internal/anchore/import_package_sbom.go +++ b/internal/anchore/import_package_sbom.go @@ -8,7 +8,7 @@ import ( "fmt" "net/http" - "github.com/anchore/syft/internal/presenter/packages" + "github.com/anchore/syft/internal/formats/syftjson" "github.com/wagoodman/go-progress" @@ -26,8 +26,8 @@ type packageSBOMImportAPI interface { func packageSbomModel(s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, scope source.Scope) (*external.ImagePackageManifest, error) { var buf bytes.Buffer - pres := packages.NewJSONPresenter(catalog, s, d, scope) - err := pres.Present(&buf) + + err := syftjson.Format().Presenter(catalog, &s, d, scope).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 cf97899ead6..1d727a55539 100644 --- a/internal/anchore/import_package_sbom_test.go +++ b/internal/anchore/import_package_sbom_test.go @@ -9,18 +9,15 @@ import ( "strings" "testing" - "github.com/anchore/syft/internal/presenter/packages" - - "github.com/wagoodman/go-progress" - - "github.com/anchore/syft/syft/distro" - - "github.com/docker/docker/pkg/ioutils" - "github.com/anchore/client-go/pkg/external" + "github.com/anchore/syft/internal/formats/syftjson" + syftjsonModel "github.com/anchore/syft/internal/formats/syftjson/model" + "github.com/anchore/syft/syft/distro" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" + "github.com/docker/docker/pkg/ioutils" "github.com/go-test/deep" + "github.com/wagoodman/go-progress" ) func must(c pkg.CPE, e error) pkg.CPE { @@ -88,19 +85,19 @@ func TestPackageSbomToModel(t *testing.T) { } var buf bytes.Buffer - pres := packages.NewJSONPresenter(c, m, &d, source.AllLayersScope) + pres := syftjson.Format().Presenter(c, &m, &d, source.AllLayersScope) if err := pres.Present(&buf); err != nil { t.Fatalf("unable to get expected json: %+v", err) } // unmarshal expected result - var expectedDoc packages.JSONDocument + var expectedDoc syftjsonModel.Document if err := json.Unmarshal(buf.Bytes(), &expectedDoc); err != nil { t.Fatalf("unable to parse json doc: %+v", err) } // unmarshal actual result - var actualDoc packages.JSONDocument + var actualDoc syftjsonModel.Document if err := json.Unmarshal(modelJSON, &actualDoc); err != nil { t.Fatalf("unable to parse json doc: %+v", err) } diff --git a/internal/presenter/packages/utils_test.go b/internal/formats/common/testutils/utils.go similarity index 88% rename from internal/presenter/packages/utils_test.go rename to internal/formats/common/testutils/utils.go index 9c6a88242a4..b8ca909a73f 100644 --- a/internal/presenter/packages/utils_test.go +++ b/internal/formats/common/testutils/utils.go @@ -1,15 +1,16 @@ -package packages +package testutils import ( "bytes" "testing" + "github.com/anchore/syft/syft/presenter" + "github.com/anchore/go-testutils" "github.com/anchore/stereoscope/pkg/filetree" "github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/syft/syft/distro" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/presenter" "github.com/anchore/syft/syft/source" "github.com/sergi/go-diff/diffmatchpatch" "github.com/stretchr/testify/assert" @@ -17,7 +18,7 @@ import ( type redactor func(s []byte) []byte -func assertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Presenter, testImage string, updateSnapshot bool, redactors ...redactor) { +func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Presenter, testImage string, updateSnapshot bool, redactors ...redactor) { var buffer bytes.Buffer // grab the latest image contents and persist @@ -50,7 +51,7 @@ func assertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Pres } } -func assertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter, updateSnapshot bool, redactors ...redactor) { +func AssertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter, updateSnapshot bool, redactors ...redactor) { var buffer bytes.Buffer err := pres.Present(&buffer) @@ -77,7 +78,7 @@ func assertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter } } -func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.Metadata, *distro.Distro) { +func ImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.Metadata, *distro.Distro) { t.Helper() catalog := pkg.NewCatalog() img := imagetest.GetGoldenFixtureImage(t, testImage) @@ -104,7 +105,7 @@ func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.M }, PURL: "a-purl-1", CPEs: []pkg.CPE{ - must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")), + pkg.MustCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"), }, }) catalog.Add(pkg.Package{ @@ -123,7 +124,7 @@ func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.M }, PURL: "a-purl-2", CPEs: []pkg.CPE{ - must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")), + pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), }, }) @@ -139,7 +140,7 @@ func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.M return catalog, src.Metadata, &dist } -func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *distro.Distro) { +func DirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *distro.Distro) { catalog := pkg.NewCatalog() // populate catalog with test data @@ -160,13 +161,13 @@ func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *dist Version: "1.0.1", Files: []pkg.PythonFileRecord{ { - Path: "/some/path/pkg1/depedencies/foo", + Path: "/some/path/pkg1/dependencies/foo", }, }, }, PURL: "a-purl-2", CPEs: []pkg.CPE{ - must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")), + pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), }, }) catalog.Add(pkg.Package{ @@ -185,7 +186,7 @@ func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *dist }, PURL: "a-purl-2", CPEs: []pkg.CPE{ - must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")), + pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), }, }) diff --git a/internal/formats/formats.go b/internal/formats/formats.go new file mode 100644 index 00000000000..5c82bd6533a --- /dev/null +++ b/internal/formats/formats.go @@ -0,0 +1,34 @@ +package formats + +import ( + "bytes" + + "github.com/anchore/syft/internal/formats/syftjson" + "github.com/anchore/syft/syft/format" +) + +// TODO: eventually this is the source of truth for all formatters +func All() []format.Format { + return []format.Format{ + syftjson.Format(), + } +} + +func Identify(by []byte) (*format.Format, error) { + for _, f := range All() { + if err := f.Validate(bytes.NewReader(by)); err != nil { + continue + } + return &f, nil + } + return nil, nil +} + +func ByOption(option format.Option) *format.Format { + for _, f := range All() { + if f.Option == option { + return &f + } + } + return nil +} diff --git a/internal/formats/formats_test.go b/internal/formats/formats_test.go new file mode 100644 index 00000000000..3df97558c5f --- /dev/null +++ b/internal/formats/formats_test.go @@ -0,0 +1,34 @@ +package formats + +import ( + "io" + "os" + "testing" + + "github.com/anchore/syft/syft/format" + "github.com/stretchr/testify/assert" +) + +func TestIdentify(t *testing.T) { + tests := []struct { + fixture string + expected format.Option + }{ + { + fixture: "test-fixtures/alpine-syft.json", + expected: format.JSONOption, + }, + } + for _, test := range tests { + t.Run(test.fixture, func(t *testing.T) { + f, err := os.Open(test.fixture) + assert.NoError(t, err) + by, err := io.ReadAll(f) + assert.NoError(t, err) + frmt, err := Identify(by) + assert.NoError(t, err) + assert.NotNil(t, frmt) + assert.Equal(t, test.expected, frmt.Option) + }) + } +} diff --git a/internal/formats/syftjson/decoder.go b/internal/formats/syftjson/decoder.go new file mode 100644 index 00000000000..46e4c47d3d9 --- /dev/null +++ b/internal/formats/syftjson/decoder.go @@ -0,0 +1,24 @@ +package syftjson + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/anchore/syft/internal/formats/syftjson/model" + "github.com/anchore/syft/syft/distro" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +func decoder(reader io.Reader) (*pkg.Catalog, *source.Metadata, *distro.Distro, source.Scope, error) { + dec := json.NewDecoder(reader) + + var doc model.Document + err := dec.Decode(&doc) + if err != nil { + return nil, nil, nil, source.UnknownScope, fmt.Errorf("unable to decode syft-json: %w", err) + } + + return toSyftModel(doc) +} diff --git a/internal/formats/syftjson/decoder_test.go b/internal/formats/syftjson/decoder_test.go new file mode 100644 index 00000000000..ac3ba44e932 --- /dev/null +++ b/internal/formats/syftjson/decoder_test.go @@ -0,0 +1,52 @@ +package syftjson + +import ( + "bytes" + "strings" + "testing" + + "github.com/anchore/syft/syft/source" + + "github.com/anchore/syft/internal/formats/common/testutils" + "github.com/go-test/deep" + "github.com/stretchr/testify/assert" +) + +func TestEncodeDecodeCycle(t *testing.T) { + testImage := "image-simple" + originalCatalog, originalMetadata, _ := testutils.ImageInput(t, testImage) + + var buf bytes.Buffer + assert.NoError(t, encoder(&buf, originalCatalog, &originalMetadata, nil, source.SquashedScope)) + + actualCatalog, actualMetadata, _, _, err := decoder(bytes.NewReader(buf.Bytes())) + assert.NoError(t, err) + + for _, d := range deep.Equal(originalMetadata, *actualMetadata) { + t.Errorf("metadata difference: %+v", d) + } + + actualPackages := actualCatalog.Sorted() + for idx, p := range originalCatalog.Sorted() { + if !assert.Equal(t, p.Name, actualPackages[idx].Name) { + t.Errorf("different package at idx=%d: %s vs %s", idx, p.Name, actualPackages[idx].Name) + continue + } + + // ids will never be equal + p.ID = "" + actualPackages[idx].ID = "" + + for _, d := range deep.Equal(*p, *actualPackages[idx]) { + if strings.Contains(d, ".VirtualPath: ") { + // location.Virtual path is not exposed in the json output + continue + } + if strings.HasSuffix(d, " != []") { + // semantically the same + continue + } + t.Errorf("package difference (%s): %+v", p.Name, d) + } + } +} diff --git a/internal/formats/syftjson/encoder.go b/internal/formats/syftjson/encoder.go new file mode 100644 index 00000000000..64f8ca08f1a --- /dev/null +++ b/internal/formats/syftjson/encoder.go @@ -0,0 +1,23 @@ +package syftjson + +import ( + "encoding/json" + "io" + + "github.com/anchore/syft/syft/distro" + + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +func encoder(output io.Writer, catalog *pkg.Catalog, srcMetadata *source.Metadata, d *distro.Distro, scope source.Scope) error { + // TODO: application config not available yet + doc := ToFormatModel(catalog, srcMetadata, d, scope, nil) + + 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/formats/syftjson/encoder_test.go b/internal/formats/syftjson/encoder_test.go new file mode 100644 index 00000000000..7376451ac92 --- /dev/null +++ b/internal/formats/syftjson/encoder_test.go @@ -0,0 +1,31 @@ +package syftjson + +import ( + "flag" + "testing" + + "github.com/anchore/syft/syft/source" + + "github.com/anchore/syft/internal/formats/common/testutils" + "github.com/anchore/syft/syft/format" +) + +var updateJson = flag.Bool("update-json", false, "update the *.golden files for json presenters") + +func TestDirectoryPresenter(t *testing.T) { + catalog, metadata, distro := testutils.DirectoryInput(t) + testutils.AssertPresenterAgainstGoldenSnapshot(t, + format.NewPresenter(encoder, catalog, &metadata, distro, source.SquashedScope), + *updateJson, + ) +} + +func TestImagePresenter(t *testing.T) { + testImage := "image-simple" + catalog, metadata, distro := testutils.ImageInput(t, testImage) + testutils.AssertPresenterAgainstGoldenImageSnapshot(t, + format.NewPresenter(encoder, catalog, &metadata, distro, source.SquashedScope), + testImage, + *updateJson, + ) +} diff --git a/internal/formats/syftjson/format.go b/internal/formats/syftjson/format.go new file mode 100644 index 00000000000..bc3f5164289 --- /dev/null +++ b/internal/formats/syftjson/format.go @@ -0,0 +1,12 @@ +package syftjson + +import "github.com/anchore/syft/syft/format" + +func Format() format.Format { + return format.NewFormat( + format.JSONOption, + encoder, + decoder, + validator, + ) +} diff --git a/internal/formats/syftjson/model/distro.go b/internal/formats/syftjson/model/distro.go new file mode 100644 index 00000000000..b136f2505a7 --- /dev/null +++ b/internal/formats/syftjson/model/distro.go @@ -0,0 +1,8 @@ +package model + +// Distro provides information about a detected Linux Distro. +type Distro struct { + Name string `json:"name"` // Name of the Linux distribution + Version string `json:"version"` // Version of the Linux distribution (major or major.minor version) + IDLike string `json:"idLike"` // the ID_LIKE field found within the /etc/os-release file +} diff --git a/internal/formats/syftjson/model/document.go b/internal/formats/syftjson/model/document.go new file mode 100644 index 00000000000..71e76f62de1 --- /dev/null +++ b/internal/formats/syftjson/model/document.go @@ -0,0 +1,23 @@ +package model + +// Document represents the syft cataloging findings as a JSON document +type Document struct { + Artifacts []Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog + ArtifactRelationships []Relationship `json:"artifactRelationships"` + Source Source `json:"source"` // Source represents the original object that was cataloged + Distro Distro `json:"distro"` // Distro represents the Linux distribution that was detected from the source + Descriptor Descriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft + Schema Schema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape +} + +// Descriptor describes what created the document as well as surrounding metadata +type Descriptor struct { + Name string `json:"name"` + Version string `json:"version"` + Configuration interface{} `json:"configuration,omitempty"` +} + +type Schema struct { + Version string `json:"version"` + URL string `json:"url"` +} diff --git a/internal/formats/syftjson/model/package.go b/internal/formats/syftjson/model/package.go new file mode 100644 index 00000000000..4078d3101a3 --- /dev/null +++ b/internal/formats/syftjson/model/package.go @@ -0,0 +1,123 @@ +package model + +import ( + "encoding/json" + "fmt" + + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +// Package represents a pkg.Package object specialized for JSON marshaling and unmarshaling. +type Package struct { + PackageBasicData + PackageCustomData +} + +// PackageBasicData contains non-ambiguous values (type-wise) from pkg.Package. +type PackageBasicData struct { + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + Type pkg.Type `json:"type"` + FoundBy string `json:"foundBy"` + Locations []source.Location `json:"locations"` + Licenses []string `json:"licenses"` + Language pkg.Language `json:"language"` + CPEs []string `json:"cpes"` + PURL string `json:"purl"` +} + +// PackageCustomData contains ambiguous values (type-wise) from pkg.Package. +type PackageCustomData struct { + MetadataType pkg.MetadataType `json:"metadataType"` + Metadata interface{} `json:"metadata"` +} + +// packageMetadataUnpacker is all values needed from Package to disambiguate ambiguous fields during json unmarshaling. +type packageMetadataUnpacker struct { + MetadataType pkg.MetadataType `json:"metadataType"` + Metadata json.RawMessage `json:"metadata"` +} + +func (p *packageMetadataUnpacker) String() string { + return fmt.Sprintf("metadataType: %s, metadata: %s", p.MetadataType, string(p.Metadata)) +} + +// UnmarshalJSON is a custom unmarshaller for handling basic values and values with ambiguous types. +// nolint:funlen +func (p *Package) UnmarshalJSON(b []byte) error { + var basic PackageBasicData + if err := json.Unmarshal(b, &basic); err != nil { + return err + } + p.PackageBasicData = basic + + var unpacker packageMetadataUnpacker + if err := json.Unmarshal(b, &unpacker); err != nil { + log.Warnf("failed to unmarshall into packageMetadataUnpacker: %v", err) + return err + } + + p.MetadataType = unpacker.MetadataType + + switch p.MetadataType { + case pkg.ApkMetadataType: + var payload pkg.ApkMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.RpmdbMetadataType: + var payload pkg.RpmdbMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.DpkgMetadataType: + var payload pkg.DpkgMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.JavaMetadataType: + var payload pkg.JavaMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.RustCargoPackageMetadataType: + var payload pkg.CargoPackageMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.GemMetadataType: + var payload pkg.GemMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.KbPackageMetadataType: + var payload pkg.KbPackageMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.PythonPackageMetadataType: + var payload pkg.PythonPackageMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.NpmPackageJSONMetadataType: + var payload pkg.NpmPackageJSONMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + } + + return nil +} diff --git a/internal/formats/syftjson/model/relationship.go b/internal/formats/syftjson/model/relationship.go new file mode 100644 index 00000000000..8820156bdc8 --- /dev/null +++ b/internal/formats/syftjson/model/relationship.go @@ -0,0 +1,8 @@ +package model + +type Relationship struct { + Parent string `json:"parent"` + Child string `json:"child"` + Type string `json:"type"` + Metadata interface{} `json:"metadata"` +} diff --git a/internal/formats/syftjson/model/source.go b/internal/formats/syftjson/model/source.go new file mode 100644 index 00000000000..5f7ef47b5e7 --- /dev/null +++ b/internal/formats/syftjson/model/source.go @@ -0,0 +1,57 @@ +package model + +import ( + "encoding/json" + "fmt" + "strconv" + + "github.com/anchore/syft/syft/source" +) + +// Source object represents the thing that was cataloged +type Source struct { + Type string `json:"type"` + Target interface{} `json:"target"` +} + +// sourceUnpacker is used to unmarshal Source objects +type sourceUnpacker struct { + Type string `json:"type"` + Target json.RawMessage `json:"target"` +} + +type ImageSource struct { + source.ImageMetadata + Scope source.Scope `json:"scope"` +} + +// UnmarshalJSON populates a source object from JSON bytes. +func (s *Source) UnmarshalJSON(b []byte) error { + var unpacker sourceUnpacker + if err := json.Unmarshal(b, &unpacker); err != nil { + return err + } + + s.Type = unpacker.Type + + switch s.Type { + case "directory": + if target, err := strconv.Unquote(string(unpacker.Target)); err == nil { + s.Target = target + } else { + s.Target = string(unpacker.Target[:]) + } + + case "image": + var payload ImageSource + if err := json.Unmarshal(unpacker.Target, &payload); err != nil { + return err + } + s.Target = payload + + default: + return fmt.Errorf("unsupported package metadata type: %+v", s.Type) + } + + return nil +} diff --git a/internal/formats/syftjson/test-fixtures/image-simple/Dockerfile b/internal/formats/syftjson/test-fixtures/image-simple/Dockerfile new file mode 100644 index 00000000000..79cfa759e35 --- /dev/null +++ b/internal/formats/syftjson/test-fixtures/image-simple/Dockerfile @@ -0,0 +1,4 @@ +# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one. +FROM scratch +ADD file-1.txt /somefile-1.txt +ADD file-2.txt /somefile-2.txt diff --git a/internal/formats/syftjson/test-fixtures/image-simple/file-1.txt b/internal/formats/syftjson/test-fixtures/image-simple/file-1.txt new file mode 100644 index 00000000000..985d3408e98 --- /dev/null +++ b/internal/formats/syftjson/test-fixtures/image-simple/file-1.txt @@ -0,0 +1 @@ +this file has contents \ No newline at end of file diff --git a/internal/formats/syftjson/test-fixtures/image-simple/file-2.txt b/internal/formats/syftjson/test-fixtures/image-simple/file-2.txt new file mode 100644 index 00000000000..396d08bbc72 --- /dev/null +++ b/internal/formats/syftjson/test-fixtures/image-simple/file-2.txt @@ -0,0 +1 @@ +file-2 contents! \ No newline at end of file diff --git a/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryPresenter.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryPresenter.golden new file mode 100644 index 00000000000..7289b4213ac --- /dev/null +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestDirectoryPresenter.golden @@ -0,0 +1,86 @@ +{ + "artifacts": [ + { + "id": "package-1-id", + "name": "package-1", + "version": "1.0.1", + "type": "python", + "foundBy": "the-cataloger-1", + "locations": [ + { + "path": "/some/path/pkg1" + } + ], + "licenses": [ + "MIT" + ], + "language": "python", + "cpes": [ + "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + ], + "purl": "a-purl-2", + "metadataType": "PythonPackageMetadata", + "metadata": { + "name": "package-1", + "version": "1.0.1", + "license": "", + "author": "", + "authorEmail": "", + "platform": "", + "files": [ + { + "path": "/some/path/pkg1/dependencies/foo" + } + ], + "sitePackagesRootPath": "" + } + }, + { + "id": "package-2-id", + "name": "package-2", + "version": "2.0.1", + "type": "deb", + "foundBy": "the-cataloger-2", + "locations": [ + { + "path": "/some/path/pkg1" + } + ], + "licenses": [], + "language": "", + "cpes": [ + "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + ], + "purl": "a-purl-2", + "metadataType": "DpkgMetadata", + "metadata": { + "package": "package-2", + "source": "", + "version": "2.0.1", + "sourceVersion": "", + "architecture": "", + "maintainer": "", + "installedSize": 0, + "files": null + } + } + ], + "artifactRelationships": [], + "source": { + "type": "directory", + "target": "/some/path" + }, + "distro": { + "name": "debian", + "version": "1.2.3", + "idLike": "like!" + }, + "descriptor": { + "name": "syft", + "version": "[not provided]" + }, + "schema": { + "version": "1.1.0", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json" + } +} diff --git a/internal/formats/syftjson/test-fixtures/snapshot/TestImagePresenter.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestImagePresenter.golden new file mode 100644 index 00000000000..a3b69cdab0b --- /dev/null +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestImagePresenter.golden @@ -0,0 +1,108 @@ +{ + "artifacts": [ + { + "id": "package-1-id", + "name": "package-1", + "version": "1.0.1", + "type": "python", + "foundBy": "the-cataloger-1", + "locations": [ + { + "path": "/somefile-1.txt", + "layerID": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59" + } + ], + "licenses": [ + "MIT" + ], + "language": "python", + "cpes": [ + "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*" + ], + "purl": "a-purl-1", + "metadataType": "PythonPackageMetadata", + "metadata": { + "name": "package-1", + "version": "1.0.1", + "license": "", + "author": "", + "authorEmail": "", + "platform": "", + "sitePackagesRootPath": "" + } + }, + { + "id": "package-2-id", + "name": "package-2", + "version": "2.0.1", + "type": "deb", + "foundBy": "the-cataloger-2", + "locations": [ + { + "path": "/somefile-2.txt", + "layerID": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec" + } + ], + "licenses": [], + "language": "", + "cpes": [ + "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + ], + "purl": "a-purl-2", + "metadataType": "DpkgMetadata", + "metadata": { + "package": "package-2", + "source": "", + "version": "2.0.1", + "sourceVersion": "", + "architecture": "", + "maintainer": "", + "installedSize": 0, + "files": null + } + } + ], + "artifactRelationships": [], + "source": { + "type": "image", + "target": { + "userInput": "user-image-input", + "imageID": "sha256:2480160b55bec40c44d3b145c7b2c1c47160db8575c3dcae086d76b9370ae7ca", + "manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "tags": [ + "stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b" + ], + "imageSize": 38, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59", + "size": 22 + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec", + "size": 16 + } + ], + "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NjcsImRpZ2VzdCI6InNoYTI1NjoyNDgwMTYwYjU1YmVjNDBjNDRkM2IxNDVjN2IyYzFjNDcxNjBkYjg1NzVjM2RjYWUwODZkNzZiOTM3MGFlN2NhIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjpmYjZiZWVjYjc1YjM5ZjRiYjgxM2RiZjE3N2U1MDFlZGQ1ZGRiM2U2OWJiNDVjZWRlYjc4YzY3NmVlMWI3YTU5In0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OjMxOWI1ODhjZTY0MjUzYTg3YjUzM2M4ZWQwMWNmMDAyNWUwZWFjOThlN2I1MTZlMTI1MzI5NTdlMTI0NGZkZWMifV19", + "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMTAtMDRUMTE6NDA6MDAuNjM4Mzk0NVoiLCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMS0xMC0wNFQxMTo0MDowMC41OTA3MzE2WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0xLnR4dCAvc29tZWZpbGUtMS50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMS0xMC0wNFQxMTo0MDowMC42MzgzOTQ1WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6ZmI2YmVlY2I3NWIzOWY0YmI4MTNkYmYxNzdlNTAxZWRkNWRkYjNlNjliYjQ1Y2VkZWI3OGM2NzZlZTFiN2E1OSIsInNoYTI1NjozMTliNTg4Y2U2NDI1M2E4N2I1MzNjOGVkMDFjZjAwMjVlMGVhYzk4ZTdiNTE2ZTEyNTMyOTU3ZTEyNDRmZGVjIl19fQ==", + "repoDigests": [], + "scope": "Squashed" + } + }, + "distro": { + "name": "debian", + "version": "1.2.3", + "idLike": "like!" + }, + "descriptor": { + "name": "syft", + "version": "[not provided]" + }, + "schema": { + "version": "1.1.0", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json" + } +} diff --git a/internal/presenter/packages/test-fixtures/snapshot/stereoscope-fixture-packages-image-simple.golden b/internal/formats/syftjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden similarity index 69% rename from internal/presenter/packages/test-fixtures/snapshot/stereoscope-fixture-packages-image-simple.golden rename to internal/formats/syftjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden index b7d3fed18e8..c1b1d2b797e 100644 Binary files a/internal/presenter/packages/test-fixtures/snapshot/stereoscope-fixture-packages-image-simple.golden and b/internal/formats/syftjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden differ diff --git a/internal/formats/syftjson/to_format_model.go b/internal/formats/syftjson/to_format_model.go new file mode 100644 index 00000000000..16d0d310c77 --- /dev/null +++ b/internal/formats/syftjson/to_format_model.go @@ -0,0 +1,133 @@ +package syftjson + +import ( + "fmt" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/formats/syftjson/model" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/internal/version" + "github.com/anchore/syft/syft/distro" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +// TODO: this is export4ed for the use of the power-user command (temp) +func ToFormatModel(catalog *pkg.Catalog, srcMetadata *source.Metadata, d *distro.Distro, scope source.Scope, applicationConfig interface{}) model.Document { + src, err := toSourceModel(srcMetadata, scope) + if err != nil { + log.Warnf("unable to create syft-json source object: %+v", err) + } + + return model.Document{ + Artifacts: toPackageModels(catalog), + ArtifactRelationships: toRelationshipModel(pkg.NewRelationships(catalog)), + Source: src, + Distro: toDistroModel(d), + Descriptor: model.Descriptor{ + Name: internal.ApplicationName, + Version: version.FromBuild().Version, + Configuration: applicationConfig, + }, + Schema: model.Schema{ + Version: internal.JSONSchemaVersion, + URL: fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", internal.JSONSchemaVersion), + }, + } +} + +func toPackageModels(catalog *pkg.Catalog) []model.Package { + artifacts := make([]model.Package, 0) + if catalog == nil { + return artifacts + } + for _, p := range catalog.Sorted() { + artifacts = append(artifacts, toPackageModel(p)) + } + return artifacts +} + +// toPackageModel crates a new Package from the given pkg.Package. +func toPackageModel(p *pkg.Package) model.Package { + var cpes = make([]string, len(p.CPEs)) + for i, c := range p.CPEs { + cpes[i] = c.BindToFmtString() + } + + // ensure collections are never nil for presentation reasons + var locations = make([]source.Location, 0) + if p.Locations != nil { + locations = p.Locations + } + + var licenses = make([]string, 0) + if p.Licenses != nil { + licenses = p.Licenses + } + + return model.Package{ + PackageBasicData: model.PackageBasicData{ + ID: string(p.ID), + Name: p.Name, + Version: p.Version, + Type: p.Type, + FoundBy: p.FoundBy, + Locations: locations, + Licenses: licenses, + Language: p.Language, + CPEs: cpes, + PURL: p.PURL, + }, + PackageCustomData: model.PackageCustomData{ + MetadataType: p.MetadataType, + Metadata: p.Metadata, + }, + } +} + +func toRelationshipModel(relationships []pkg.Relationship) []model.Relationship { + result := make([]model.Relationship, len(relationships)) + for i, r := range relationships { + result[i] = model.Relationship{ + Parent: string(r.Parent), + Child: string(r.Child), + Type: string(r.Type), + Metadata: r.Metadata, + } + } + return result +} + +// toSourceModel creates a new source object to be represented into JSON. +func toSourceModel(src *source.Metadata, scope source.Scope) (model.Source, error) { + switch src.Scheme { + case source.ImageScheme: + return model.Source{ + Type: "image", + Target: model.ImageSource{ + ImageMetadata: src.ImageMetadata, + Scope: scope, + }, + }, nil + case source.DirectoryScheme: + return model.Source{ + Type: "directory", + Target: src.Path, + }, nil + default: + return model.Source{}, fmt.Errorf("unsupported source: %q", src.Scheme) + } +} + +// toDistroModel creates a struct with the Linux distribution to be represented in JSON. +func toDistroModel(d *distro.Distro) model.Distro { + if d == nil { + return model.Distro{} + } + + return model.Distro{ + Name: d.Name(), + Version: d.FullVersion(), + IDLike: d.IDLike, + } +} diff --git a/internal/formats/syftjson/to_syft_model.go b/internal/formats/syftjson/to_syft_model.go new file mode 100644 index 00000000000..f9ee6cab069 --- /dev/null +++ b/internal/formats/syftjson/to_syft_model.go @@ -0,0 +1,73 @@ +package syftjson + +import ( + "github.com/anchore/syft/internal/formats/syftjson/model" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/distro" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +func toSyftModel(doc model.Document) (*pkg.Catalog, *source.Metadata, *distro.Distro, source.Scope, error) { + dist, err := distro.NewDistro(distro.Type(doc.Distro.Name), doc.Distro.Version, doc.Distro.IDLike) + if err != nil { + return nil, nil, nil, source.UnknownScope, err + } + + srcMetadata, scope := toSyftSourceData(doc.Source) + + return toSyftCatalog(doc.Artifacts), srcMetadata, &dist, scope, nil +} + +func toSyftSourceData(s model.Source) (*source.Metadata, source.Scope) { + switch s.Type { + case "directory": + return &source.Metadata{ + Scheme: source.DirectoryScheme, + Path: s.Target.(string), + }, source.UnknownScope + case "image": + parsedSource := s.Target.(model.ImageSource) + return &source.Metadata{ + Scheme: source.ImageScheme, + ImageMetadata: parsedSource.ImageMetadata, + }, parsedSource.Scope + } + return nil, source.UnknownScope +} + +func toSyftCatalog(pkgs []model.Package) *pkg.Catalog { + catalog := pkg.NewCatalog() + for _, p := range pkgs { + catalog.Add(toSyftPackage(p)) + } + return catalog +} + +func toSyftPackage(p model.Package) pkg.Package { + var cpes []pkg.CPE + for _, c := range p.CPEs { + value, err := pkg.NewCPE(c) + if err != nil { + log.Warnf("excluding invalid CPE %q: %v", c, err) + continue + } + + cpes = append(cpes, value) + } + + return pkg.Package{ + ID: pkg.ID(p.ID), + Name: p.Name, + Version: p.Version, + FoundBy: p.FoundBy, + Locations: p.Locations, + Licenses: p.Licenses, + Language: p.Language, + Type: p.Type, + CPEs: cpes, + PURL: p.PURL, + MetadataType: p.MetadataType, + Metadata: p.Metadata, + } +} diff --git a/internal/formats/syftjson/validator.go b/internal/formats/syftjson/validator.go new file mode 100644 index 00000000000..d359b60d940 --- /dev/null +++ b/internal/formats/syftjson/validator.go @@ -0,0 +1,31 @@ +package syftjson + +import ( + "encoding/json" + "fmt" + "io" + "strings" + + "github.com/anchore/syft/internal/formats/syftjson/model" +) + +func validator(reader io.Reader) error { + type Document struct { + Schema model.Schema `json:"schema"` + } + + dec := json.NewDecoder(reader) + + var doc Document + err := dec.Decode(&doc) + if err != nil { + return fmt.Errorf("unable to decode: %w", err) + } + + // note: we accept all schema versions + // TODO: add per-schema version parsing + if strings.Contains(doc.Schema.URL, "anchore/syft") { + return nil + } + return fmt.Errorf("could not extract syft schema") +} diff --git a/internal/formats/test-fixtures/alpine-syft.json b/internal/formats/test-fixtures/alpine-syft.json new file mode 100644 index 00000000000..936f6d5150e --- /dev/null +++ b/internal/formats/test-fixtures/alpine-syft.json @@ -0,0 +1,1846 @@ +{ + "artifacts": [ + { + "id": "880965c7-95ff-483c-80bb-22799bbbee63", + "name": "alpine-baselayout", + "version": "3.2.0-r16", + "type": "apk", + "foundBy": "apkdb-cataloger", + "locations": [ + { + "path": "/lib/apk/db/installed", + "layerID": "sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68" + } + ], + "licenses": [ + "GPL-2.0-only" + ], + "language": "", + "cpes": [ + "cpe:2.3:a:alpine-baselayout:alpine-baselayout:3.2.0-r16:*:*:*:*:*:*:*", + "cpe:2.3:a:alpine-baselayout:alpine_baselayout:3.2.0-r16:*:*:*:*:*:*:*", + "cpe:2.3:a:alpine_baselayout:alpine-baselayout:3.2.0-r16:*:*:*:*:*:*:*", + "cpe:2.3:a:alpine_baselayout:alpine_baselayout:3.2.0-r16:*:*:*:*:*:*:*", + "cpe:2.3:a:alpine:alpine-baselayout:3.2.0-r16:*:*:*:*:*:*:*", + "cpe:2.3:a:alpine:alpine_baselayout:3.2.0-r16:*:*:*:*:*:*:*" + ], + "purl": "pkg:alpine/alpine-baselayout@3.2.0-r16?arch=x86_64", + "metadataType": "ApkMetadata", + "metadata": { + "package": "alpine-baselayout", + "originPackage": "alpine-baselayout", + "maintainer": "Natanael Copa ", + "version": "3.2.0-r16", + "license": "GPL-2.0-only", + "architecture": "x86_64", + "url": "https://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout", + "description": "Alpine base dir structure and init scripts", + "size": 20716, + "installedSize": 413696, + "pullDependencies": "/bin/sh so:libc.musl-x86_64.so.1", + "pullChecksum": "Q1UJtB9cNV4r+/VbxySkEei++qbho=", + "gitCommitOfApkPort": "8a8c96a0ea2fcd824c361aa4438763fa33ee8ca0", + "files": [ + { + "path": "/dev" + }, + { + "path": "/dev/pts" + }, + { + "path": "/dev/shm" + }, + { + "path": "/etc" + }, + { + "path": "/etc/fstab", + "digest": { + "algorithm": "sha1", + "value": "Q11Q7hNe8QpDS531guqCdrXBzoA/o=" + } + }, + { + "path": "/etc/group", + "digest": { + "algorithm": "sha1", + "value": "Q13K+olJg5ayzHSVNUkggZJXuB+9Y=" + } + }, + { + "path": "/etc/hostname", + "digest": { + "algorithm": "sha1", + "value": "Q16nVwYVXP/tChvUPdukVD2ifXOmc=" + } + }, + { + "path": "/etc/hosts", + "digest": { + "algorithm": "sha1", + "value": "Q1BD6zJKZTRWyqGnPi4tSfd3krsMU=" + } + }, + { + "path": "/etc/inittab", + "digest": { + "algorithm": "sha1", + "value": "Q1TsthbhW7QzWRe1E/NKwTOuD4pHc=" + } + }, + { + "path": "/etc/modules", + "digest": { + "algorithm": "sha1", + "value": "Q1toogjUipHGcMgECgPJX64SwUT1M=" + } + }, + { + "path": "/etc/motd", + "digest": { + "algorithm": "sha1", + "value": "Q1XmduVVNURHQ27TvYp1Lr5TMtFcA=" + } + }, + { + "path": "/etc/mtab", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q1kiljhXXH1LlQroHsEJIkPZg2eiw=" + } + }, + { + "path": "/etc/passwd", + "digest": { + "algorithm": "sha1", + "value": "Q1TchuuLUfur0izvfZQZxgN/LJhB8=" + } + }, + { + "path": "/etc/profile", + "digest": { + "algorithm": "sha1", + "value": "Q1VmHPWPjjvz4oCsbmYCUB4uWpSkc=" + } + }, + { + "path": "/etc/protocols", + "digest": { + "algorithm": "sha1", + "value": "Q1omKlp3vgGq2ZqYzyD/KHNdo8rDc=" + } + }, + { + "path": "/etc/services", + "digest": { + "algorithm": "sha1", + "value": "Q19WLCv5ItKg4MH7RWfNRh1I7byQc=" + } + }, + { + "path": "/etc/shadow", + "ownerUid": "0", + "ownerGid": "42", + "permissions": "640", + "digest": { + "algorithm": "sha1", + "value": "Q1ltrPIAW2zHeDiajsex2Bdmq3uqA=" + } + }, + { + "path": "/etc/shells", + "digest": { + "algorithm": "sha1", + "value": "Q1ojm2YdpCJ6B/apGDaZ/Sdb2xJkA=" + } + }, + { + "path": "/etc/sysctl.conf", + "digest": { + "algorithm": "sha1", + "value": "Q14upz3tfnNxZkIEsUhWn7Xoiw96g=" + } + }, + { + "path": "/etc/apk" + }, + { + "path": "/etc/conf.d" + }, + { + "path": "/etc/crontabs" + }, + { + "path": "/etc/crontabs/root", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "600", + "digest": { + "algorithm": "sha1", + "value": "Q1vfk1apUWI4yLJGhhNRd0kJixfvY=" + } + }, + { + "path": "/etc/init.d" + }, + { + "path": "/etc/modprobe.d" + }, + { + "path": "/etc/modprobe.d/aliases.conf", + "digest": { + "algorithm": "sha1", + "value": "Q1WUbh6TBYNVK7e4Y+uUvLs/7viqk=" + } + }, + { + "path": "/etc/modprobe.d/blacklist.conf", + "digest": { + "algorithm": "sha1", + "value": "Q1xxYGU6S6TLQvb7ervPrWWwAWqMg=" + } + }, + { + "path": "/etc/modprobe.d/i386.conf", + "digest": { + "algorithm": "sha1", + "value": "Q1pnay/njn6ol9cCssL7KiZZ8etlc=" + } + }, + { + "path": "/etc/modprobe.d/kms.conf", + "digest": { + "algorithm": "sha1", + "value": "Q1ynbLn3GYDpvajba/ldp1niayeog=" + } + }, + { + "path": "/etc/modules-load.d" + }, + { + "path": "/etc/network" + }, + { + "path": "/etc/network/if-down.d" + }, + { + "path": "/etc/network/if-post-down.d" + }, + { + "path": "/etc/network/if-pre-up.d" + }, + { + "path": "/etc/network/if-up.d" + }, + { + "path": "/etc/opt" + }, + { + "path": "/etc/periodic" + }, + { + "path": "/etc/periodic/15min" + }, + { + "path": "/etc/periodic/daily" + }, + { + "path": "/etc/periodic/hourly" + }, + { + "path": "/etc/periodic/monthly" + }, + { + "path": "/etc/periodic/weekly" + }, + { + "path": "/etc/profile.d" + }, + { + "path": "/etc/profile.d/README", + "digest": { + "algorithm": "sha1", + "value": "Q135OWsCzzvnB2fmFx62kbqm1Ax1k=" + } + }, + { + "path": "/etc/profile.d/color_prompt.sh.disabled", + "digest": { + "algorithm": "sha1", + "value": "Q10wL23GuSCVfumMRgakabUI6EsSk=" + } + }, + { + "path": "/etc/profile.d/locale.sh", + "digest": { + "algorithm": "sha1", + "value": "Q1S8j+WW71mWxfVy8ythqU7HUVoBw=" + } + }, + { + "path": "/etc/sysctl.d" + }, + { + "path": "/home" + }, + { + "path": "/lib" + }, + { + "path": "/lib/firmware" + }, + { + "path": "/lib/mdev" + }, + { + "path": "/lib/modules-load.d" + }, + { + "path": "/lib/sysctl.d" + }, + { + "path": "/lib/sysctl.d/00-alpine.conf", + "digest": { + "algorithm": "sha1", + "value": "Q1HpElzW1xEgmKfERtTy7oommnq6c=" + } + }, + { + "path": "/media" + }, + { + "path": "/media/cdrom" + }, + { + "path": "/media/floppy" + }, + { + "path": "/media/usb" + }, + { + "path": "/mnt" + }, + { + "path": "/opt" + }, + { + "path": "/proc" + }, + { + "path": "/root", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "700" + }, + { + "path": "/run" + }, + { + "path": "/sbin" + }, + { + "path": "/sbin/mkmntdirs", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q1VcFI6coRl6HWWVShMW+G5Sb3JH8=" + } + }, + { + "path": "/srv" + }, + { + "path": "/sys" + }, + { + "path": "/tmp", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "1777" + }, + { + "path": "/usr" + }, + { + "path": "/usr/lib" + }, + { + "path": "/usr/lib/modules-load.d" + }, + { + "path": "/usr/local" + }, + { + "path": "/usr/local/bin" + }, + { + "path": "/usr/local/lib" + }, + { + "path": "/usr/local/share" + }, + { + "path": "/usr/sbin" + }, + { + "path": "/usr/share" + }, + { + "path": "/usr/share/man" + }, + { + "path": "/usr/share/misc" + }, + { + "path": "/var" + }, + { + "path": "/var/run", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q11/SNZz/8cK2dSKK+cJpVrZIuF4Q=" + } + }, + { + "path": "/var/cache" + }, + { + "path": "/var/cache/misc" + }, + { + "path": "/var/empty", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "555" + }, + { + "path": "/var/lib" + }, + { + "path": "/var/lib/misc" + }, + { + "path": "/var/local" + }, + { + "path": "/var/lock" + }, + { + "path": "/var/lock/subsys" + }, + { + "path": "/var/log" + }, + { + "path": "/var/mail" + }, + { + "path": "/var/opt" + }, + { + "path": "/var/spool" + }, + { + "path": "/var/spool/mail", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q1dzbdazYZA2nTzSIG3YyNw7d4Juc=" + } + }, + { + "path": "/var/spool/cron" + }, + { + "path": "/var/spool/cron/crontabs", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q1OFZt+ZMp7j0Gny0rqSKuWJyqYmA=" + } + }, + { + "path": "/var/tmp", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "1777" + } + ] + } + }, + { + "id": "8dbd8f4d-c13f-4120-8c2b-4596e5c46fce", + "name": "alpine-keys", + "version": "2.3-r1", + "type": "apk", + "foundBy": "apkdb-cataloger", + "locations": [ + { + "path": "/lib/apk/db/installed", + "layerID": "sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68" + } + ], + "licenses": [ + "MIT" + ], + "language": "", + "cpes": [ + "cpe:2.3:a:alpine-keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*", + "cpe:2.3:a:alpine-keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*", + "cpe:2.3:a:alpine_keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*", + "cpe:2.3:a:alpine_keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*", + "cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*", + "cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*" + ], + "purl": "pkg:alpine/alpine-keys@2.3-r1?arch=x86_64", + "metadataType": "ApkMetadata", + "metadata": { + "package": "alpine-keys", + "originPackage": "alpine-keys", + "maintainer": "Natanael Copa ", + "version": "2.3-r1", + "license": "MIT", + "architecture": "x86_64", + "url": "https://alpinelinux.org", + "description": "Public keys for Alpine Linux packages", + "size": 5988, + "installedSize": 118784, + "pullDependencies": "", + "pullChecksum": "Q1ZV5wTtRXerQz0CBrwtKcvErpPeY=", + "gitCommitOfApkPort": "0972396a79fd723a429e518120fdf7ab48407468", + "files": [ + { + "path": "/etc" + }, + { + "path": "/etc/apk" + }, + { + "path": "/etc/apk/keys" + }, + { + "path": "/etc/apk/keys/alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub", + "digest": { + "algorithm": "sha1", + "value": "Q1OvCFSO94z97c80mIDCxqGkh2Og4=" + } + }, + { + "path": "/etc/apk/keys/alpine-devel@lists.alpinelinux.org-5243ef4b.rsa.pub", + "digest": { + "algorithm": "sha1", + "value": "Q1v7YWZYzAWoclaLDI45jEguI7YN0=" + } + }, + { + "path": "/etc/apk/keys/alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub", + "digest": { + "algorithm": "sha1", + "value": "Q1NnGuDsdQOx4ZNYfB3N97eLyGPkI=" + } + }, + { + "path": "/usr" + }, + { + "path": "/usr/share" + }, + { + "path": "/usr/share/apk" + }, + { + "path": "/usr/share/apk/keys" + }, + { + "path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub", + "digest": { + "algorithm": "sha1", + "value": "Q1OvCFSO94z97c80mIDCxqGkh2Og4=" + } + }, + { + "path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-5243ef4b.rsa.pub", + "digest": { + "algorithm": "sha1", + "value": "Q1v7YWZYzAWoclaLDI45jEguI7YN0=" + } + }, + { + "path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-524d27bb.rsa.pub", + "digest": { + "algorithm": "sha1", + "value": "Q1BTqS+H/UUyhQuzHwiBl47+BTKuU=" + } + }, + { + "path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub", + "digest": { + "algorithm": "sha1", + "value": "Q1NnGuDsdQOx4ZNYfB3N97eLyGPkI=" + } + }, + { + "path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-58199dcc.rsa.pub", + "digest": { + "algorithm": "sha1", + "value": "Q1Oaxdcsa6AYoPdLi0U4lO3J2we18=" + } + }, + { + "path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-58cbb476.rsa.pub", + "digest": { + "algorithm": "sha1", + "value": "Q1yPq+su65ksNox3uXB+DR7P18+QU=" + } + }, + { + "path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-58e4f17d.rsa.pub", + "digest": { + "algorithm": "sha1", + "value": "Q1MpZDNX0LeLHvSOwVUyXiXx11NN0=" + } + }, + { + "path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-5e69ca50.rsa.pub", + "digest": { + "algorithm": "sha1", + "value": "Q1glCQ/eJbvA5xqcswdjFrWv5Fnk0=" + } + }, + { + "path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-60ac2099.rsa.pub", + "digest": { + "algorithm": "sha1", + "value": "Q1XUdDEoNTtjlvrS+iunk6ziFgIpU=" + } + }, + { + "path": "/usr/share/apk/keys/aarch64" + }, + { + "path": "/usr/share/apk/keys/aarch64/alpine-devel@lists.alpinelinux.org-58199dcc.rsa.pub", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q17j9nWJkQ+wfIuVQzIFrmFZ7fSOc=" + } + }, + { + "path": "/usr/share/apk/keys/armhf" + }, + { + "path": "/usr/share/apk/keys/armhf/alpine-devel@lists.alpinelinux.org-524d27bb.rsa.pub", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q1U9QtsdN+rYZ9Zh76EfXy00JZHMg=" + } + }, + { + "path": "/usr/share/apk/keys/armv7" + }, + { + "path": "/usr/share/apk/keys/armv7/alpine-devel@lists.alpinelinux.org-524d27bb.rsa.pub", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q1U9QtsdN+rYZ9Zh76EfXy00JZHMg=" + } + }, + { + "path": "/usr/share/apk/keys/mips64" + }, + { + "path": "/usr/share/apk/keys/mips64/alpine-devel@lists.alpinelinux.org-5e69ca50.rsa.pub", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q1hCZdFx+LvzbLtPs753je78gEEBQ=" + } + }, + { + "path": "/usr/share/apk/keys/ppc64le" + }, + { + "path": "/usr/share/apk/keys/ppc64le/alpine-devel@lists.alpinelinux.org-58cbb476.rsa.pub", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q1t21dhCLbTJmAHXSCeOMq/2vfSgo=" + } + }, + { + "path": "/usr/share/apk/keys/riscv64" + }, + { + "path": "/usr/share/apk/keys/riscv64/alpine-devel@lists.alpinelinux.org-60ac2099.rsa.pub", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q1NVPbZavaXpsItFwQYDWbpor7yYE=" + } + }, + { + "path": "/usr/share/apk/keys/s390x" + }, + { + "path": "/usr/share/apk/keys/s390x/alpine-devel@lists.alpinelinux.org-58e4f17d.rsa.pub", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q1sjbV2r2w0Ih2vwdzC4Jq6UI7cMQ=" + } + }, + { + "path": "/usr/share/apk/keys/x86" + }, + { + "path": "/usr/share/apk/keys/x86/alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q1Ii51i7Nrc4uft14HhqugaUqdH64=" + } + }, + { + "path": "/usr/share/apk/keys/x86/alpine-devel@lists.alpinelinux.org-5243ef4b.rsa.pub", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q1Y49eVxhpvftbQ3yAdvlLfcrPLTU=" + } + }, + { + "path": "/usr/share/apk/keys/x86_64" + }, + { + "path": "/usr/share/apk/keys/x86_64/alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q1Ii51i7Nrc4uft14HhqugaUqdH64=" + } + }, + { + "path": "/usr/share/apk/keys/x86_64/alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q1AUFY+fwSBTcrYetjT7NHvafrSQc=" + } + } + ] + } + }, + { + "id": "b03a5cd0-e8e4-4a0c-b283-86b4942b4cfc", + "name": "apk-tools", + "version": "2.12.7-r0", + "type": "apk", + "foundBy": "apkdb-cataloger", + "locations": [ + { + "path": "/lib/apk/db/installed", + "layerID": "sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68" + } + ], + "licenses": [ + "GPL-2.0-only" + ], + "language": "", + "cpes": [ + "cpe:2.3:a:apk-tools:apk-tools:2.12.7-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:apk-tools:apk_tools:2.12.7-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:apk_tools:apk-tools:2.12.7-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:apk_tools:apk_tools:2.12.7-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:apk:apk-tools:2.12.7-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:apk:apk_tools:2.12.7-r0:*:*:*:*:*:*:*" + ], + "purl": "pkg:alpine/apk-tools@2.12.7-r0?arch=x86_64", + "metadataType": "ApkMetadata", + "metadata": { + "package": "apk-tools", + "originPackage": "apk-tools", + "maintainer": "Natanael Copa ", + "version": "2.12.7-r0", + "license": "GPL-2.0-only", + "architecture": "x86_64", + "url": "https://gitlab.alpinelinux.org/alpine/apk-tools", + "description": "Alpine Package Keeper - package manager for alpine", + "size": 119979, + "installedSize": 311296, + "pullDependencies": "musl>=1.2 so:libc.musl-x86_64.so.1 so:libcrypto.so.1.1 so:libssl.so.1.1 so:libz.so.1", + "pullChecksum": "Q1VnSoy5XasRtvFODOZ9P0pZG4Hxs=", + "gitCommitOfApkPort": "03c6b1b0ec20f417cee0f3c1d56795b27871b1c5", + "files": [ + { + "path": "/etc" + }, + { + "path": "/etc/apk" + }, + { + "path": "/etc/apk/keys" + }, + { + "path": "/etc/apk/protected_paths.d" + }, + { + "path": "/lib" + }, + { + "path": "/lib/libapk.so.3.12.0", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q1fg+B1BJHpxBSS5dRPU+3zeeuFI4=" + } + }, + { + "path": "/sbin" + }, + { + "path": "/sbin/apk", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q1LugZocafK9LifJwfELs3VqdEIxw=" + } + }, + { + "path": "/var" + }, + { + "path": "/var/cache" + }, + { + "path": "/var/cache/misc" + }, + { + "path": "/var/lib" + }, + { + "path": "/var/lib/apk" + } + ] + } + }, + { + "id": "cb3f3296-98d8-4e88-bf0a-3bdc53f817fb", + "name": "busybox", + "version": "1.33.1-r3", + "type": "apk", + "foundBy": "apkdb-cataloger", + "locations": [ + { + "path": "/lib/apk/db/installed", + "layerID": "sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68" + } + ], + "licenses": [ + "GPL-2.0-only" + ], + "language": "", + "cpes": [ + "cpe:2.3:a:busybox:busybox:1.33.1-r3:*:*:*:*:*:*:*" + ], + "purl": "pkg:alpine/busybox@1.33.1-r3?arch=x86_64", + "metadataType": "ApkMetadata", + "metadata": { + "package": "busybox", + "originPackage": "busybox", + "maintainer": "Natanael Copa ", + "version": "1.33.1-r3", + "license": "GPL-2.0-only", + "architecture": "x86_64", + "url": "https://busybox.net/", + "description": "Size optimized toolbox of many common UNIX utilities", + "size": 499368, + "installedSize": 950272, + "pullDependencies": "so:libc.musl-x86_64.so.1", + "pullChecksum": "Q1rGvmeXEbASEg01DqEqMbxV0fts4=", + "gitCommitOfApkPort": "98710256acf90e670e79f32f1f0350b830979d24", + "files": [ + { + "path": "/bin" + }, + { + "path": "/bin/busybox", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q135CE+2ocG61L7La8CI3kfLURucU=" + } + }, + { + "path": "/bin/sh", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q1pcfTfDNEbNKQc2s1tia7da05M8Q=" + } + }, + { + "path": "/etc" + }, + { + "path": "/etc/securetty", + "digest": { + "algorithm": "sha1", + "value": "Q1mB95Hq2NUTZ599RDiSsj9w5FrOU=" + } + }, + { + "path": "/etc/udhcpd.conf", + "digest": { + "algorithm": "sha1", + "value": "Q1UAiPZcDIW1ClRzobfggcCQ77V28=" + } + }, + { + "path": "/etc/logrotate.d" + }, + { + "path": "/etc/logrotate.d/acpid", + "digest": { + "algorithm": "sha1", + "value": "Q1TylyCINVmnS+A/Tead4vZhE7Bks=" + } + }, + { + "path": "/etc/network" + }, + { + "path": "/etc/network/if-down.d" + }, + { + "path": "/etc/network/if-post-down.d" + }, + { + "path": "/etc/network/if-post-up.d" + }, + { + "path": "/etc/network/if-pre-down.d" + }, + { + "path": "/etc/network/if-pre-up.d" + }, + { + "path": "/etc/network/if-up.d" + }, + { + "path": "/etc/network/if-up.d/dad", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "775", + "digest": { + "algorithm": "sha1", + "value": "Q1ORf+lPRKuYgdkBBcKoevR1t60Q4=" + } + }, + { + "path": "/sbin" + }, + { + "path": "/tmp", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "1777" + }, + { + "path": "/usr" + }, + { + "path": "/usr/sbin" + }, + { + "path": "/usr/share" + }, + { + "path": "/usr/share/udhcpc" + }, + { + "path": "/usr/share/udhcpc/default.script", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q1t9vir/ZrX3nbSIYT9BDLWZenkVQ=" + } + }, + { + "path": "/var" + }, + { + "path": "/var/cache" + }, + { + "path": "/var/cache/misc" + }, + { + "path": "/var/lib" + }, + { + "path": "/var/lib/udhcpd" + } + ] + } + }, + { + "id": "bdac28cf-4448-43d1-bdb6-1b6e8df0a2d3", + "name": "ca-certificates-bundle", + "version": "20191127-r5", + "type": "apk", + "foundBy": "apkdb-cataloger", + "locations": [ + { + "path": "/lib/apk/db/installed", + "layerID": "sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68" + } + ], + "licenses": [ + "MPL-2.0", + "AND", + "MIT" + ], + "language": "", + "cpes": [ + "cpe:2.3:a:ca-certificates-bundle:ca-certificates-bundle:20191127-r5:*:*:*:*:*:*:*", + "cpe:2.3:a:ca-certificates-bundle:ca_certificates_bundle:20191127-r5:*:*:*:*:*:*:*", + "cpe:2.3:a:ca_certificates_bundle:ca-certificates-bundle:20191127-r5:*:*:*:*:*:*:*", + "cpe:2.3:a:ca_certificates_bundle:ca_certificates_bundle:20191127-r5:*:*:*:*:*:*:*", + "cpe:2.3:a:ca-certificates:ca-certificates-bundle:20191127-r5:*:*:*:*:*:*:*", + "cpe:2.3:a:ca-certificates:ca_certificates_bundle:20191127-r5:*:*:*:*:*:*:*", + "cpe:2.3:a:ca_certificates:ca-certificates-bundle:20191127-r5:*:*:*:*:*:*:*", + "cpe:2.3:a:ca_certificates:ca_certificates_bundle:20191127-r5:*:*:*:*:*:*:*", + "cpe:2.3:a:ca:ca-certificates-bundle:20191127-r5:*:*:*:*:*:*:*", + "cpe:2.3:a:ca:ca_certificates_bundle:20191127-r5:*:*:*:*:*:*:*" + ], + "purl": "pkg:alpine/ca-certificates-bundle@20191127-r5?arch=x86_64", + "metadataType": "ApkMetadata", + "metadata": { + "package": "ca-certificates-bundle", + "originPackage": "ca-certificates", + "maintainer": "Natanael Copa ", + "version": "20191127-r5", + "license": "MPL-2.0 AND MIT", + "architecture": "x86_64", + "url": "https://www.mozilla.org/en-US/about/governance/policies/security-group/certs/", + "description": "Pre generated bundle of Mozilla certificates", + "size": 124473, + "installedSize": 233472, + "pullDependencies": "", + "pullChecksum": "Q1bQ4InxwFIUUNl11tjOwqrqAeAIA=", + "gitCommitOfApkPort": "51999ab9304ec11c68c4ea9e99350ac7570e7f21", + "files": [ + { + "path": "/etc" + }, + { + "path": "/etc/ssl" + }, + { + "path": "/etc/ssl/cert.pem", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q1Nj6gTBdkZpTFW/obJGdpfvK0StA=" + } + }, + { + "path": "/etc/ssl/certs" + }, + { + "path": "/etc/ssl/certs/ca-certificates.crt", + "digest": { + "algorithm": "sha1", + "value": "Q1yxhc/Gjt/YEWliIZBXQz/RBrszU=" + } + } + ] + } + }, + { + "id": "ef75a96d-f7be-45fa-abf0-a66c2779cfee", + "name": "libc-utils", + "version": "0.7.2-r3", + "type": "apk", + "foundBy": "apkdb-cataloger", + "locations": [ + { + "path": "/lib/apk/db/installed", + "layerID": "sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68" + } + ], + "licenses": [ + "BSD-2-Clause", + "AND", + "BSD-3-Clause" + ], + "language": "", + "cpes": [ + "cpe:2.3:a:libc-utils:libc-utils:0.7.2-r3:*:*:*:*:*:*:*", + "cpe:2.3:a:libc-utils:libc_utils:0.7.2-r3:*:*:*:*:*:*:*", + "cpe:2.3:a:libc_utils:libc-utils:0.7.2-r3:*:*:*:*:*:*:*", + "cpe:2.3:a:libc_utils:libc_utils:0.7.2-r3:*:*:*:*:*:*:*", + "cpe:2.3:a:libc:libc-utils:0.7.2-r3:*:*:*:*:*:*:*", + "cpe:2.3:a:libc:libc_utils:0.7.2-r3:*:*:*:*:*:*:*" + ], + "purl": "pkg:alpine/libc-utils@0.7.2-r3?arch=x86_64", + "metadataType": "ApkMetadata", + "metadata": { + "package": "libc-utils", + "originPackage": "libc-dev", + "maintainer": "Natanael Copa ", + "version": "0.7.2-r3", + "license": "BSD-2-Clause AND BSD-3-Clause", + "architecture": "x86_64", + "url": "https://alpinelinux.org", + "description": "Meta package to pull in correct libc", + "size": 1228, + "installedSize": 4096, + "pullDependencies": "musl-utils", + "pullChecksum": "Q1UsLZaz0/Go44hd4Vw49UCt40Vp8=", + "gitCommitOfApkPort": "60424133be2e79bbfeff3d58147a22886f817ce2", + "files": [] + } + }, + { + "id": "bb1fd421-fb64-499b-bc59-fe67b8553c1d", + "name": "libcrypto1.1", + "version": "1.1.1l-r0", + "type": "apk", + "foundBy": "apkdb-cataloger", + "locations": [ + { + "path": "/lib/apk/db/installed", + "layerID": "sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68" + } + ], + "licenses": [ + "OpenSSL" + ], + "language": "", + "cpes": [ + "cpe:2.3:a:libcrypto1.1:libcrypto1.1:1.1.1l-r0:*:*:*:*:*:*:*" + ], + "purl": "pkg:alpine/libcrypto1.1@1.1.1l-r0?arch=x86_64", + "metadataType": "ApkMetadata", + "metadata": { + "package": "libcrypto1.1", + "originPackage": "openssl", + "maintainer": "Timo Teras ", + "version": "1.1.1l-r0", + "license": "OpenSSL", + "architecture": "x86_64", + "url": "https://www.openssl.org/", + "description": "Crypto library from openssl", + "size": 1211998, + "installedSize": 2768896, + "pullDependencies": "so:libc.musl-x86_64.so.1", + "pullChecksum": "Q1Sywj1TEYEdvX0+MuqC+ea+zXxR0=", + "gitCommitOfApkPort": "3ef1965150d5b1de4af88dc51cf407e11b20b848", + "files": [ + { + "path": "/etc" + }, + { + "path": "/etc/ssl" + }, + { + "path": "/etc/ssl/ct_log_list.cnf", + "digest": { + "algorithm": "sha1", + "value": "Q1olh8TpdAi2QnTl4FK3TjdUiSwTo=" + } + }, + { + "path": "/etc/ssl/ct_log_list.cnf.dist", + "digest": { + "algorithm": "sha1", + "value": "Q1olh8TpdAi2QnTl4FK3TjdUiSwTo=" + } + }, + { + "path": "/etc/ssl/openssl.cnf", + "digest": { + "algorithm": "sha1", + "value": "Q1wGuxVEOK9iGLj1i8D3BSBnT7MJA=" + } + }, + { + "path": "/etc/ssl/openssl.cnf.dist", + "digest": { + "algorithm": "sha1", + "value": "Q1wGuxVEOK9iGLj1i8D3BSBnT7MJA=" + } + }, + { + "path": "/etc/ssl/certs" + }, + { + "path": "/etc/ssl/misc" + }, + { + "path": "/etc/ssl/misc/CA.pl", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q1IACevKhK93GYBHp96Ie26jgZ17s=" + } + }, + { + "path": "/etc/ssl/misc/tsget", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q13NVgfr7dQUuGYxur0tNalH6EIjU=" + } + }, + { + "path": "/etc/ssl/misc/tsget.pl", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q1ufKVrFftKLv8K/vullp/2reEDdo=" + } + }, + { + "path": "/etc/ssl/private" + }, + { + "path": "/lib" + }, + { + "path": "/lib/libcrypto.so.1.1", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q1p0rHZXV1yOM1jGs0zuNSpi951t8=" + } + }, + { + "path": "/usr" + }, + { + "path": "/usr/lib" + }, + { + "path": "/usr/lib/libcrypto.so.1.1", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q1T2si+c7ts7sgDxQYve4B3i1Dgo0=" + } + }, + { + "path": "/usr/lib/engines-1.1" + }, + { + "path": "/usr/lib/engines-1.1/afalg.so", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q13GvceRRzjQ6bnB5laIGCkDOHTM0=" + } + }, + { + "path": "/usr/lib/engines-1.1/capi.so", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q1h8YLf+3MhbqdHUhGNEabdH5MooA=" + } + }, + { + "path": "/usr/lib/engines-1.1/padlock.so", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q1TzNLJ3/GEmO9XW9XuZ/Izoy7zPk=" + } + } + ] + } + }, + { + "id": "114e7b49-1001-4d72-9f2e-086e0aa5a8b7", + "name": "libretls", + "version": "3.3.3p1-r2", + "type": "apk", + "foundBy": "apkdb-cataloger", + "locations": [ + { + "path": "/lib/apk/db/installed", + "layerID": "sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68" + } + ], + "licenses": [ + "ISC", + "AND", + "(BSD-3-Clause", + "OR", + "MIT)" + ], + "language": "", + "cpes": [ + "cpe:2.3:a:libretls:libretls:3.3.3p1-r2:*:*:*:*:*:*:*" + ], + "purl": "pkg:alpine/libretls@3.3.3p1-r2?arch=x86_64", + "metadataType": "ApkMetadata", + "metadata": { + "package": "libretls", + "originPackage": "libretls", + "maintainer": "Ariadne Conill ", + "version": "3.3.3p1-r2", + "license": "ISC AND (BSD-3-Clause OR MIT)", + "architecture": "x86_64", + "url": "https://git.causal.agency/libretls/", + "description": "port of libtls from libressl to openssl", + "size": 28872, + "installedSize": 86016, + "pullDependencies": "ca-certificates-bundle so:libc.musl-x86_64.so.1 so:libcrypto.so.1.1 so:libssl.so.1.1", + "pullChecksum": "Q1mKL75PNQ3352oQUtXLS8GfI+37M=", + "gitCommitOfApkPort": "161d02ee8c48f3c3cae1d2b4b5d008fa318da0e1", + "files": [ + { + "path": "/usr" + }, + { + "path": "/usr/lib" + }, + { + "path": "/usr/lib/libtls.so.2", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q1nNEC9T/t6W+Ecm0DxqMUnRvcT6k=" + } + }, + { + "path": "/usr/lib/libtls.so.2.0.3", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q1t1MtXTHs0kMZ9+GCPJLriysmtA8=" + } + } + ] + } + }, + { + "id": "ed01e864-9235-4b28-b69a-4c95a3c0457b", + "name": "libssl1.1", + "version": "1.1.1l-r0", + "type": "apk", + "foundBy": "apkdb-cataloger", + "locations": [ + { + "path": "/lib/apk/db/installed", + "layerID": "sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68" + } + ], + "licenses": [ + "OpenSSL" + ], + "language": "", + "cpes": [ + "cpe:2.3:a:libssl1.1:libssl1.1:1.1.1l-r0:*:*:*:*:*:*:*" + ], + "purl": "pkg:alpine/libssl1.1@1.1.1l-r0?arch=x86_64", + "metadataType": "ApkMetadata", + "metadata": { + "package": "libssl1.1", + "originPackage": "openssl", + "maintainer": "Timo Teras ", + "version": "1.1.1l-r0", + "license": "OpenSSL", + "architecture": "x86_64", + "url": "https://www.openssl.org/", + "description": "SSL shared libraries", + "size": 212918, + "installedSize": 540672, + "pullDependencies": "so:libc.musl-x86_64.so.1 so:libcrypto.so.1.1", + "pullChecksum": "Q1Fgsc0DiapwjaJHbihB9ZrLQhr7Q=", + "gitCommitOfApkPort": "3ef1965150d5b1de4af88dc51cf407e11b20b848", + "files": [ + { + "path": "/lib" + }, + { + "path": "/lib/libssl.so.1.1", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q1qaeh6oI3hVHuFty7JD5ASOu8S8k=" + } + }, + { + "path": "/usr" + }, + { + "path": "/usr/lib" + }, + { + "path": "/usr/lib/libssl.so.1.1", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q18j35pe3yp6HOgMih1wlGP1/mm2c=" + } + } + ] + } + }, + { + "id": "2df82ce2-fc7f-40ce-a39b-6bac545cbba8", + "name": "musl", + "version": "1.2.2-r3", + "type": "apk", + "foundBy": "apkdb-cataloger", + "locations": [ + { + "path": "/lib/apk/db/installed", + "layerID": "sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68" + } + ], + "licenses": [ + "MIT" + ], + "language": "", + "cpes": [ + "cpe:2.3:a:musl:musl:1.2.2-r3:*:*:*:*:*:*:*" + ], + "purl": "pkg:alpine/musl@1.2.2-r3?arch=x86_64", + "metadataType": "ApkMetadata", + "metadata": { + "package": "musl", + "originPackage": "musl", + "maintainer": "Timo Teräs ", + "version": "1.2.2-r3", + "license": "MIT", + "architecture": "x86_64", + "url": "https://musl.libc.org/", + "description": "the musl c library (libc) implementation", + "size": 382765, + "installedSize": 622592, + "pullDependencies": "", + "pullChecksum": "Q1n4yypJ4tpU5g+jxI7oHT0Bm4Xjo=", + "gitCommitOfApkPort": "1e9cba35059e3bb4635f12ee7e10dfe687df31ad", + "files": [ + { + "path": "/lib" + }, + { + "path": "/lib/ld-musl-x86_64.so.1", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q1cZei2MQS2nixN1UwDj0FT9qWO4U=" + } + }, + { + "path": "/lib/libc.musl-x86_64.so.1", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q17yJ3JFNypA4mxhJJr0ou6CzsJVI=" + } + } + ] + } + }, + { + "id": "d8ed04e1-eccd-4441-b937-e960bfffe114", + "name": "musl-utils", + "version": "1.2.2-r3", + "type": "apk", + "foundBy": "apkdb-cataloger", + "locations": [ + { + "path": "/lib/apk/db/installed", + "layerID": "sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68" + } + ], + "licenses": [ + "MIT", + "BSD", + "GPL2+" + ], + "language": "", + "cpes": [ + "cpe:2.3:a:musl-utils:musl-utils:1.2.2-r3:*:*:*:*:*:*:*", + "cpe:2.3:a:musl-utils:musl_utils:1.2.2-r3:*:*:*:*:*:*:*", + "cpe:2.3:a:musl_utils:musl-utils:1.2.2-r3:*:*:*:*:*:*:*", + "cpe:2.3:a:musl_utils:musl_utils:1.2.2-r3:*:*:*:*:*:*:*", + "cpe:2.3:a:musl:musl-utils:1.2.2-r3:*:*:*:*:*:*:*", + "cpe:2.3:a:musl:musl_utils:1.2.2-r3:*:*:*:*:*:*:*" + ], + "purl": "pkg:alpine/musl-utils@1.2.2-r3?arch=x86_64", + "metadataType": "ApkMetadata", + "metadata": { + "package": "musl-utils", + "originPackage": "musl", + "maintainer": "Timo Teräs ", + "version": "1.2.2-r3", + "license": "MIT BSD GPL2+", + "architecture": "x86_64", + "url": "https://musl.libc.org/", + "description": "the musl c library (libc) implementation", + "size": 36662, + "installedSize": 147456, + "pullDependencies": "scanelf so:libc.musl-x86_64.so.1", + "pullChecksum": "Q1WNvvoHVxRySUmjgvqQie71lLRd8=", + "gitCommitOfApkPort": "1e9cba35059e3bb4635f12ee7e10dfe687df31ad", + "files": [ + { + "path": "/sbin" + }, + { + "path": "/sbin/ldconfig", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q1Kja2+POZKxEkUOZqwSjC6kmaED4=" + } + }, + { + "path": "/usr" + }, + { + "path": "/usr/bin" + }, + { + "path": "/usr/bin/getconf", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q1ENetcQcXKEGU2b0TD1S+WDuwAOE=" + } + }, + { + "path": "/usr/bin/getent", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q1AtbzfYTO6GHhaimM7wXWuLGcECI=" + } + }, + { + "path": "/usr/bin/iconv", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q1y+KPYlKOaOdDtMOapc2sTDsPKSU=" + } + }, + { + "path": "/usr/bin/ldd", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q1yFAhGggmL7ERgbIA7KQxyTzf3ks=" + } + } + ] + } + }, + { + "id": "96bf3afe-d927-4daa-91c5-7178e9462ac5", + "name": "scanelf", + "version": "1.3.2-r0", + "type": "apk", + "foundBy": "apkdb-cataloger", + "locations": [ + { + "path": "/lib/apk/db/installed", + "layerID": "sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68" + } + ], + "licenses": [ + "GPL-2.0-only" + ], + "language": "", + "cpes": [ + "cpe:2.3:a:scanelf:scanelf:1.3.2-r0:*:*:*:*:*:*:*" + ], + "purl": "pkg:alpine/scanelf@1.3.2-r0?arch=x86_64", + "metadataType": "ApkMetadata", + "metadata": { + "package": "scanelf", + "originPackage": "pax-utils", + "maintainer": "Natanael Copa ", + "version": "1.3.2-r0", + "license": "GPL-2.0-only", + "architecture": "x86_64", + "url": "https://wiki.gentoo.org/wiki/Hardened/PaX_Utilities", + "description": "Scan ELF binaries for stuff", + "size": 36511, + "installedSize": 94208, + "pullDependencies": "so:libc.musl-x86_64.so.1", + "pullChecksum": "Q1OWKo0fmUbClrhW0CPNjwA8AMLWo=", + "gitCommitOfApkPort": "4c7f72840f5052bc63af1796c3ad0021f97949f2", + "files": [ + { + "path": "/usr" + }, + { + "path": "/usr/bin" + }, + { + "path": "/usr/bin/scanelf", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q1LhDyQrsweSqYTuRcjhG2d0pxhrE=" + } + } + ] + } + }, + { + "id": "562ec670-5625-4c25-b8a7-ebaacf04d06c", + "name": "ssl_client", + "version": "1.33.1-r3", + "type": "apk", + "foundBy": "apkdb-cataloger", + "locations": [ + { + "path": "/lib/apk/db/installed", + "layerID": "sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68" + } + ], + "licenses": [ + "GPL-2.0-only" + ], + "language": "", + "cpes": [ + "cpe:2.3:a:ssl-client:ssl-client:1.33.1-r3:*:*:*:*:*:*:*", + "cpe:2.3:a:ssl-client:ssl_client:1.33.1-r3:*:*:*:*:*:*:*", + "cpe:2.3:a:ssl_client:ssl-client:1.33.1-r3:*:*:*:*:*:*:*", + "cpe:2.3:a:ssl_client:ssl_client:1.33.1-r3:*:*:*:*:*:*:*", + "cpe:2.3:a:ssl:ssl-client:1.33.1-r3:*:*:*:*:*:*:*", + "cpe:2.3:a:ssl:ssl_client:1.33.1-r3:*:*:*:*:*:*:*" + ], + "purl": "pkg:alpine/ssl_client@1.33.1-r3?arch=x86_64", + "metadataType": "ApkMetadata", + "metadata": { + "package": "ssl_client", + "originPackage": "busybox", + "maintainer": "Natanael Copa ", + "version": "1.33.1-r3", + "license": "GPL-2.0-only", + "architecture": "x86_64", + "url": "https://busybox.net/", + "description": "EXternal ssl_client for busybox wget", + "size": 4381, + "installedSize": 28672, + "pullDependencies": "so:libc.musl-x86_64.so.1 so:libtls.so.2", + "pullChecksum": "Q1d7WkMOIKXIk3qxexUxSo60HOofY=", + "gitCommitOfApkPort": "98710256acf90e670e79f32f1f0350b830979d24", + "files": [ + { + "path": "/usr" + }, + { + "path": "/usr/bin" + }, + { + "path": "/usr/bin/ssl_client", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q1gPKQTCzC1isM7R89HFwb+A1XmM4=" + } + } + ] + } + }, + { + "id": "52382e1a-320b-4e62-8103-39baa39d85b3", + "name": "zlib", + "version": "1.2.11-r3", + "type": "apk", + "foundBy": "apkdb-cataloger", + "locations": [ + { + "path": "/lib/apk/db/installed", + "layerID": "sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68" + } + ], + "licenses": [ + "Zlib" + ], + "language": "", + "cpes": [ + "cpe:2.3:a:zlib:zlib:1.2.11-r3:*:*:*:*:*:*:*" + ], + "purl": "pkg:alpine/zlib@1.2.11-r3?arch=x86_64", + "metadataType": "ApkMetadata", + "metadata": { + "package": "zlib", + "originPackage": "zlib", + "maintainer": "Natanael Copa ", + "version": "1.2.11-r3", + "license": "Zlib", + "architecture": "x86_64", + "url": "https://zlib.net/", + "description": "A compression/decompression Library", + "size": 51398, + "installedSize": 110592, + "pullDependencies": "so:libc.musl-x86_64.so.1", + "pullChecksum": "Q1Sc/5gG4ey/zvnyPPQ/Au7bXqc08=", + "gitCommitOfApkPort": "388a4fb3640f8ccbd18e105df3ad741dca4247e1", + "files": [ + { + "path": "/lib" + }, + { + "path": "/lib/libz.so.1", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "777", + "digest": { + "algorithm": "sha1", + "value": "Q1a2H8hP24ryCAGf8fnc1Nha9IIHc=" + } + }, + { + "path": "/lib/libz.so.1.2.11", + "ownerUid": "0", + "ownerGid": "0", + "permissions": "755", + "digest": { + "algorithm": "sha1", + "value": "Q19Iml1N+v08tnk1ZtqGEVIEtIFJI=" + } + } + ] + } + } + ], + "artifactRelationships": [], + "source": { + "type": "image", + "target": { + "userInput": "alpine:latest", + "imageID": "sha256:14119a10abf4669e8cdbdff324a9f9605d99697215a0d21c360fe8dfa8471bab", + "manifestDigest": "sha256:69704ef328d05a9f806b6b8502915e6a0a4faa4d72018dc42343f511490daf8a", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "tags": [], + "imageSize": 5590942, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68", + "size": 5590942 + } + ], + "manifest": "ewogICAic2NoZW1hVmVyc2lvbiI6IDIsCiAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5kaXN0cmlidXRpb24ubWFuaWZlc3QudjIranNvbiIsCiAgICJjb25maWciOiB7CiAgICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRvY2tlci5jb250YWluZXIuaW1hZ2UudjEranNvbiIsCiAgICAgICJzaXplIjogMTQ3MSwKICAgICAgImRpZ2VzdCI6ICJzaGEyNTY6MTQxMTlhMTBhYmY0NjY5ZThjZGJkZmYzMjRhOWY5NjA1ZDk5Njk3MjE1YTBkMjFjMzYwZmU4ZGZhODQ3MWJhYiIKICAgfSwKICAgImxheWVycyI6IFsKICAgICAgewogICAgICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLAogICAgICAgICAic2l6ZSI6IDI4MTQ0NDYsCiAgICAgICAgICJkaWdlc3QiOiAic2hhMjU2OmEwZDBhMGQ0NmY4YjUyNDczOTgyYTNjNDY2MzE4ZjQ3OTc2NzU3NzU1MWE1M2ZmYzkwNzRjOWZhNzAzNTk4MmUiCiAgICAgIH0KICAgXQp9", + "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpbIi9iaW4vc2giXSwiSW1hZ2UiOiJzaGEyNTY6ZDNlMGI2MjU4ZWMyZjcyNWMxOTY2OGYxMWFlNTMyM2MzYjAyNDVlMTk3ZWM0Nzg0MjRlYzZhODc5MzU2OTBlYiIsIlZvbHVtZXMiOm51bGwsIldvcmtpbmdEaXIiOiIiLCJFbnRyeXBvaW50IjpudWxsLCJPbkJ1aWxkIjpudWxsLCJMYWJlbHMiOm51bGx9LCJjb250YWluZXIiOiIzMzAyODljNjQ5ZGI4NmY1ZmIxYWU1YmZlZjE4NTAxMDEyYjU1MGFkYjA2MzhiOTE5M2Q0YTNhNGI2NWEyZjliIiwiY29udGFpbmVyX2NvbmZpZyI6eyJIb3N0bmFtZSI6IjMzMDI4OWM2NDlkYiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpbIi9iaW4vc2giLCItYyIsIiMobm9wKSAiLCJDTUQgW1wiL2Jpbi9zaFwiXSJdLCJJbWFnZSI6InNoYTI1NjpkM2UwYjYyNThlYzJmNzI1YzE5NjY4ZjExYWU1MzIzYzNiMDI0NWUxOTdlYzQ3ODQyNGVjNmE4NzkzNTY5MGViIiwiVm9sdW1lcyI6bnVsbCwiV29ya2luZ0RpciI6IiIsIkVudHJ5cG9pbnQiOm51bGwsIk9uQnVpbGQiOm51bGwsIkxhYmVscyI6e319LCJjcmVhdGVkIjoiMjAyMS0wOC0yN1QxNzoxOTo0NS43NTg2MTE1MjNaIiwiZG9ja2VyX3ZlcnNpb24iOiIyMC4xMC43IiwiaGlzdG9yeSI6W3siY3JlYXRlZCI6IjIwMjEtMDgtMjdUMTc6MTk6NDUuNTUzMDkyMzYzWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBBREQgZmlsZTphYWQ0MjkwZDI3NTgwY2MxYTA5NGZmYWY5OGMzY2EyZmM1ZDY5OWZlNjk1ZGZiOGU2ZTlmYWMyMGYxMTI5NDUwIGluIC8gIn0seyJjcmVhdGVkIjoiMjAyMS0wOC0yN1QxNzoxOTo0NS43NTg2MTE1MjNaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApICBDTUQgW1wiL2Jpbi9zaFwiXSIsImVtcHR5X2xheWVyIjp0cnVlfV0sIm9zIjoibGludXgiLCJyb290ZnMiOnsidHlwZSI6ImxheWVycyIsImRpZmZfaWRzIjpbInNoYTI1NjplMmViMDZkOGFmODIxOGNmZWM4MjEwMTQ3MzU3YTY4YjdlMTNmN2M0ODViOTkxYzI4OGMyZDAxZGMyMjhiYjY4Il19fQ==", + "repoDigests": [ + "index.docker.io/library/alpine@sha256:e1c082e3d3c45cccac829840a25941e679c25d438cc8412c2fa221cf1a824e6a" + ] + } + }, + "distro": { + "name": "alpine", + "version": "3.14.2", + "idLike": "" + }, + "descriptor": { + "name": "syft", + "version": "[not provided]" + }, + "schema": { + "version": "1.1.0", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json" + } +} diff --git a/internal/presenter/packages/cyclonedx_presenter_test.go b/internal/presenter/packages/cyclonedx_presenter_test.go index 4914edbb445..083a72022b9 100644 --- a/internal/presenter/packages/cyclonedx_presenter_test.go +++ b/internal/presenter/packages/cyclonedx_presenter_test.go @@ -4,13 +4,15 @@ import ( "flag" "regexp" "testing" + + "github.com/anchore/syft/internal/formats/common/testutils" ) var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx presenters") func TestCycloneDxDirectoryPresenter(t *testing.T) { - catalog, metadata, _ := presenterDirectoryInput(t) - assertPresenterAgainstGoldenSnapshot(t, + catalog, metadata, _ := testutils.DirectoryInput(t) + testutils.AssertPresenterAgainstGoldenSnapshot(t, NewCycloneDxPresenter(catalog, metadata), *updateCycloneDx, cycloneDxRedactor, @@ -19,8 +21,8 @@ func TestCycloneDxDirectoryPresenter(t *testing.T) { func TestCycloneDxImagePresenter(t *testing.T) { testImage := "image-simple" - catalog, metadata, _ := presenterImageInput(t, testImage) - assertPresenterAgainstGoldenImageSnapshot(t, + catalog, metadata, _ := testutils.ImageInput(t, testImage) + testutils.AssertPresenterAgainstGoldenImageSnapshot(t, NewCycloneDxPresenter(catalog, metadata), testImage, *updateCycloneDx, diff --git a/internal/presenter/packages/json_distribution.go b/internal/presenter/packages/json_distribution.go deleted file mode 100644 index 5ba939c03ec..00000000000 --- a/internal/presenter/packages/json_distribution.go +++ /dev/null @@ -1,23 +0,0 @@ -package packages - -import "github.com/anchore/syft/syft/distro" - -// JSONDistribution provides information about a detected Linux JSONDistribution. -type JSONDistribution struct { - Name string `json:"name"` // Name of the Linux distribution - Version string `json:"version"` // Version of the Linux distribution (major or major.minor version) - IDLike string `json:"idLike"` // the ID_LIKE field found within the /etc/os-release file -} - -// NewJSONDistribution creates a struct with the Linux distribution to be represented in JSON. -func NewJSONDistribution(d *distro.Distro) JSONDistribution { - if d == nil { - return JSONDistribution{} - } - - return JSONDistribution{ - Name: d.Name(), - Version: d.FullVersion(), - IDLike: d.IDLike, - } -} diff --git a/internal/presenter/packages/json_document.go b/internal/presenter/packages/json_document.go deleted file mode 100644 index 399d037ae45..00000000000 --- a/internal/presenter/packages/json_document.go +++ /dev/null @@ -1,62 +0,0 @@ -package packages - -import ( - "fmt" - - "github.com/anchore/syft/internal" - "github.com/anchore/syft/internal/version" - "github.com/anchore/syft/syft/distro" - "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/source" -) - -// JSONDocument represents the syft cataloging findings as a JSON document -type JSONDocument struct { - Artifacts []JSONPackage `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog - ArtifactRelationships []JSONRelationship `json:"artifactRelationships"` - Source JSONSource `json:"source"` // Source represents the original object that was cataloged - Distro JSONDistribution `json:"distro"` // Distro represents the Linux distribution that was detected from the source - Descriptor JSONDescriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft - Schema JSONSchema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape -} - -// NewJSONDocument creates and populates a new JSON document struct from the given cataloging results. -func NewJSONDocument(catalog *pkg.Catalog, srcMetadata source.Metadata, d *distro.Distro, scope source.Scope, configuration interface{}) (JSONDocument, error) { - src, err := NewJSONSource(srcMetadata, scope) - if err != nil { - return JSONDocument{}, err - } - - artifacts, err := NewJSONPackages(catalog) - if err != nil { - return JSONDocument{}, err - } - - return JSONDocument{ - Artifacts: artifacts, - ArtifactRelationships: newJSONRelationships(pkg.NewRelationships(catalog)), - Source: src, - Distro: NewJSONDistribution(d), - Descriptor: JSONDescriptor{ - Name: internal.ApplicationName, - Version: version.FromBuild().Version, - Configuration: configuration, - }, - Schema: JSONSchema{ - Version: internal.JSONSchemaVersion, - URL: fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", internal.JSONSchemaVersion), - }, - }, nil -} - -// JSONDescriptor describes what created the document as well as surrounding metadata -type JSONDescriptor struct { - Name string `json:"name"` - Version string `json:"version"` - Configuration interface{} `json:"configuration,omitempty"` -} - -type JSONSchema struct { - Version string `json:"version"` - URL string `json:"url"` -} diff --git a/internal/presenter/packages/json_package.go b/internal/presenter/packages/json_package.go deleted file mode 100644 index a2ea899dd3e..00000000000 --- a/internal/presenter/packages/json_package.go +++ /dev/null @@ -1,71 +0,0 @@ -package packages - -import ( - "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/source" -) - -// JSONPackage represents a pkg.Package object specialized for JSON marshaling and unmarshaling. -type JSONPackage struct { - ID string `json:"id"` - Name string `json:"name"` - Version string `json:"version"` - Type string `json:"type"` - FoundBy string `json:"foundBy"` - Locations []source.Location `json:"locations"` - Licenses []string `json:"licenses"` - Language string `json:"language"` - CPEs []string `json:"cpes"` - PURL string `json:"purl"` - MetadataType string `json:"metadataType"` - Metadata interface{} `json:"metadata"` -} - -func NewJSONPackages(catalog *pkg.Catalog) ([]JSONPackage, error) { - artifacts := make([]JSONPackage, 0) - if catalog == nil { - return artifacts, nil - } - for _, p := range catalog.Sorted() { - art, err := NewJSONPackage(p) - if err != nil { - return nil, err - } - artifacts = append(artifacts, art) - } - return artifacts, nil -} - -// NewJSONPackage crates a new JSONPackage from the given pkg.Package. -func NewJSONPackage(p *pkg.Package) (JSONPackage, error) { - var cpes = make([]string, len(p.CPEs)) - for i, c := range p.CPEs { - cpes[i] = c.BindToFmtString() - } - - // ensure collections are never nil for presentation reasons - var locations = make([]source.Location, 0) - if p.Locations != nil { - locations = p.Locations - } - - var licenses = make([]string, 0) - if p.Licenses != nil { - licenses = p.Licenses - } - - return JSONPackage{ - ID: string(p.ID), - Name: p.Name, - Version: p.Version, - Type: string(p.Type), - FoundBy: p.FoundBy, - Locations: locations, - Licenses: licenses, - Language: string(p.Language), - CPEs: cpes, - PURL: p.PURL, - MetadataType: string(p.MetadataType), - Metadata: p.Metadata, - }, nil -} diff --git a/internal/presenter/packages/json_presenter.go b/internal/presenter/packages/json_presenter.go deleted file mode 100644 index cbdc4d107d3..00000000000 --- a/internal/presenter/packages/json_presenter.go +++ /dev/null @@ -1,43 +0,0 @@ -package packages - -import ( - "encoding/json" - "io" - - "github.com/anchore/syft/syft/distro" - "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/source" -) - -// JSONPresenter is a JSON presentation object for the syft results -type JSONPresenter struct { - catalog *pkg.Catalog - srcMetadata source.Metadata - distro *distro.Distro - scope source.Scope -} - -// NewJSONPresenter creates a new JSON presenter object for the given cataloging results. -func NewJSONPresenter(catalog *pkg.Catalog, s source.Metadata, d *distro.Distro, scope source.Scope) *JSONPresenter { - return &JSONPresenter{ - catalog: catalog, - srcMetadata: s, - distro: d, - scope: scope, - } -} - -// Present the catalog results to the given writer. -func (pres *JSONPresenter) Present(output io.Writer) error { - // we do not pass in configuration for backwards compatibility - doc, err := NewJSONDocument(pres.catalog, pres.srcMetadata, pres.distro, pres.scope, nil) - if err != nil { - return err - } - - 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/packages/json_presenter_test.go b/internal/presenter/packages/json_presenter_test.go deleted file mode 100644 index 9620c2da725..00000000000 --- a/internal/presenter/packages/json_presenter_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package packages - -import ( - "flag" - "testing" - - "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 TestJSONDirectoryPresenter(t *testing.T) { - catalog, metadata, dist := presenterDirectoryInput(t) - assertPresenterAgainstGoldenSnapshot(t, - NewJSONPresenter(catalog, metadata, dist, source.SquashedScope), - *updateJSONGoldenFiles, - ) - -} - -func TestJSONImagePresenter(t *testing.T) { - testImage := "image-simple" - catalog, metadata, dist := presenterImageInput(t, testImage) - assertPresenterAgainstGoldenImageSnapshot(t, - NewJSONPresenter(catalog, metadata, dist, source.SquashedScope), - testImage, - *updateJSONGoldenFiles, - ) -} diff --git a/internal/presenter/packages/json_relationship.go b/internal/presenter/packages/json_relationship.go deleted file mode 100644 index 326e4a4a570..00000000000 --- a/internal/presenter/packages/json_relationship.go +++ /dev/null @@ -1,23 +0,0 @@ -package packages - -import "github.com/anchore/syft/syft/pkg" - -type JSONRelationship struct { - Parent string `json:"parent"` - Child string `json:"child"` - Type string `json:"type"` - Metadata interface{} `json:"metadata"` -} - -func newJSONRelationships(relationships []pkg.Relationship) []JSONRelationship { - result := make([]JSONRelationship, len(relationships)) - for i, r := range relationships { - result[i] = JSONRelationship{ - Parent: string(r.Parent), - Child: string(r.Child), - Type: string(r.Type), - Metadata: r.Metadata, - } - } - return result -} diff --git a/internal/presenter/packages/json_source.go b/internal/presenter/packages/json_source.go deleted file mode 100644 index d0c2a477f1e..00000000000 --- a/internal/presenter/packages/json_source.go +++ /dev/null @@ -1,39 +0,0 @@ -package packages - -import ( - "fmt" - - "github.com/anchore/syft/syft/source" -) - -// JSONSource object represents the thing that was cataloged -type JSONSource struct { - Type string `json:"type"` - Target interface{} `json:"target"` -} - -type JSONImageSource struct { - source.ImageMetadata - Scope source.Scope `json:"scope"` -} - -// NewJSONSource creates a new source object to be represented into JSON. -func NewJSONSource(src source.Metadata, scope source.Scope) (JSONSource, error) { - switch src.Scheme { - case source.ImageScheme: - return JSONSource{ - Type: "image", - Target: JSONImageSource{ - Scope: scope, - ImageMetadata: src.ImageMetadata, - }, - }, nil - case source.DirectoryScheme: - return JSONSource{ - Type: "directory", - Target: src.Path, - }, nil - default: - return JSONSource{}, fmt.Errorf("unsupported source: %q", src.Scheme) - } -} diff --git a/internal/presenter/packages/spdx_helpers_test.go b/internal/presenter/packages/spdx_helpers_test.go index 985e26fed4c..5002dc62147 100644 --- a/internal/presenter/packages/spdx_helpers_test.go +++ b/internal/presenter/packages/spdx_helpers_test.go @@ -12,7 +12,7 @@ import ( ) func Test_getSPDXExternalRefs(t *testing.T) { - testCPE := must(pkg.NewCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*")) + testCPE := pkg.MustCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*") tests := []struct { name string input pkg.Package diff --git a/internal/presenter/packages/spdx_json_presenter_test.go b/internal/presenter/packages/spdx_json_presenter_test.go index 7d6a7f7381d..98d8377593d 100644 --- a/internal/presenter/packages/spdx_json_presenter_test.go +++ b/internal/presenter/packages/spdx_json_presenter_test.go @@ -4,13 +4,15 @@ import ( "flag" "regexp" "testing" + + "github.com/anchore/syft/internal/formats/common/testutils" ) var updateSpdxJson = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json presenters") func TestSPDXJSONDirectoryPresenter(t *testing.T) { - catalog, metadata, _ := presenterDirectoryInput(t) - assertPresenterAgainstGoldenSnapshot(t, + catalog, metadata, _ := testutils.DirectoryInput(t) + testutils.AssertPresenterAgainstGoldenSnapshot(t, NewSPDXJSONPresenter(catalog, metadata), *updateSpdxJson, spdxJsonRedactor, @@ -19,8 +21,8 @@ func TestSPDXJSONDirectoryPresenter(t *testing.T) { func TestSPDXJSONImagePresenter(t *testing.T) { testImage := "image-simple" - catalog, metadata, _ := presenterImageInput(t, testImage) - assertPresenterAgainstGoldenImageSnapshot(t, + catalog, metadata, _ := testutils.ImageInput(t, testImage) + testutils.AssertPresenterAgainstGoldenImageSnapshot(t, NewSPDXJSONPresenter(catalog, metadata), testImage, *updateSpdxJson, diff --git a/internal/presenter/packages/spdx_tag_value_presenter_test.go b/internal/presenter/packages/spdx_tag_value_presenter_test.go index 8ffff1d6207..95dd4a793b2 100644 --- a/internal/presenter/packages/spdx_tag_value_presenter_test.go +++ b/internal/presenter/packages/spdx_tag_value_presenter_test.go @@ -4,13 +4,15 @@ import ( "flag" "regexp" "testing" + + "github.com/anchore/syft/internal/formats/common/testutils" ) var updateSpdxTagValue = flag.Bool("update-spdx-tv", false, "update the *.golden files for spdx-tv presenters") func TestSPDXTagValueDirectoryPresenter(t *testing.T) { - catalog, metadata, _ := presenterDirectoryInput(t) - assertPresenterAgainstGoldenSnapshot(t, + catalog, metadata, _ := testutils.DirectoryInput(t) + testutils.AssertPresenterAgainstGoldenSnapshot(t, NewSPDXTagValuePresenter(catalog, metadata), *updateSpdxTagValue, spdxTagValueRedactor, @@ -19,8 +21,8 @@ func TestSPDXTagValueDirectoryPresenter(t *testing.T) { func TestSPDXTagValueImagePresenter(t *testing.T) { testImage := "image-simple" - catalog, metadata, _ := presenterImageInput(t, testImage) - assertPresenterAgainstGoldenImageSnapshot(t, + catalog, metadata, _ := testutils.ImageInput(t, testImage) + testutils.AssertPresenterAgainstGoldenImageSnapshot(t, NewSPDXTagValuePresenter(catalog, metadata), testImage, *updateSpdxTagValue, diff --git a/internal/presenter/packages/table_presenter_test.go b/internal/presenter/packages/table_presenter_test.go index 7d59fb1badb..01ebdaca8ae 100644 --- a/internal/presenter/packages/table_presenter_test.go +++ b/internal/presenter/packages/table_presenter_test.go @@ -4,6 +4,8 @@ import ( "flag" "testing" + "github.com/anchore/syft/internal/formats/common/testutils" + "github.com/go-test/deep" ) @@ -11,8 +13,8 @@ var updateTablePresenterGoldenFiles = flag.Bool("update-table", false, "update t func TestTablePresenter(t *testing.T) { testImage := "image-simple" - catalog, _, _ := presenterImageInput(t, testImage) - assertPresenterAgainstGoldenImageSnapshot(t, + catalog, _, _ := testutils.ImageInput(t, testImage) + testutils.AssertPresenterAgainstGoldenImageSnapshot(t, NewTablePresenter(catalog), testImage, *updateTablePresenterGoldenFiles, diff --git a/internal/presenter/packages/test-fixtures/snapshot/TestSPDXJSONDirectoryPresenter.golden b/internal/presenter/packages/test-fixtures/snapshot/TestSPDXJSONDirectoryPresenter.golden index 249517449ad..a9fa1b587b7 100644 --- a/internal/presenter/packages/test-fixtures/snapshot/TestSPDXJSONDirectoryPresenter.golden +++ b/internal/presenter/packages/test-fixtures/snapshot/TestSPDXJSONDirectoryPresenter.golden @@ -3,7 +3,7 @@ "name": "/some/path", "spdxVersion": "SPDX-2.2", "creationInfo": { - "created": "2021-09-16T20:44:35.198887Z", + "created": "2021-10-12T18:40:22.948394Z", "creators": [ "Organization: Anchore, Inc", "Tool: syft-[not provided]" @@ -11,7 +11,7 @@ "licenseListVersion": "3.14" }, "dataLicense": "CC0-1.0", - "documentNamespace": "https://anchore.com/syft/image/", + "documentNamespace": "https:/anchore.com/syft/dir/some/path-98ae71fb-f276-4c5c-acf7-25770bf7bca2", "packages": [ { "SPDXID": "SPDXRef-Package-python-package-1-1.0.1", @@ -32,7 +32,7 @@ ], "filesAnalyzed": false, "hasFiles": [ - "SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9" + "SPDXRef-File-package-1-efae7fecc76ca25da40f79d7ef5b8933510434914835832c7976f3e866aa756a" ], "licenseDeclared": "MIT", "sourceInfo": "acquired package info from installed python package manifest file: /some/path/pkg1", @@ -63,17 +63,17 @@ ], "files": [ { - "SPDXID": "SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9", + "SPDXID": "SPDXRef-File-package-1-efae7fecc76ca25da40f79d7ef5b8933510434914835832c7976f3e866aa756a", "name": "foo", "licenseConcluded": "", - "fileName": "/some/path/pkg1/depedencies/foo" + "fileName": "/some/path/pkg1/dependencies/foo" } ], "relationships": [ { "spdxElementId": "SPDXRef-Package-python-package-1-1.0.1", "relationshipType": "CONTAINS", - "relatedSpdxElement": "SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9" + "relatedSpdxElement": "SPDXRef-File-package-1-efae7fecc76ca25da40f79d7ef5b8933510434914835832c7976f3e866aa756a" } ] } diff --git a/internal/presenter/packages/test-fixtures/snapshot/TestSPDXJSONImagePresenter.golden b/internal/presenter/packages/test-fixtures/snapshot/TestSPDXJSONImagePresenter.golden index 8906ef161ac..5d345603101 100644 --- a/internal/presenter/packages/test-fixtures/snapshot/TestSPDXJSONImagePresenter.golden +++ b/internal/presenter/packages/test-fixtures/snapshot/TestSPDXJSONImagePresenter.golden @@ -3,7 +3,7 @@ "name": "user-image-input", "spdxVersion": "SPDX-2.2", "creationInfo": { - "created": "2021-09-16T20:44:35.203911Z", + "created": "2021-10-12T18:40:22.953633Z", "creators": [ "Organization: Anchore, Inc", "Tool: syft-[not provided]" @@ -11,7 +11,7 @@ "licenseListVersion": "3.14" }, "dataLicense": "CC0-1.0", - "documentNamespace": "https://anchore.com/syft/image/user-image-input", + "documentNamespace": "https:/anchore.com/syft/image/user-image-input-149edbad-3c01-4ee0-b3a0-75232312bf51", "packages": [ { "SPDXID": "SPDXRef-Package-python-package-1-1.0.1", diff --git a/internal/presenter/packages/test-fixtures/snapshot/TestTextImagePresenter.golden b/internal/presenter/packages/test-fixtures/snapshot/TestTextImagePresenter.golden index eb23e8bca73..4ab3a446e0c 100644 --- a/internal/presenter/packages/test-fixtures/snapshot/TestTextImagePresenter.golden +++ b/internal/presenter/packages/test-fixtures/snapshot/TestTextImagePresenter.golden @@ -1,11 +1,11 @@ [Image] Layer: 0 - Digest: sha256:ffb5e9eaa453a002110719d12c294960117ca2903953d1faa40f01dc3f77045c + Digest: sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59 Size: 22 MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip Layer: 1 - Digest: sha256:8463854829fc53d47b9dcdf7ee79fe7eb4ca7933c910f67f8521412f7a2f5c21 + Digest: sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec Size: 16 MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip diff --git a/internal/presenter/packages/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/internal/presenter/packages/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden index 249e2544c8e..a5985f95c4f 100644 Binary files a/internal/presenter/packages/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden and b/internal/presenter/packages/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden differ diff --git a/internal/presenter/packages/text_presenter_test.go b/internal/presenter/packages/text_presenter_test.go index 2a2164285ea..a6533d356af 100644 --- a/internal/presenter/packages/text_presenter_test.go +++ b/internal/presenter/packages/text_presenter_test.go @@ -3,13 +3,15 @@ package packages import ( "flag" "testing" + + "github.com/anchore/syft/internal/formats/common/testutils" ) var updateTextPresenterGoldenFiles = flag.Bool("update-text", false, "update the *.golden files for text presenters") func TestTextDirectoryPresenter(t *testing.T) { - catalog, metadata, _ := presenterDirectoryInput(t) - assertPresenterAgainstGoldenSnapshot(t, + catalog, metadata, _ := testutils.DirectoryInput(t) + testutils.AssertPresenterAgainstGoldenSnapshot(t, NewTextPresenter(catalog, metadata), *updateTextPresenterGoldenFiles, ) @@ -17,8 +19,8 @@ func TestTextDirectoryPresenter(t *testing.T) { func TestTextImagePresenter(t *testing.T) { testImage := "image-simple" - catalog, metadata, _ := presenterImageInput(t, testImage) - assertPresenterAgainstGoldenImageSnapshot(t, + catalog, metadata, _ := testutils.ImageInput(t, testImage) + testutils.AssertPresenterAgainstGoldenImageSnapshot(t, NewTextPresenter(catalog, metadata), testImage, *updateTextPresenterGoldenFiles, diff --git a/internal/presenter/poweruser/json_document.go b/internal/presenter/poweruser/json_document.go index d468bfaf5c5..9811c955717 100644 --- a/internal/presenter/poweruser/json_document.go +++ b/internal/presenter/poweruser/json_document.go @@ -1,7 +1,8 @@ package poweruser import ( - "github.com/anchore/syft/internal/presenter/packages" + "github.com/anchore/syft/internal/formats/syftjson" + "github.com/anchore/syft/internal/formats/syftjson/model" ) type JSONDocument struct { @@ -13,16 +14,11 @@ type JSONDocument struct { FileContents []JSONFileContents `json:"fileContents,omitempty"` // note: must have omitempty FileMetadata []JSONFileMetadata `json:"fileMetadata,omitempty"` // note: must have omitempty Secrets []JSONSecrets `json:"secrets,omitempty"` // note: must have omitempty - packages.JSONDocument + model.Document } // NewJSONDocument creates and populates a new JSON document struct from the given cataloging results. func NewJSONDocument(config JSONDocumentConfig) (JSONDocument, error) { - pkgsDoc, err := packages.NewJSONDocument(config.PackageCatalog, config.SourceMetadata, config.Distro, config.ApplicationConfig.Package.Cataloger.ScopeOpt, config.ApplicationConfig) - if err != nil { - return JSONDocument{}, err - } - fileMetadata, err := NewJSONFileMetadata(config.FileMetadata, config.FileDigests) if err != nil { return JSONDocument{}, err @@ -33,6 +29,6 @@ func NewJSONDocument(config JSONDocumentConfig) (JSONDocument, error) { FileContents: NewJSONFileContents(config.FileContents), FileMetadata: fileMetadata, Secrets: NewJSONSecrets(config.Secrets), - JSONDocument: pkgsDoc, + Document: syftjson.ToFormatModel(config.PackageCatalog, &config.SourceMetadata, config.Distro, config.ApplicationConfig.Package.Cataloger.ScopeOpt, config.ApplicationConfig), }, nil } diff --git a/syft/encode_decode.go b/syft/encode_decode.go new file mode 100644 index 00000000000..654a68715cc --- /dev/null +++ b/syft/encode_decode.go @@ -0,0 +1,48 @@ +package syft + +import ( + "bytes" + "fmt" + "io" + + "github.com/anchore/syft/internal/formats" + "github.com/anchore/syft/syft/distro" + "github.com/anchore/syft/syft/format" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +// Encode takes all SBOM elements and a format option and encodes an SBOM document. +// TODO: encapsulate input data into common sbom document object +func Encode(catalog *pkg.Catalog, metadata *source.Metadata, dist *distro.Distro, scope source.Scope, 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, catalog, dist, metadata, scope); err != nil { + return nil, fmt.Errorf("unable to encode sbom: %w", err) + } + + return buff.Bytes(), nil +} + +// Decode takes a reader for an SBOM and generates all internal SBOM elements. +// TODO: encapsulate return data into common sbom document object +func Decode(reader io.Reader) (*pkg.Catalog, *source.Metadata, *distro.Distro, source.Scope, format.Option, error) { + by, err := io.ReadAll(reader) + if err != nil { + return nil, nil, nil, source.UnknownScope, format.UnknownFormatOption, fmt.Errorf("unable to read sbom: %w", err) + } + + f, err := formats.Identify(by) + if err != nil { + return nil, nil, nil, source.UnknownScope, format.UnknownFormatOption, fmt.Errorf("unable to detect format: %w", err) + } + if f == nil { + return nil, nil, nil, source.UnknownScope, format.UnknownFormatOption, fmt.Errorf("unable to identify format") + } + c, m, d, s, err := f.Decode(bytes.NewReader(by)) + return c, m, d, s, f.Option, err +} diff --git a/syft/encode_decode_test.go b/syft/encode_decode_test.go new file mode 100644 index 00000000000..304291c53cc --- /dev/null +++ b/syft/encode_decode_test.go @@ -0,0 +1,53 @@ +package syft + +import ( + "bytes" + "testing" + + "github.com/go-test/deep" + + "github.com/anchore/syft/syft/format" + "github.com/anchore/syft/syft/source" + "github.com/stretchr/testify/assert" +) + +// TestEncodeDecodeEncodeCycleComparison is testing for differences in how SBOM documents get encoded on multiple cycles. +// By encding and decoding the sbom we can compare the differences between the set of resulting objects. However, +// this requires specific comparisons being done, and select redactions/omissions being made. Additionally, there are +// already unit tests on each format encoder-decoder for properly functioning comparisons in depth, so there is no need +// to do an object-to-object comparison. For this reason this test focuses on a bytes-to-bytes comparison after an +// encode-decode-encode loop which will detect lossy behavior in both directions. +func TestEncodeDecodeEncodeCycleComparison(t *testing.T) { + testImage := "image-simple" + tests := []struct { + format format.Option + }{ + { + format: format.JSONOption, + }, + } + for _, test := range tests { + t.Run(testImage, func(t *testing.T) { + + src, err := source.NewFromDirectory("./test-fixtures/pkgs") + if err != nil { + t.Fatalf("cant get dir") + } + originalCatalog, d, err := CatalogPackages(&src, source.SquashedScope) + + by1, err := Encode(originalCatalog, &src.Metadata, d, source.SquashedScope, test.format) + assert.NoError(t, err) + + newCatalog, newMetadata, newDistro, newScope, newFormat, err := Decode(bytes.NewReader(by1)) + assert.NoError(t, err) + assert.Equal(t, test.format, newFormat) + + by2, err := Encode(newCatalog, newMetadata, newDistro, newScope, test.format) + assert.NoError(t, err) + for _, diff := range deep.Equal(by1, by2) { + t.Errorf(diff) + } + assert.True(t, bytes.Equal(by1, by2)) + }) + } +} diff --git a/syft/format/decoder.go b/syft/format/decoder.go new file mode 100644 index 00000000000..a48c3c98bff --- /dev/null +++ b/syft/format/decoder.go @@ -0,0 +1,13 @@ +package format + +import ( + "io" + + "github.com/anchore/syft/syft/distro" + + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +// Decoder is a function that can convert an SBOM document of a specific format from a reader into Syft native objects. +type Decoder func(reader io.Reader) (*pkg.Catalog, *source.Metadata, *distro.Distro, source.Scope, error) diff --git a/syft/format/encoder.go b/syft/format/encoder.go new file mode 100644 index 00000000000..9f253c342d4 --- /dev/null +++ b/syft/format/encoder.go @@ -0,0 +1,12 @@ +package format + +import ( + "io" + + "github.com/anchore/syft/syft/distro" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +// 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, *pkg.Catalog, *source.Metadata, *distro.Distro, source.Scope) error diff --git a/syft/format/format.go b/syft/format/format.go new file mode 100644 index 00000000000..ad78fb0aa4a --- /dev/null +++ b/syft/format/format.go @@ -0,0 +1,62 @@ +package format + +import ( + "errors" + "io" + + "github.com/anchore/syft/syft/distro" + + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +var ( + ErrEncodingNotSupported = errors.New("encoding not supported") + ErrDecodingNotSupported = errors.New("decoding not supported") + ErrValidationNotSupported = errors.New("validation not supported") +) + +type Format struct { + Option Option + encoder Encoder + decoder Decoder + validator Validator +} + +func NewFormat(option Option, encoder Encoder, decoder Decoder, validator Validator) Format { + return Format{ + Option: option, + encoder: encoder, + decoder: decoder, + validator: validator, + } +} + +func (f Format) Encode(output io.Writer, catalog *pkg.Catalog, d *distro.Distro, metadata *source.Metadata, scope source.Scope) error { + if f.encoder == nil { + return ErrEncodingNotSupported + } + return f.encoder(output, catalog, metadata, d, scope) +} + +func (f Format) Decode(reader io.Reader) (*pkg.Catalog, *source.Metadata, *distro.Distro, source.Scope, error) { + if f.decoder == nil { + return nil, nil, nil, source.UnknownScope, ErrDecodingNotSupported + } + return f.decoder(reader) +} + +func (f Format) Validate(reader io.Reader) error { + if f.validator == nil { + return ErrValidationNotSupported + } + + return f.validator(reader) +} + +func (f Format) Presenter(catalog *pkg.Catalog, metadata *source.Metadata, d *distro.Distro, scope source.Scope) *Presenter { + if f.encoder == nil { + return nil + } + return NewPresenter(f.encoder, catalog, metadata, d, scope) +} diff --git a/syft/format/option.go b/syft/format/option.go new file mode 100644 index 00000000000..09ae7268dae --- /dev/null +++ b/syft/format/option.go @@ -0,0 +1,43 @@ +package format + +import "strings" + +const ( + UnknownFormatOption Option = "UnknownFormatOption" + JSONOption Option = "json" + TextOption Option = "text" + TableOption Option = "table" + CycloneDxOption Option = "cyclonedx" + SPDXTagValueOption Option = "spdx-tag-value" + SPDXJSONOption Option = "spdx-json" +) + +var AllPresenters = []Option{ + JSONOption, + TextOption, + TableOption, + CycloneDxOption, + SPDXTagValueOption, + SPDXJSONOption, +} + +type Option string + +func ParseOption(userStr string) Option { + switch strings.ToLower(userStr) { + case string(JSONOption): + return JSONOption + case string(TextOption): + return TextOption + case string(TableOption): + return TableOption + case string(CycloneDxOption), "cyclone", "cyclone-dx": + return CycloneDxOption + case string(SPDXTagValueOption), "spdx", "spdx-tagvalue", "spdxtagvalue", "spdx-tv", "spdxtv": + return SPDXTagValueOption + case string(SPDXJSONOption), "spdxjson": + return SPDXJSONOption + default: + return UnknownFormatOption + } +} diff --git a/syft/format/presenter.go b/syft/format/presenter.go new file mode 100644 index 00000000000..d57b26a0902 --- /dev/null +++ b/syft/format/presenter.go @@ -0,0 +1,32 @@ +package format + +import ( + "io" + + "github.com/anchore/syft/syft/distro" + + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +type Presenter struct { + catalog *pkg.Catalog + srcMetadata *source.Metadata + distro *distro.Distro + scope source.Scope + encoder Encoder +} + +func NewPresenter(encoder Encoder, catalog *pkg.Catalog, srcMetadata *source.Metadata, d *distro.Distro, scope source.Scope) *Presenter { + return &Presenter{ + catalog: catalog, + srcMetadata: srcMetadata, + distro: d, + encoder: encoder, + scope: scope, + } +} + +func (pres *Presenter) Present(output io.Writer) error { + return pres.encoder(output, pres.catalog, pres.srcMetadata, pres.distro, pres.scope) +} diff --git a/syft/format/validator.go b/syft/format/validator.go new file mode 100644 index 00000000000..6559186b091 --- /dev/null +++ b/syft/format/validator.go @@ -0,0 +1,12 @@ +package format + +import "io" + +// Validator reads the SBOM from the given reader and assesses whether the document conforms to the specific SBOM format. +// The validator should positively confirm if the SBOM is not only the format but also has the minimal set of values +// that the format requires. For example, all syftjson formatted documents have a schema section which should have +// "anchore/syft" within the version --if this isn't found then the validator should raise an error. These active +// assertions protect against "simple" format decoding validations that may lead to false positives (e.g. I decoded +// json successfully therefore this must be the target format, however, all values are their default zero-value and +// really represent a different format that also uses json) +type Validator func(reader io.Reader) error diff --git a/syft/pkg/cpe.go b/syft/pkg/cpe.go index b6f6e221ad7..52a86adce48 100644 --- a/syft/pkg/cpe.go +++ b/syft/pkg/cpe.go @@ -35,6 +35,14 @@ func NewCPE(cpeStr string) (CPE, error) { return *value, nil } +func MustCPE(cpeStr string) CPE { + c, err := NewCPE(cpeStr) + if err != nil { + panic(err) + } + return c +} + func normalizeCpeField(field string) string { // keep dashes and forward slashes unescaped return strings.ReplaceAll(wfn.StripSlashes(field), `\/`, "/") diff --git a/syft/presenter/packages/presenter.go b/syft/presenter/packages/presenter.go index 6d0d594efd4..e6dcc10779b 100644 --- a/syft/presenter/packages/presenter.go +++ b/syft/presenter/packages/presenter.go @@ -5,26 +5,31 @@ a specific Presenter implementation given user configuration. package packages import ( + "github.com/anchore/syft/internal/formats" "github.com/anchore/syft/internal/presenter/packages" + "github.com/anchore/syft/syft/format" "github.com/anchore/syft/syft/presenter" ) // Presenter returns a presenter for images or directories -func Presenter(option PresenterOption, config PresenterConfig) presenter.Presenter { +func Presenter(option format.Option, config PresenterConfig) presenter.Presenter { switch option { - case JSONPresenterOption: - return packages.NewJSONPresenter(config.Catalog, config.SourceMetadata, config.Distro, config.Scope) - case TextPresenterOption: + case format.TextOption: return packages.NewTextPresenter(config.Catalog, config.SourceMetadata) - case TablePresenterOption: + case format.TableOption: return packages.NewTablePresenter(config.Catalog) - case CycloneDxPresenterOption: + case format.CycloneDxOption: return packages.NewCycloneDxPresenter(config.Catalog, config.SourceMetadata) - case SPDXTagValuePresenterOption: + case format.SPDXTagValueOption: return packages.NewSPDXTagValuePresenter(config.Catalog, config.SourceMetadata) - case SPDXJSONPresenterOption: + case format.SPDXJSONOption: return packages.NewSPDXJSONPresenter(config.Catalog, config.SourceMetadata) default: - return nil + // TODO: the final state is that all other cases would be replaced by formats.ByOption (wed remove this function entirely) + f := formats.ByOption(option) + if f == nil { + return nil + } + return f.Presenter(config.Catalog, &config.SourceMetadata, config.Distro, config.Scope) } } diff --git a/syft/presenter/packages/presenter_option.go b/syft/presenter/packages/presenter_option.go deleted file mode 100644 index 7bdcbc07572..00000000000 --- a/syft/presenter/packages/presenter_option.go +++ /dev/null @@ -1,43 +0,0 @@ -package packages - -import "strings" - -const ( - UnknownPresenterOption PresenterOption = "UnknownPresenterOption" - JSONPresenterOption PresenterOption = "json" - TextPresenterOption PresenterOption = "text" - TablePresenterOption PresenterOption = "table" - CycloneDxPresenterOption PresenterOption = "cyclonedx" - SPDXTagValuePresenterOption PresenterOption = "spdx-tag-value" - SPDXJSONPresenterOption PresenterOption = "spdx-json" -) - -var AllPresenters = []PresenterOption{ - JSONPresenterOption, - TextPresenterOption, - TablePresenterOption, - CycloneDxPresenterOption, - SPDXTagValuePresenterOption, - SPDXJSONPresenterOption, -} - -type PresenterOption string - -func ParsePresenterOption(userStr string) PresenterOption { - switch strings.ToLower(userStr) { - case string(JSONPresenterOption): - return JSONPresenterOption - case string(TextPresenterOption): - return TextPresenterOption - case string(TablePresenterOption): - return TablePresenterOption - case string(CycloneDxPresenterOption), "cyclone", "cyclone-dx": - return CycloneDxPresenterOption - case string(SPDXTagValuePresenterOption), "spdx", "spdx-tagvalue", "spdxtagvalue", "spdx-tv": - return SPDXTagValuePresenterOption - case string(SPDXJSONPresenterOption), "spdxjson": - return SPDXJSONPresenterOption - default: - return UnknownPresenterOption - } -} diff --git a/syft/test-fixtures/pkgs/project/package-lock.json b/syft/test-fixtures/pkgs/project/package-lock.json new file mode 100644 index 00000000000..f18505de8a1 --- /dev/null +++ b/syft/test-fixtures/pkgs/project/package-lock.json @@ -0,0 +1,52 @@ +{ + "name": "npm-lock", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "collapse-white-space": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.0.0.tgz", + "integrity": "sha512-eh9krktAIMDL0KHuN7WTBJ/0PMv8KUvfQRBkIlGmW61idRM2DJjgd1qXEPr4wyk2PimZZeNww3RVYo6CMvDGlg==" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "insert-css": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/insert-css/-/insert-css-2.0.0.tgz", + "integrity": "sha1-610Ql7dUL0x56jBg067gfQU4gPQ=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } +} diff --git a/test/integration/package_ownership_relationship_test.go b/test/integration/package_ownership_relationship_test.go index 67dfb3499c3..3d4165b94e3 100644 --- a/test/integration/package_ownership_relationship_test.go +++ b/test/integration/package_ownership_relationship_test.go @@ -3,10 +3,11 @@ package integration import ( "bytes" "encoding/json" - exportedPackages "github.com/anchore/syft/syft/presenter/packages" "testing" - internalPackages "github.com/anchore/syft/internal/presenter/packages" + syftjsonModel "github.com/anchore/syft/internal/formats/syftjson/model" + "github.com/anchore/syft/syft/format" + exportedPackages "github.com/anchore/syft/syft/presenter/packages" ) func TestPackageOwnershipRelationships(t *testing.T) { @@ -24,7 +25,7 @@ func TestPackageOwnershipRelationships(t *testing.T) { t.Run(test.fixture, func(t *testing.T) { catalog, d, src := catalogFixtureImage(t, test.fixture) - p := exportedPackages.Presenter(exportedPackages.JSONPresenterOption, exportedPackages.PresenterConfig{ + p := exportedPackages.Presenter(format.JSONOption, exportedPackages.PresenterConfig{ SourceMetadata: src.Metadata, Catalog: catalog, Distro: d, @@ -39,7 +40,7 @@ func TestPackageOwnershipRelationships(t *testing.T) { t.Fatalf("unable to present: %+v", err) } - var doc internalPackages.JSONDocument + var doc syftjsonModel.Document decoder := json.NewDecoder(output) if err := decoder.Decode(&doc); err != nil { t.Fatalf("unable to decode json doc: %+v", err)