diff --git a/cmd/syft/cli/attest/attest.go b/cmd/syft/cli/attest/attest.go index 9cf64227a84..6c92711db0b 100644 --- a/cmd/syft/cli/attest/attest.go +++ b/cmd/syft/cli/attest/attest.go @@ -56,17 +56,23 @@ var ( intotoJSONDsseType = `application/vnd.in-toto+json` ) +//nolint:funlen func Run(ctx context.Context, app *config.Application, ko sigopts.KeyOpts, args []string) error { // We cannot generate an attestation for more than one output if len(app.Outputs) > 1 { return fmt.Errorf("unable to generate attestation for more than one output") } - // can only be an image for attestation or OCI DIR - userInput := args[0] - si, err := parseImageSource(userInput, app) - if err != nil { - return err + // TODO support input from config somehow + var userInputs []source.Input + for _, userInput := range args { + // could be an image or a directory, with or without a scheme + // can only be an image for attestation or OCI DIR + si, err := parseImageSource(userInput, app) + if err != nil { + return err + } + userInputs = append(userInputs, *si) } output := parseAttestationOutput(app.Outputs) @@ -107,7 +113,7 @@ func Run(ctx context.Context, app *config.Application, ko sigopts.KeyOpts, args subscription := eventBus.Subscribe() return eventloop.EventLoop( - execWorker(app, *si, format, predicateType, sv), + execWorkers(app, userInputs, format, predicateType, sv), eventloop.SetupSignals(), subscription, stereoscope.Cleanup, @@ -151,6 +157,16 @@ func parseImageSource(userInput string, app *config.Application) (s *source.Inpu return si, nil } +func execWorkers(app *config.Application, sourceInputs []source.Input, format sbom.Format, predicateType string, sv *sign.SignerVerifier) <-chan error { + errs := make(chan error) + for _, src := range sourceInputs { + for err := range execWorker(app, src, format, predicateType, sv) { + errs <- err + } + } + return errs +} + func execWorker(app *config.Application, sourceInput source.Input, format sbom.Format, predicateType string, sv *sign.SignerVerifier) <-chan error { errs := make(chan error) go func() { @@ -165,7 +181,7 @@ func execWorker(app *config.Application, sourceInput source.Input, format sbom.F return } - s, err := packages.GenerateSBOM(src, errs, app) + s, err := packages.GenerateSBOM([]source.Source{*src}, errs, app) if err != nil { errs <- err return diff --git a/cmd/syft/cli/commands.go b/cmd/syft/cli/commands.go index 17d38fd8ef4..1aa73dedfce 100644 --- a/cmd/syft/cli/commands.go +++ b/cmd/syft/cli/commands.go @@ -114,7 +114,7 @@ func validateArgs(cmd *cobra.Command, args []string) error { return fmt.Errorf("an image/directory argument is required") } - return cobra.MaximumNArgs(1)(cmd, args) + return nil // cobra.MaximumNArgs(1)(cmd, args) } func checkForApplicationUpdate() { diff --git a/cmd/syft/cli/eventloop/tasks.go b/cmd/syft/cli/eventloop/tasks.go index 4d16f64f610..a692d6f7bee 100644 --- a/cmd/syft/cli/eventloop/tasks.go +++ b/cmd/syft/cli/eventloop/tasks.go @@ -8,11 +8,12 @@ import ( "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) -type Task func(*sbom.Artifacts, *source.Source) ([]artifact.Relationship, error) +type Task func(*sbom.SBOM, *source.Source) error func Tasks(app *config.Application) ([]Task, error) { var tasks []Task @@ -45,16 +46,28 @@ func generateCatalogPackagesTask(app *config.Application) (Task, error) { return nil, nil } - task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) { + task := func(results *sbom.SBOM, src *source.Source) error { packageCatalog, relationships, theDistro, err := syft.CatalogPackages(src, app.ToCatalogerConfig()) if err != nil { - return nil, err + return err + } + + for p := range packageCatalog.Enumerate() { + results.Relationships = append(results.Relationships, artifact.Relationship{ + From: p, + To: &src.Metadata, + Type: artifact.SourceRelationship, + }) + results.Artifacts.PackageCatalog.Add(p) } - results.PackageCatalog = packageCatalog - results.LinuxDistribution = theDistro + if theDistro != nil { + results.Artifacts.LinuxDistributions = []linux.Release{*theDistro} + } + + results.Relationships = append(results.Relationships, relationships...) - return relationships, nil + return nil } return task, nil @@ -67,18 +80,18 @@ func generateCatalogFileMetadataTask(app *config.Application) (Task, error) { metadataCataloger := file.NewMetadataCataloger() - task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) { + task := func(results *sbom.SBOM, src *source.Source) error { resolver, err := src.FileResolver(app.FileMetadata.Cataloger.ScopeOpt) if err != nil { - return nil, err + return err } result, err := metadataCataloger.Catalog(resolver) if err != nil { - return nil, err + return err } - results.FileMetadata = result - return nil, nil + results.Artifacts.FileMetadata = result + return nil } return task, nil @@ -113,18 +126,18 @@ func generateCatalogFileDigestsTask(app *config.Application) (Task, error) { return nil, err } - task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) { + task := func(results *sbom.SBOM, src *source.Source) error { resolver, err := src.FileResolver(app.FileMetadata.Cataloger.ScopeOpt) if err != nil { - return nil, err + return err } result, err := digestsCataloger.Catalog(resolver) if err != nil { - return nil, err + return err } - results.FileDigests = result - return nil, nil + results.Artifacts.FileDigests = result + return nil } return task, nil @@ -145,18 +158,18 @@ func generateCatalogSecretsTask(app *config.Application) (Task, error) { return nil, err } - task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) { + task := func(results *sbom.SBOM, src *source.Source) error { resolver, err := src.FileResolver(app.Secrets.Cataloger.ScopeOpt) if err != nil { - return nil, err + return err } result, err := secretsCataloger.Catalog(resolver) if err != nil { - return nil, err + return err } - results.Secrets = result - return nil, nil + results.Artifacts.Secrets = result + return nil } return task, nil @@ -173,18 +186,18 @@ func generateCatalogFileClassificationsTask(app *config.Application) (Task, erro return nil, err } - task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) { + task := func(results *sbom.SBOM, src *source.Source) error { resolver, err := src.FileResolver(app.FileClassification.Cataloger.ScopeOpt) if err != nil { - return nil, err + return err } result, err := classifierCataloger.Catalog(resolver) if err != nil { - return nil, err + return err } - results.FileClassifications = result - return nil, nil + results.Artifacts.FileClassifications = result + return nil } return task, nil @@ -200,33 +213,27 @@ func generateCatalogContentsTask(app *config.Application) (Task, error) { return nil, err } - task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) { + task := func(results *sbom.SBOM, src *source.Source) error { resolver, err := src.FileResolver(app.FileContents.Cataloger.ScopeOpt) if err != nil { - return nil, err + return err } result, err := contentsCataloger.Catalog(resolver) if err != nil { - return nil, err + return err } - results.FileContents = result - return nil, nil + results.Artifacts.FileContents = result + return nil } return task, nil } -func RunTask(t Task, a *sbom.Artifacts, src *source.Source, c chan<- artifact.Relationship, errs chan<- error) { - defer close(c) - - relationships, err := t(a, src) +func RunTask(t Task, a *sbom.SBOM, src *source.Source, errs chan<- error) { + err := t(a, src) if err != nil { errs <- err return } - - for _, relationship := range relationships { - c <- relationship - } } diff --git a/cmd/syft/cli/packages/packages.go b/cmd/syft/cli/packages/packages.go index 40681b944db..bd91098ffb8 100644 --- a/cmd/syft/cli/packages/packages.go +++ b/cmd/syft/cli/packages/packages.go @@ -19,9 +19,10 @@ import ( "github.com/anchore/syft/internal/ui" "github.com/anchore/syft/internal/version" "github.com/anchore/syft/syft" - "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/event" + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/formats/template" + "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) @@ -43,11 +44,15 @@ func Run(ctx context.Context, app *config.Application, args []string) error { } }() - // could be an image or a directory, with or without a scheme - userInput := args[0] - si, err := source.ParseInput(userInput, app.Platform, true) - if err != nil { - return fmt.Errorf("could not generate source input for packages command: %w", err) + // TODO support input from config somehow + var userInputs []source.Input + for _, userInput := range args { + // could be an image or a directory, with or without a scheme + si, err := source.ParseInput(userInput, app.Platform, true) + if err != nil { + return fmt.Errorf("could not generate source input for packages command: %w", err) + } + userInputs = append(userInputs, *si) } eventBus := partybus.NewBus() @@ -56,7 +61,7 @@ func Run(ctx context.Context, app *config.Application, args []string) error { subscription := eventBus.Subscribe() return eventloop.EventLoop( - execWorker(app, *si, writer), + execWorker(app, userInputs, writer), eventloop.SetupSignals(), subscription, stereoscope.Cleanup, @@ -64,32 +69,44 @@ func Run(ctx context.Context, app *config.Application, args []string) error { ) } -func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <-chan error { +func execWorker(app *config.Application, input []source.Input, writer sbom.Writer) <-chan error { errs := make(chan error) go func() { defer close(errs) - src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions) - if cleanup != nil { - defer cleanup() - } - if err != nil { - errs <- fmt.Errorf("failed to construct source from user input %q: %w", si.UserInput, err) - return + var sources []source.Source + for _, si := range input { + src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions) + if cleanup != nil { + defer cleanup() + } + if err != nil { + errs <- fmt.Errorf("failed to construct source from user input %q: %w", si.UserInput, err) + return + } + sources = append(sources, *src) } - s, err := GenerateSBOM(src, errs, app) + s, err := GenerateSBOM(sources, errs, app) if err != nil { errs <- err return } if s == nil { - errs <- fmt.Errorf("no SBOM produced for %q", si.UserInput) + userInput := "" + for i, in := range input { + if i > 0 { + userInput += " " + } + userInput += in.UserInput + } + errs <- fmt.Errorf("no SBOM produced for %q", userInput) } if app.Anchore.Host != "" { - if err := runPackageSbomUpload(src, *s, app); err != nil { + // FIXME + if err := runPackageSbomUpload(&sources[0], *s, app); err != nil { errs <- err return } @@ -103,14 +120,21 @@ func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <- return errs } -func GenerateSBOM(src *source.Source, errs chan error, app *config.Application) (*sbom.SBOM, error) { +func GenerateSBOM(sources []source.Source, errs chan error, app *config.Application) (*sbom.SBOM, error) { tasks, err := eventloop.Tasks(app) if err != nil { return nil, err } s := sbom.SBOM{ - Source: src.Metadata, + Artifacts: sbom.Artifacts{ + PackageCatalog: pkg.NewCatalog(), + Secrets: map[source.Coordinates][]file.SearchResult{}, + FileDigests: map[source.Coordinates][]file.Digest{}, + FileClassifications: map[source.Coordinates][]file.Classification{}, + FileMetadata: map[source.Coordinates]source.FileMetadata{}, + FileContents: map[source.Coordinates]string{}, + }, Descriptor: sbom.Descriptor{ Name: internal.ApplicationName, Version: version.FromBuild().Version, @@ -118,30 +142,20 @@ func GenerateSBOM(src *source.Source, errs chan error, app *config.Application) }, } - buildRelationships(&s, src, tasks, errs) + buildSBOM(&s, sources, tasks, errs) return &s, nil } -func buildRelationships(s *sbom.SBOM, src *source.Source, tasks []eventloop.Task, errs chan error) { - var relationships []<-chan artifact.Relationship - for _, task := range tasks { - c := make(chan artifact.Relationship) - relationships = append(relationships, c) - go eventloop.RunTask(task, &s.Artifacts, src, c, errs) - } - - s.Relationships = append(s.Relationships, MergeRelationships(relationships...)...) -} - -func MergeRelationships(cs ...<-chan artifact.Relationship) (relationships []artifact.Relationship) { - for _, c := range cs { - for n := range c { - relationships = append(relationships, n) +func buildSBOM(s *sbom.SBOM, sources []source.Source, tasks []eventloop.Task, errs chan error) { + for _, src := range sources { + src := src + meta := &src.Metadata + s.Sources = append(s.Sources, *meta) + for _, task := range tasks { + eventloop.RunTask(task, s, &src, errs) } } - - return relationships } func runPackageSbomUpload(src *source.Source, s sbom.SBOM, app *config.Application) error { diff --git a/cmd/syft/cli/poweruser/poweruser.go b/cmd/syft/cli/poweruser/poweruser.go index 9c773596404..a60022a6d62 100644 --- a/cmd/syft/cli/poweruser/poweruser.go +++ b/cmd/syft/cli/poweruser/poweruser.go @@ -11,7 +11,6 @@ import ( "github.com/anchore/stereoscope" "github.com/anchore/syft/cmd/syft/cli/eventloop" "github.com/anchore/syft/cmd/syft/cli/options" - "github.com/anchore/syft/cmd/syft/cli/packages" "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/bus" "github.com/anchore/syft/internal/config" @@ -19,7 +18,6 @@ import ( "github.com/anchore/syft/internal/ui" "github.com/anchore/syft/internal/version" "github.com/anchore/syft/syft" - "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/event" "github.com/anchore/syft/syft/formats/syftjson" "github.com/anchore/syft/syft/sbom" @@ -90,7 +88,7 @@ func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <- } s := sbom.SBOM{ - Source: src.Metadata, + Sources: []source.Metadata{src.Metadata}, Descriptor: sbom.Descriptor{ Name: internal.ApplicationName, Version: version.FromBuild().Version, @@ -98,16 +96,10 @@ func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <- }, } - var relationships []<-chan artifact.Relationship for _, task := range tasks { - c := make(chan artifact.Relationship) - relationships = append(relationships, c) - - go eventloop.RunTask(task, &s.Artifacts, src, c, errs) + eventloop.RunTask(task, &s, src, errs) } - s.Relationships = append(s.Relationships, packages.MergeRelationships(relationships...)...) - bus.Publish(partybus.Event{ Type: event.Exit, Value: func() error { return writer.Write(s) }, diff --git a/internal/anchore/import.go b/internal/anchore/import.go index e3459a26f08..8549673bfc4 100644 --- a/internal/anchore/import.go +++ b/internal/anchore/import.go @@ -77,13 +77,13 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error { } prog.N++ - manifestDigest, err := importManifest(authedCtx, c.client.ImportsApi, sessionID, cfg.SBOM.Source.ImageMetadata.RawManifest, stage) + manifestDigest, err := importManifest(authedCtx, c.client.ImportsApi, sessionID, cfg.SBOM.Sources[0].ImageMetadata.RawManifest, stage) if err != nil { return fmt.Errorf("failed to import Manifest: %w", err) } prog.N++ - configDigest, err := importConfig(authedCtx, c.client.ImportsApi, sessionID, cfg.SBOM.Source.ImageMetadata.RawConfig, stage) + configDigest, err := importConfig(authedCtx, c.client.ImportsApi, sessionID, cfg.SBOM.Sources[0].ImageMetadata.RawConfig, stage) if err != nil { return fmt.Errorf("failed to import Config: %w", err) } diff --git a/internal/anchore/import_package_sbom.go b/internal/anchore/import_package_sbom.go index cf655197208..e4cd3a52e41 100644 --- a/internal/anchore/import_package_sbom.go +++ b/internal/anchore/import_package_sbom.go @@ -38,18 +38,23 @@ func toImportSBOMModel(s sbom.SBOM) importSBOM { m := syftjson.ToFormatModel(s) var idLike string - if len(m.Distro.IDLike) > 0 { - idLike = m.Distro.IDLike[0] + if len(m.Distros[0].IDLike) > 0 { + idLike = m.Distros[0].IDLike[0] } - var version = m.Distro.VersionID // note: version is intentionally not used as the default + var version = m.Distros[0].VersionID // note: version is intentionally not used as the default if version == "" { - version = m.Distro.Version + version = m.Distros[0].Version } - var name = m.Distro.ID // note: name is intentionally not used as the default + var name = m.Distros[0].ID // note: name is intentionally not used as the default if name == "" { - name = m.Distro.Name + name = m.Distros[0].Name + } + + source := syftjsonModel.Source{} + if len(m.Sources) > 0 { + source = m.Sources[0] } return importSBOM{ @@ -57,7 +62,7 @@ func toImportSBOMModel(s sbom.SBOM) importSBOM { ArtifactRelationships: m.ArtifactRelationships, Files: m.Files, Secrets: m.Secrets, - Source: m.Source, + Source: source, Distro: external.ImportDistribution{ Name: name, Version: version, diff --git a/internal/anchore/import_package_sbom_test.go b/internal/anchore/import_package_sbom_test.go index c1603efd71c..6fcc494b6f2 100644 --- a/internal/anchore/import_package_sbom_test.go +++ b/internal/anchore/import_package_sbom_test.go @@ -94,12 +94,12 @@ func sbomFixture() sbom.SBOM { TopLevelPackages: []string{"top-level"}, }, }), - LinuxDistribution: &linux.Release{ - ID: "centos", + LinuxDistributions: []linux.Release{{ + OSID: "centos", Version: "8.0", VersionID: "8.0", IDLike: []string{"rhel"}, - }, + }}, }, Relationships: []artifact.Relationship{ { @@ -108,7 +108,7 @@ func sbomFixture() sbom.SBOM { Type: artifact.ContainsRelationship, }, }, - Source: source.Metadata{ + Sources: []source.Metadata{{ Scheme: source.ImageScheme, ImageMetadata: source.ImageMetadata{ UserInput: "user-in", @@ -118,7 +118,7 @@ func sbomFixture() sbom.SBOM { MediaType: "mediatype!", Tags: nil, }, - }, + }}, } } @@ -179,7 +179,7 @@ func TestPackageSbomImport(t *testing.T) { // validating that the mock got the right parameters (api.ImportImagePackages) if test.api.sessionID != sessionID { - t.Errorf("different session ID: %s != %s", test.api.sessionID, sessionID) + t.Errorf("different session OSID: %s != %s", test.api.sessionID, sessionID) } for _, d := range deep.Equal(&test.api.model, theModel) { @@ -204,15 +204,15 @@ func Test_packageSbomModel(t *testing.T) { name: "distro: has single distro id-like", sbom: sbom.SBOM{ Artifacts: sbom.Artifacts{ - LinuxDistribution: &linux.Release{ + LinuxDistributions: []linux.Release{{ Name: "centos-name", - ID: "centos-id", + OSID: "centos-id", IDLike: []string{ "centos-id-like-1", }, Version: "version", VersionID: "version-id", - }, + }}, }, }, traits: []modelAssertion{ @@ -223,16 +223,16 @@ func Test_packageSbomModel(t *testing.T) { name: "distro: has multiple distro id-like", sbom: sbom.SBOM{ Artifacts: sbom.Artifacts{ - LinuxDistribution: &linux.Release{ + LinuxDistributions: []linux.Release{{ Name: "centos-name", - ID: "centos-id", + OSID: "centos-id", IDLike: []string{ "centos-id-like-1", "centos-id-like-2", }, Version: "version", VersionID: "version-id", - }, + }}, }, }, traits: []modelAssertion{ @@ -243,13 +243,13 @@ func Test_packageSbomModel(t *testing.T) { name: "distro: has no distro id-like", sbom: sbom.SBOM{ Artifacts: sbom.Artifacts{ - LinuxDistribution: &linux.Release{ + LinuxDistributions: []linux.Release{{ Name: "centos-name", - ID: "centos-id", + OSID: "centos-id", IDLike: []string{}, Version: "version", VersionID: "version-id", - }, + }}, }, }, traits: []modelAssertion{ @@ -260,13 +260,13 @@ func Test_packageSbomModel(t *testing.T) { name: "distro: has no version-id", sbom: sbom.SBOM{ Artifacts: sbom.Artifacts{ - LinuxDistribution: &linux.Release{ + LinuxDistributions: []linux.Release{{ Name: "centos-name", - ID: "centos-id", + OSID: "centos-id", IDLike: []string{}, Version: "version", VersionID: "", - }, + }}, }, }, traits: []modelAssertion{ @@ -277,13 +277,13 @@ func Test_packageSbomModel(t *testing.T) { name: "distro: has no id", sbom: sbom.SBOM{ Artifacts: sbom.Artifacts{ - LinuxDistribution: &linux.Release{ + LinuxDistributions: []linux.Release{{ Name: "centos-name", - ID: "", + OSID: "", IDLike: []string{}, Version: "version", VersionID: "version-id", - }, + }}, }, }, traits: []modelAssertion{ @@ -369,7 +369,7 @@ func Test_packageSbomModel(t *testing.T) { modelBytes, err := json.Marshal(&modelPkg) require.NoError(t, err) - fixPkg := syftjson.ToFormatModel(fix).Source + fixPkg := syftjson.ToFormatModel(fix).Sources[0] fixBytes, err := json.Marshal(&fixPkg) require.NoError(t, err) diff --git a/syft/artifact/relationship.go b/syft/artifact/relationship.go index 4c2fadfab02..1385e07fc26 100644 --- a/syft/artifact/relationship.go +++ b/syft/artifact/relationship.go @@ -10,6 +10,9 @@ const ( // ContainsRelationship (supports any-to-any linkages) is a proxy for the SPDX 2.2 CONTAINS relationship. ContainsRelationship RelationshipType = "contains" + // SourceRelationship (supports many-to-one linkages) + SourceRelationship RelationshipType = "source" + // RuntimeDependencyOfRelationship is a proxy for the SPDX 2.2.1 RUNTIME_DEPENDENCY_OF relationship. RuntimeDependencyOfRelationship RelationshipType = "runtime-dependency-of" diff --git a/syft/formats/common/cyclonedxhelpers/decoder.go b/syft/formats/common/cyclonedxhelpers/decoder.go index 36d99e26014..707afd9c530 100644 --- a/syft/formats/common/cyclonedxhelpers/decoder.go +++ b/syft/formats/common/cyclonedxhelpers/decoder.go @@ -51,22 +51,23 @@ func ToSyftModel(bom *cyclonedx.BOM) (*sbom.SBOM, error) { return nil, fmt.Errorf("no content defined in CycloneDX BOM") } + idMap := make(map[string]interface{}) + s := &sbom.SBOM{ Artifacts: sbom.Artifacts{ - PackageCatalog: pkg.NewCatalog(), - LinuxDistribution: linuxReleaseFromComponents(*bom.Components), + PackageCatalog: pkg.NewCatalog(), + LinuxDistributions: linuxReleasesFromComponents(*bom.Components), }, - Source: extractComponents(bom.Metadata), + Sources: extractSources(bom.Metadata, idMap), Descriptor: extractDescriptor(bom.Metadata), } - idMap := make(map[string]interface{}) - if err := collectBomPackages(bom, s, idMap); err != nil { return nil, err } - collectRelationships(bom, s, idMap) + collectDependencyRelationships(bom, s, idMap) + collectCompositionRelationships(bom, s, idMap) return s, nil } @@ -86,11 +87,11 @@ func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[str case cyclonedx.ComponentTypeOS: case cyclonedx.ComponentTypeContainer: case cyclonedx.ComponentTypeApplication, cyclonedx.ComponentTypeFramework, cyclonedx.ComponentTypeLibrary: - p := decodeComponent(component) - idMap[component.BOMRef] = p + p := *decodeComponent(component) // TODO there must be a better way than needing to call this manually: p.SetID() - s.Artifacts.PackageCatalog.Add(*p) + idMap[component.BOMRef] = p + s.Artifacts.PackageCatalog.Add(p) } if component.Components != nil { @@ -100,14 +101,19 @@ func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[str } } -func linuxReleaseFromComponents(components []cyclonedx.Component) *linux.Release { +func linuxReleasesFromComponents(components []cyclonedx.Component) []linux.Release { + var out []linux.Release for i := range components { component := &components[i] if component.Type == cyclonedx.ComponentTypeOS { - return linuxReleaseFromOSComponent(component) + rel := linuxReleaseFromOSComponent(component) + if rel == nil { + continue + } + out = append(out, *rel) } } - return nil + return out } func linuxReleaseFromOSComponent(component *cyclonedx.Component) *linux.Release { @@ -138,7 +144,7 @@ func linuxReleaseFromOSComponent(component *cyclonedx.Component) *linux.Release CPEName: component.CPE, PrettyName: name, Name: name, - ID: name, + OSID: name, IDLike: []string{name}, Version: version, VersionID: version, @@ -183,23 +189,63 @@ func getPropertyValue(component *cyclonedx.Component, name string) string { return "" } -func collectRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) { +func collectDependencyRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) { if bom.Dependencies == nil { return } for _, d := range *bom.Dependencies { + if d.Dependencies == nil { + continue + } from, fromOk := idMap[d.Ref].(artifact.Identifiable) - if fromOk { - if d.Dependencies == nil { + if !fromOk { + continue + } + for _, t := range *d.Dependencies { + to, toOk := idMap[t.Ref].(artifact.Identifiable) + if !toOk { + continue + } + // FIXME the relationshipType information is lost + relationshipType := artifact.DependencyOfRelationship + if _, ok := to.(*source.Metadata); ok { + relationshipType = artifact.SourceRelationship + } + s.Relationships = append(s.Relationships, artifact.Relationship{ + From: from, + To: to, + Type: relationshipType, + }) + } + } +} + +func collectCompositionRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) { + if bom.Compositions != nil { + for _, c := range *bom.Compositions { + // if c.Aggregate == cyclonedx.CompositionAggregateComplete + if c.Assemblies == nil || c.Dependencies == nil { continue } - for _, t := range *d.Dependencies { - to, toOk := idMap[t.Ref].(artifact.Identifiable) - if toOk { + for _, f := range *c.Assemblies { + from, fromOk := idMap[string(f)].(artifact.Identifiable) + if !fromOk { + continue + } + for _, t := range *c.Dependencies { + to, toOk := idMap[string(t)].(artifact.Identifiable) + if !toOk { + continue + } + // FIXME the relationshipType information is lost + relationshipType := artifact.DependencyOfRelationship + if _, ok := to.(*source.Metadata); ok { + relationshipType = artifact.SourceRelationship + } s.Relationships = append(s.Relationships, artifact.Relationship{ From: from, To: to, - Type: artifact.DependencyOfRelationship, // FIXME this information is lost + Type: relationshipType, }) } } @@ -207,32 +253,47 @@ func collectRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]int } } -func extractComponents(meta *cyclonedx.Metadata) source.Metadata { +func extractSources(meta *cyclonedx.Metadata, idMap map[string]interface{}) []source.Metadata { if meta == nil || meta.Component == nil { - return source.Metadata{} + return nil } - c := meta.Component + return extractComponentSources(*meta.Component, idMap) +} +func extractComponentSources(c cyclonedx.Component, idMap map[string]interface{}) []source.Metadata { image := source.ImageMetadata{ UserInput: c.Name, - ID: c.BOMRef, + ID: c.Description, ManifestDigest: c.Version, } + var sources []source.Metadata + switch c.Type { case cyclonedx.ComponentTypeContainer: - return source.Metadata{ + sources = append(sources, source.Metadata{ Scheme: source.ImageScheme, ImageMetadata: image, - } + }) case cyclonedx.ComponentTypeFile: - return source.Metadata{ + sources = append(sources, source.Metadata{ Scheme: source.FileScheme, // or source.DirectoryScheme Path: c.Name, ImageMetadata: image, + }) + } + + for i := range sources { + idMap[c.BOMRef] = &sources[i] + } + + if c.Components != nil { + for _, child := range *c.Components { + sources = append(sources, extractComponentSources(child, idMap)...) } } - return source.Metadata{} + + return sources } // if there is more than one tool in meta.Tools' list the last item will be used diff --git a/syft/formats/common/cyclonedxhelpers/decoder_test.go b/syft/formats/common/cyclonedxhelpers/decoder_test.go index c2f64775e82..9a03e21d959 100644 --- a/syft/formats/common/cyclonedxhelpers/decoder_test.go +++ b/syft/formats/common/cyclonedxhelpers/decoder_test.go @@ -210,8 +210,8 @@ func Test_decode(t *testing.T) { test: for _, e := range test.expected { if e.os != "" { - assert.Equal(t, e.os, sbom.Artifacts.LinuxDistribution.ID) - assert.Equal(t, e.ver, sbom.Artifacts.LinuxDistribution.VersionID) + assert.Equal(t, e.os, sbom.Artifacts.LinuxDistributions[0].OSID) + assert.Equal(t, e.ver, sbom.Artifacts.LinuxDistributions[0].VersionID) } if e.pkg != "" { for p := range sbom.Artifacts.PackageCatalog.Enumerate() { diff --git a/syft/formats/common/cyclonedxhelpers/format.go b/syft/formats/common/cyclonedxhelpers/format.go index a22b191b2d1..b7b69c6f4da 100644 --- a/syft/formats/common/cyclonedxhelpers/format.go +++ b/syft/formats/common/cyclonedxhelpers/format.go @@ -10,6 +10,7 @@ import ( "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/linux" + "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) @@ -21,14 +22,14 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM { // https://github.com/CycloneDX/specification/blob/master/schema/bom-1.3-strict.schema.json#L36 // "pattern": "^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" cdxBOM.SerialNumber = uuid.New().URN() - cdxBOM.Metadata = toBomDescriptor(internal.ApplicationName, s.Descriptor.Version, s.Source) + cdxBOM.Metadata = toBomDescriptor(internal.ApplicationName, s.Descriptor.Version, s.Sources) packages := s.Artifacts.PackageCatalog.Sorted() components := make([]cyclonedx.Component, len(packages)) for i, p := range packages { components[i] = encodeComponent(p) } - components = append(components, toOSComponent(s.Artifacts.LinuxDistribution)...) + components = append(components, toOSComponent(s.Artifacts.LinuxDistributions)...) cdxBOM.Components = &components dependencies := toDependencies(s.Relationships) @@ -39,67 +40,88 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM { return cdxBOM } -func toOSComponent(distro *linux.Release) []cyclonedx.Component { - if distro == nil { - return []cyclonedx.Component{} - } - eRefs := &[]cyclonedx.ExternalReference{} - if distro.BugReportURL != "" { - *eRefs = append(*eRefs, cyclonedx.ExternalReference{ - URL: distro.BugReportURL, - Type: cyclonedx.ERTypeIssueTracker, - }) - } - if distro.HomeURL != "" { - *eRefs = append(*eRefs, cyclonedx.ExternalReference{ - URL: distro.HomeURL, - Type: cyclonedx.ERTypeWebsite, - }) - } - if distro.SupportURL != "" { - *eRefs = append(*eRefs, cyclonedx.ExternalReference{ - URL: distro.SupportURL, - Type: cyclonedx.ERTypeOther, - Comment: "support", - }) - } - if distro.PrivacyPolicyURL != "" { - *eRefs = append(*eRefs, cyclonedx.ExternalReference{ - URL: distro.PrivacyPolicyURL, - Type: cyclonedx.ERTypeOther, - Comment: "privacyPolicy", - }) - } - if len(*eRefs) == 0 { - eRefs = nil - } - props := encodeProperties(distro, "syft:distro") - var properties *[]cyclonedx.Property - if len(props) > 0 { - properties = &props +func encodeSource(srcMetadata source.Metadata) *cyclonedx.Component { + switch srcMetadata.Scheme { + case source.ImageScheme: + return &cyclonedx.Component{ + BOMRef: getBOMRef(&srcMetadata), + Type: cyclonedx.ComponentTypeContainer, + Name: srcMetadata.ImageMetadata.UserInput, + Version: srcMetadata.ImageMetadata.ManifestDigest, + Description: srcMetadata.ImageMetadata.ID, + } + case source.DirectoryScheme, source.FileScheme: + return &cyclonedx.Component{ + BOMRef: getBOMRef(&srcMetadata), + Type: cyclonedx.ComponentTypeFile, + Name: srcMetadata.Path, + } } - return []cyclonedx.Component{ - { - Type: cyclonedx.ComponentTypeOS, + + return nil +} + +func toOSComponent(distros []linux.Release) []cyclonedx.Component { + var out []cyclonedx.Component + for _, distro := range distros { + eRefs := &[]cyclonedx.ExternalReference{} + if distro.BugReportURL != "" { + *eRefs = append(*eRefs, cyclonedx.ExternalReference{ + URL: distro.BugReportURL, + Type: cyclonedx.ERTypeIssueTracker, + }) + } + if distro.HomeURL != "" { + *eRefs = append(*eRefs, cyclonedx.ExternalReference{ + URL: distro.HomeURL, + Type: cyclonedx.ERTypeWebsite, + }) + } + if distro.SupportURL != "" { + *eRefs = append(*eRefs, cyclonedx.ExternalReference{ + URL: distro.SupportURL, + Type: cyclonedx.ERTypeOther, + Comment: "support", + }) + } + if distro.PrivacyPolicyURL != "" { + *eRefs = append(*eRefs, cyclonedx.ExternalReference{ + URL: distro.PrivacyPolicyURL, + Type: cyclonedx.ERTypeOther, + Comment: "privacyPolicy", + }) + } + if len(*eRefs) == 0 { + eRefs = nil + } + props := encodeProperties(distro, "syft:distro") + var properties *[]cyclonedx.Property + if len(props) > 0 { + properties = &props + } + out = append(out, cyclonedx.Component{ + BOMRef: string(distro.ID()), + Type: cyclonedx.ComponentTypeOS, // FIXME is it idiomatic to be using SWID here for specific name and version information? SWID: &cyclonedx.SWID{ - TagID: distro.ID, - Name: distro.ID, + TagID: distro.OSID, + Name: distro.OSID, Version: distro.VersionID, }, Description: distro.PrettyName, - Name: distro.ID, + Name: distro.OSID, Version: distro.VersionID, // TODO should we add a PURL? CPE: distro.CPEName, ExternalReferences: eRefs, Properties: properties, - }, + }) } + return out } // NewBomDescriptor returns a new BomDescriptor tailored for the current time and "syft" tool details. -func toBomDescriptor(name, version string, srcMetadata source.Metadata) *cyclonedx.Metadata { +func toBomDescriptor(name, version string, srcMetadata []source.Metadata) *cyclonedx.Metadata { return &cyclonedx.Metadata{ Timestamp: time.Now().Format(time.RFC3339), Tools: &[]cyclonedx.Tool{ @@ -128,6 +150,8 @@ func isExpressiblePackageRelationship(ty artifact.RelationshipType) bool { return true case artifact.DependencyOfRelationship: return true + case artifact.SourceRelationship: + return true } return false } @@ -142,39 +166,53 @@ func toDependencies(relationships []artifact.Relationship) []cyclonedx.Dependenc } innerDeps := []cyclonedx.Dependency{} - innerDeps = append(innerDeps, cyclonedx.Dependency{Ref: string(r.From.ID())}) + innerDeps = append(innerDeps, cyclonedx.Dependency{Ref: getBOMRef(r.To)}) result = append(result, cyclonedx.Dependency{ - Ref: string(r.To.ID()), + Ref: getBOMRef(r.From), Dependencies: &innerDeps, }) } return result } -func toBomDescriptorComponent(srcMetadata source.Metadata) *cyclonedx.Component { - switch srcMetadata.Scheme { - case source.ImageScheme: - bomRef, err := artifact.IDByHash(srcMetadata.ImageMetadata.ID) - if err != nil { - log.Warnf("unable to get fingerprint of image metadata=%s: %+v", srcMetadata.ImageMetadata.ID, err) - } - return &cyclonedx.Component{ - BOMRef: string(bomRef), - Type: cyclonedx.ComponentTypeContainer, - Name: srcMetadata.ImageMetadata.UserInput, - Version: srcMetadata.ImageMetadata.ManifestDigest, +func getBOMRef(o artifact.Identifiable) string { + var id string + if p, ok := o.(pkg.Package); ok { + id = deriveBomRef(p) + } else if p, ok := o.(*pkg.Package); ok { + id = deriveBomRef(*p) + } else if meta, ok := o.(*source.Metadata); ok { + switch meta.Scheme { + case source.ImageScheme: + id = meta.ImageMetadata.UserInput + case source.DirectoryScheme, source.FileScheme: + id = meta.Path + default: + id = string(meta.ID()) } - case source.DirectoryScheme, source.FileScheme: - bomRef, err := artifact.IDByHash(srcMetadata.Path) - if err != nil { - log.Warnf("unable to get fingerprint of source metadata path=%s: %+v", srcMetadata.Path, err) - } - return &cyclonedx.Component{ - BOMRef: string(bomRef), - Type: cyclonedx.ComponentTypeFile, - Name: srcMetadata.Path, + } else { + id = string(o.ID()) + } + return id +} + +func toBomDescriptorComponent(srcMetadata []source.Metadata) *cyclonedx.Component { + if len(srcMetadata) == 0 { + return nil + } + + component := encodeSource(srcMetadata[0]) + + if len(srcMetadata) > 1 { + var components []cyclonedx.Component + for i, m := range srcMetadata { + if i == 0 { + continue + } + components = append(components, *encodeSource(m)) } + component.Components = &components } - return nil + return component } diff --git a/syft/formats/common/spdxhelpers/to_syft_model.go b/syft/formats/common/spdxhelpers/to_syft_model.go index 9d7f05b3e00..ef3cb141b4b 100644 --- a/syft/formats/common/spdxhelpers/to_syft_model.go +++ b/syft/formats/common/spdxhelpers/to_syft_model.go @@ -25,18 +25,13 @@ func ToSyftModel(doc *spdx.Document2_2) (*sbom.SBOM, error) { spdxIDMap := make(map[string]interface{}) - src := source.Metadata{Scheme: source.UnknownScheme} - if doc.CreationInfo != nil { - src.Scheme = extractSchemeFromNamespace(doc.CreationInfo.DocumentNamespace) - } - s := &sbom.SBOM{ - Source: src, + Sources: getSources(doc), Artifacts: sbom.Artifacts{ - PackageCatalog: pkg.NewCatalog(), - FileMetadata: map[source.Coordinates]source.FileMetadata{}, - FileDigests: map[source.Coordinates][]file.Digest{}, - LinuxDistribution: findLinuxReleaseByPURL(doc), + PackageCatalog: pkg.NewCatalog(), + FileMetadata: map[source.Coordinates]source.FileMetadata{}, + FileDigests: map[source.Coordinates][]file.Digest{}, + LinuxDistributions: findLinuxReleasesByPURL(doc), }, } @@ -49,6 +44,19 @@ func ToSyftModel(doc *spdx.Document2_2) (*sbom.SBOM, error) { return s, nil } +func getSources(doc *spdx.Document2_2) []source.Metadata { + src := source.Metadata{Scheme: source.UnknownScheme} + if doc.CreationInfo != nil { + src.Scheme = extractSchemeFromNamespace(doc.CreationInfo.DocumentNamespace) + } + + // FIXME upgrade to SPDX 2.3 in order to support PackagePrimaryPurpose + // for _, p := range doc.Packages { + // if p.PackagePrimaryPurpose ... + // } + return []source.Metadata{src} +} + // NOTE(jonas): SPDX doesn't inform what an SBOM is about, // image, directory, for example. This is our best effort to determine // the scheme. Syft-generated SBOMs have in the namespace @@ -73,7 +81,8 @@ func extractSchemeFromNamespace(ns string) source.Scheme { return source.UnknownScheme } -func findLinuxReleaseByPURL(doc *spdx.Document2_2) *linux.Release { +func findLinuxReleasesByPURL(doc *spdx.Document2_2) []linux.Release { + var out []linux.Release for _, p := range doc.Packages { purlValue := findPURLValue(p) if purlValue == "" { @@ -92,18 +101,18 @@ func findLinuxReleaseByPURL(doc *spdx.Document2_2) *linux.Release { if len(parts) > 1 { version = parts[1] } - return &linux.Release{ + out = append(out, linux.Release{ PrettyName: name, Name: name, - ID: name, + OSID: name, IDLike: []string{name}, Version: version, VersionID: version, - } + }) } } - return nil + return out } func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *spdx.Document2_2) { diff --git a/syft/formats/common/testutils/utils.go b/syft/formats/common/testutils/utils.go index 5276c46e5d6..6608eca2946 100644 --- a/syft/formats/common/testutils/utils.go +++ b/syft/formats/common/testutils/utils.go @@ -123,16 +123,16 @@ func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBO return sbom.SBOM{ Artifacts: sbom.Artifacts{ PackageCatalog: catalog, - LinuxDistribution: &linux.Release{ + LinuxDistributions: []linux.Release{{ PrettyName: "debian", Name: "debian", - ID: "debian", + OSID: "debian", IDLike: []string{"like!"}, Version: "1.2.3", VersionID: "1.2.3", - }, + }}, }, - Source: src.Metadata, + Sources: []source.Metadata{src.Metadata}, Descriptor: sbom.Descriptor{ Name: "syft", Version: "v0.42.0-bogus", @@ -204,16 +204,16 @@ func DirectoryInput(t testing.TB) sbom.SBOM { return sbom.SBOM{ Artifacts: sbom.Artifacts{ PackageCatalog: catalog, - LinuxDistribution: &linux.Release{ + LinuxDistributions: []linux.Release{{ PrettyName: "debian", Name: "debian", - ID: "debian", + OSID: "debian", IDLike: []string{"like!"}, Version: "1.2.3", VersionID: "1.2.3", - }, + }}, }, - Source: src.Metadata, + Sources: []source.Metadata{src.Metadata}, Descriptor: sbom.Descriptor{ Name: "syft", Version: "v0.42.0-bogus", diff --git a/syft/formats/cyclonedxjson/decoder_test.go b/syft/formats/cyclonedxjson/decoder_test.go index e561ff13757..65d08e15a9f 100644 --- a/syft/formats/cyclonedxjson/decoder_test.go +++ b/syft/formats/cyclonedxjson/decoder_test.go @@ -49,8 +49,8 @@ func Test_decodeJSON(t *testing.T) { split := strings.SplitN(test.distro, ":", 2) name := split[0] version := split[1] - assert.Equal(t, bom.Artifacts.LinuxDistribution.ID, name) - assert.Equal(t, bom.Artifacts.LinuxDistribution.Version, version) + assert.Equal(t, bom.Artifacts.LinuxDistributions[0].OSID, name) + assert.Equal(t, bom.Artifacts.LinuxDistributions[0].Version, version) pkgs: for _, pkg := range test.packages { diff --git a/syft/formats/cyclonedxxml/decoder_test.go b/syft/formats/cyclonedxxml/decoder_test.go index 7a664333995..b24ff261c72 100644 --- a/syft/formats/cyclonedxxml/decoder_test.go +++ b/syft/formats/cyclonedxxml/decoder_test.go @@ -49,8 +49,8 @@ func Test_decodeXML(t *testing.T) { split := strings.SplitN(test.distro, ":", 2) name := split[0] version := split[1] - assert.Equal(t, bom.Artifacts.LinuxDistribution.ID, name) - assert.Equal(t, bom.Artifacts.LinuxDistribution.Version, version) + assert.Equal(t, bom.Artifacts.LinuxDistributions[0].OSID, name) + assert.Equal(t, bom.Artifacts.LinuxDistributions[0].Version, version) pkgs: for _, pkg := range test.packages { diff --git a/syft/formats/github/encoder.go b/syft/formats/github/encoder.go index 6a2b2b66bed..6ac7c3c7507 100644 --- a/syft/formats/github/encoder.go +++ b/syft/formats/github/encoder.go @@ -40,8 +40,8 @@ func toGithubModel(s *sbom.SBOM) DependencySnapshot { func toSnapshotMetadata(s *sbom.SBOM) Metadata { out := Metadata{} - if s.Artifacts.LinuxDistribution != nil { - d := s.Artifacts.LinuxDistribution + if len(s.Artifacts.LinuxDistributions) > 0 { + d := s.Artifacts.LinuxDistributions[0] qualifiers := packageurl.Qualifiers{} if len(d.IDLike) > 0 { qualifiers = append(qualifiers, packageurl.Qualifier{ @@ -49,7 +49,7 @@ func toSnapshotMetadata(s *sbom.SBOM) Metadata { Value: strings.Join(d.IDLike, ","), }) } - purl := packageurl.NewPackageURL("generic", "", d.ID, d.VersionID, qualifiers, "") + purl := packageurl.NewPackageURL("generic", "", d.OSID, d.VersionID, qualifiers, "") out["syft:distro"] = purl.ToString() } @@ -71,8 +71,15 @@ func isArchive(path string) bool { } // toPath Generates a string representation of the package location, optionally including the layer hash -func toPath(s source.Metadata, p pkg.Package) string { - inputPath := strings.TrimPrefix(s.Path, "./") +func toPath(s *source.Metadata, p pkg.Package) string { + scheme := source.UnknownScheme + imageInput := "" + inputPath := "" + if s != nil { + scheme = s.Scheme + imageInput = s.ImageMetadata.UserInput + inputPath = strings.TrimPrefix(s.Path, "./") + } if inputPath == "." { inputPath = "" } @@ -84,9 +91,9 @@ func toPath(s source.Metadata, p pkg.Package) string { packagePath = location.VirtualPath } packagePath = strings.TrimPrefix(packagePath, "/") - switch s.Scheme { + switch scheme { case source.ImageScheme: - image := strings.ReplaceAll(s.ImageMetadata.UserInput, ":/", "//") + image := strings.ReplaceAll(imageInput, ":/", "//") return fmt.Sprintf("%s:/%s", image, packagePath) case source.FileScheme: if isArchive(inputPath) { @@ -100,7 +107,7 @@ func toPath(s source.Metadata, p pkg.Package) string { return packagePath } } - return fmt.Sprintf("%s%s", inputPath, s.ImageMetadata.UserInput) + return fmt.Sprintf("%s:%s:%s", inputPath, imageInput, p.PURL) } // toGithubManifests manifests, each of which represents a specific location that has dependencies @@ -108,7 +115,8 @@ func toGithubManifests(s *sbom.SBOM) Manifests { manifests := map[string]*Manifest{} for _, p := range s.Artifacts.PackageCatalog.Sorted() { - path := toPath(s.Source, p) + p := p + path := toPath(s.Source(&p), p) manifest, ok := manifests[path] if !ok { manifest = &Manifest{ diff --git a/syft/formats/github/encoder_test.go b/syft/formats/github/encoder_test.go index 3eb58c75571..60d652c031b 100644 --- a/syft/formats/github/encoder_test.go +++ b/syft/formats/github/encoder_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/anchore/packageurl-go" + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" @@ -15,19 +16,19 @@ import ( func Test_toGithubModel(t *testing.T) { s := sbom.SBOM{ - Source: source.Metadata{ + Sources: []source.Metadata{{ Scheme: source.ImageScheme, ImageMetadata: source.ImageMetadata{ UserInput: "ubuntu:18.04", Architecture: "amd64", }, - }, + }}, Artifacts: sbom.Artifacts{ - LinuxDistribution: &linux.Release{ - ID: "ubuntu", + LinuxDistributions: []linux.Release{{ + OSID: "ubuntu", VersionID: "18.04", IDLike: []string{"debian"}, - }, + }}, PackageCatalog: pkg.NewCatalog(), }, } @@ -77,7 +78,13 @@ func Test_toGithubModel(t *testing.T) { nil, "", ).ToString() + p.SetID() s.Artifacts.PackageCatalog.Add(p) + s.Relationships = append(s.Relationships, artifact.Relationship{ + From: p, + To: &s.Sources[0], + Type: artifact.SourceRelationship, + }) } actual := toGithubModel(&s) @@ -140,28 +147,28 @@ func Test_toGithubModel(t *testing.T) { assert.JSONEq(t, string(s1), string(s2)) // Just test the other schemes: - s.Source.Path = "." - s.Source.Scheme = source.DirectoryScheme + s.Sources[0].Path = "." + s.Sources[0].Scheme = source.DirectoryScheme actual = toGithubModel(&s) assert.Equal(t, "etc", actual.Manifests["etc"].Name) - s.Source.Path = "./artifacts" - s.Source.Scheme = source.DirectoryScheme + s.Sources[0].Path = "./artifacts" + s.Sources[0].Scheme = source.DirectoryScheme actual = toGithubModel(&s) assert.Equal(t, "artifacts/etc", actual.Manifests["artifacts/etc"].Name) - s.Source.Path = "/artifacts" - s.Source.Scheme = source.DirectoryScheme + s.Sources[0].Path = "/artifacts" + s.Sources[0].Scheme = source.DirectoryScheme actual = toGithubModel(&s) assert.Equal(t, "/artifacts/etc", actual.Manifests["/artifacts/etc"].Name) - s.Source.Path = "./executable" - s.Source.Scheme = source.FileScheme + s.Sources[0].Path = "./executable" + s.Sources[0].Scheme = source.FileScheme actual = toGithubModel(&s) assert.Equal(t, "executable", actual.Manifests["executable"].Name) - s.Source.Path = "./archive.tar.gz" - s.Source.Scheme = source.FileScheme + s.Sources[0].Path = "./archive.tar.gz" + s.Sources[0].Scheme = source.FileScheme actual = toGithubModel(&s) assert.Equal(t, "archive.tar.gz:/etc", actual.Manifests["archive.tar.gz:/etc"].Name) } diff --git a/syft/formats/spdx22json/to_format_model.go b/syft/formats/spdx22json/to_format_model.go index f55aee9650c..50462b17ba9 100644 --- a/syft/formats/spdx22json/to_format_model.go +++ b/syft/formats/spdx22json/to_format_model.go @@ -20,7 +20,9 @@ import ( // toFormatModel creates and populates a new JSON document struct that follows the SPDX 2.2 spec from the given cataloging results. func toFormatModel(s sbom.SBOM) *model.Document { - name, namespace := spdxhelpers.DocumentNameAndNamespace(s.Source) + name, namespace := spdxhelpers.DocumentNameAndNamespace(s.Sources[0]) + + // FIXME handle multiple sources properly relationships := s.RelationshipsSorted() diff --git a/syft/formats/spdx22tagvalue/encoder_test.go b/syft/formats/spdx22tagvalue/encoder_test.go index f479d2715eb..f9812ff925f 100644 --- a/syft/formats/spdx22tagvalue/encoder_test.go +++ b/syft/formats/spdx22tagvalue/encoder_test.go @@ -50,9 +50,9 @@ func TestSPDXJSONSPDXIDs(t *testing.T) { PackageCatalog: pkg.NewCatalog(pkgs...), }, Relationships: nil, - Source: source.Metadata{ + Sources: []source.Metadata{{ Scheme: source.DirectoryScheme, - }, + }}, Descriptor: sbom.Descriptor{ Name: "syft", Version: "v0.42.0-bogus", diff --git a/syft/formats/spdx22tagvalue/to_format_model.go b/syft/formats/spdx22tagvalue/to_format_model.go index ae4c004776c..cf461aae7ad 100644 --- a/syft/formats/spdx22tagvalue/to_format_model.go +++ b/syft/formats/spdx22tagvalue/to_format_model.go @@ -17,7 +17,9 @@ import ( // //nolint:funlen func toFormatModel(s sbom.SBOM) *spdx.Document2_2 { - name, namespace := spdxhelpers.DocumentNameAndNamespace(s.Source) + name, namespace := spdxhelpers.DocumentNameAndNamespace(s.Sources[0]) + + // FIXME handle multiple sources properly return &spdx.Document2_2{ CreationInfo: &spdx.CreationInfo2_2{ diff --git a/syft/formats/syftjson/encoder_test.go b/syft/formats/syftjson/encoder_test.go index 22d66e1f286..cc8aa59d927 100644 --- a/syft/formats/syftjson/encoder_test.go +++ b/syft/formats/syftjson/encoder_test.go @@ -136,15 +136,15 @@ func TestEncodeFullJSONDocument(t *testing.T) { FileContents: map[source.Coordinates]string{ source.NewLocation("/a/place/a").Coordinates: "the-contents", }, - LinuxDistribution: &linux.Release{ - ID: "redhat", + LinuxDistributions: []linux.Release{{ + OSID: "redhat", Version: "7", VersionID: "7", IDLike: []string{ "rhel", }, }, - }, + }}, Relationships: []artifact.Relationship{ { From: p1, @@ -155,7 +155,7 @@ func TestEncodeFullJSONDocument(t *testing.T) { }, }, }, - Source: source.Metadata{ + Sources: []source.Metadata{{ Scheme: source.ImageScheme, ImageMetadata: source.ImageMetadata{ UserInput: "user-image-input", @@ -182,7 +182,7 @@ func TestEncodeFullJSONDocument(t *testing.T) { RawConfig: []byte("eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZp..."), RepoDigests: []string{}, }, - }, + }}, Descriptor: sbom.Descriptor{ Name: "syft", Version: "v0.42.0-bogus", diff --git a/syft/formats/syftjson/model/document.go b/syft/formats/syftjson/model/document.go index eb41ec79f11..9a9db4972fe 100644 --- a/syft/formats/syftjson/model/document.go +++ b/syft/formats/syftjson/model/document.go @@ -6,8 +6,8 @@ type Document struct { ArtifactRelationships []Relationship `json:"artifactRelationships"` Files []File `json:"files,omitempty"` // note: must have omitempty Secrets []Secrets `json:"secrets,omitempty"` // note: must have omitempty - Source Source `json:"source"` // Source represents the original object that was cataloged - Distro LinuxRelease `json:"distro"` // Distro represents the Linux distribution that was detected from the source + Sources []Source `json:"sources"` // Sources represents the original objects that were cataloged + Distros []LinuxRelease `json:"distros"` // Distros represents the Linux distributions that were detected 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 } diff --git a/syft/formats/syftjson/model/linux_release.go b/syft/formats/syftjson/model/linux_release.go index 3aa79a3d7a9..a5270ca121a 100644 --- a/syft/formats/syftjson/model/linux_release.go +++ b/syft/formats/syftjson/model/linux_release.go @@ -7,6 +7,7 @@ import ( type IDLikes []string type LinuxRelease struct { + UID string `json:"uid,omitempty"` PrettyName string `json:"prettyName,omitempty"` Name string `json:"name,omitempty"` ID string `json:"id,omitempty"` diff --git a/syft/formats/syftjson/model/source.go b/syft/formats/syftjson/model/source.go index 169405220d9..9ef14efebd1 100644 --- a/syft/formats/syftjson/model/source.go +++ b/syft/formats/syftjson/model/source.go @@ -10,12 +10,14 @@ import ( // Source object represents the thing that was cataloged type Source struct { + ID string `json:"id"` Type string `json:"type"` Target interface{} `json:"target"` } // sourceUnpacker is used to unmarshal Source objects type sourceUnpacker struct { + ID string `json:"id"` Type string `json:"type"` Target json.RawMessage `json:"target"` } @@ -27,6 +29,7 @@ func (s *Source) UnmarshalJSON(b []byte) error { return err } + s.ID = unpacker.ID s.Type = unpacker.Type switch s.Type { diff --git a/syft/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden b/syft/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden index f7118ea6d23..fa84f318f19 100644 --- a/syft/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden +++ b/syft/formats/syftjson/test-fixtures/snapshot/TestDirectoryEncoder.golden @@ -66,20 +66,24 @@ } ], "artifactRelationships": [], - "source": { - "type": "directory", - "target": "/some/path" - }, - "distro": { - "prettyName": "debian", - "name": "debian", - "id": "debian", - "idLike": [ - "like!" - ], - "version": "1.2.3", - "versionID": "1.2.3" - }, + "sources": [ + { + "type": "directory", + "target": "/some/path" + } + ], + "distros": [ + { + "prettyName": "debian", + "name": "debian", + "id": "debian", + "idLike": [ + "like!" + ], + "version": "1.2.3", + "versionID": "1.2.3" + } + ], "descriptor": { "name": "syft", "version": "v0.42.0-bogus", diff --git a/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden b/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden index 664b82f9cb5..19ebdad5205 100644 --- a/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden +++ b/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden @@ -138,44 +138,48 @@ ] } ], - "source": { - "type": "image", - "target": { - "userInput": "user-image-input", - "imageID": "sha256:c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0", - "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:3de16c5b8659a2e8d888b8ded8427be7a5686a3c8c4e4dd30de20f362827285b", - "size": 22 - }, - { - "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", - "digest": "sha256:366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703", - "size": 16 - } + "sources": [ + { + "type": "image", + "target": { + "userInput": "user-image-input", + "imageID": "sha256:c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0", + "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:3de16c5b8659a2e8d888b8ded8427be7a5686a3c8c4e4dd30de20f362827285b", + "size": 22 + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703", + "size": 16 + } + ], + "manifest": "ZXlKelkyaGxiV0ZXWlhKemFXOXVJam95TENKdFpXUnBZVlI1Y0dVaU9pSmguLi4=", + "config": "ZXlKaGNtTm9hWFJsWTNSMWNtVWlPaUpoYldRMk5DSXNJbU52Ym1acC4uLg==", + "repoDigests": [], + "architecture": "", + "os": "" + } + } + ], + "distros": [ + { + "id": "redhat", + "idLike": [ + "rhel" ], - "manifest": "ZXlKelkyaGxiV0ZXWlhKemFXOXVJam95TENKdFpXUnBZVlI1Y0dVaU9pSmguLi4=", - "config": "ZXlKaGNtTm9hWFJsWTNSMWNtVWlPaUpoYldRMk5DSXNJbU52Ym1acC4uLg==", - "repoDigests": [], - "architecture": "", - "os": "" + "version": "7", + "versionID": "7" } - }, - "distro": { - "id": "redhat", - "idLike": [ - "rhel" - ], - "version": "7", - "versionID": "7" - }, + ], "descriptor": { "name": "syft", "version": "v0.42.0-bogus", diff --git a/syft/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden b/syft/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden index a7dda4af6cb..a8aa02ad3a3 100644 --- a/syft/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden +++ b/syft/formats/syftjson/test-fixtures/snapshot/TestImageEncoder.golden @@ -63,46 +63,50 @@ } ], "artifactRelationships": [], - "source": { - "type": "image", - "target": { - "userInput": "user-image-input", - "imageID": "sha256:5dd5f5f4247e4e946f555f0de7681a631a5240b614e52717d0aed04808e8c65f", - "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:7ef28e9c2d56471ee090b578a678bdf28c3b5a311ca7b2e28c2a4185e5bb34c0", - "size": 22 - }, - { - "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", - "digest": "sha256:86da8aee621161bea2efaf27a2709ddab5e7d44e30ecdfda728b02c03a28fd98", - "size": 16 - } + "sources": [ + { + "type": "image", + "target": { + "userInput": "user-image-input", + "imageID": "sha256:5dd5f5f4247e4e946f555f0de7681a631a5240b614e52717d0aed04808e8c65f", + "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:7ef28e9c2d56471ee090b578a678bdf28c3b5a311ca7b2e28c2a4185e5bb34c0", + "size": 22 + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:86da8aee621161bea2efaf27a2709ddab5e7d44e30ecdfda728b02c03a28fd98", + "size": 16 + } + ], + "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NzMsImRpZ2VzdCI6InNoYTI1Njo1ZGQ1ZjVmNDI0N2U0ZTk0NmY1NTVmMGRlNzY4MWE2MzFhNTI0MGI2MTRlNTI3MTdkMGFlZDA0ODA4ZThjNjVmIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1Njo3ZWYyOGU5YzJkNTY0NzFlZTA5MGI1NzhhNjc4YmRmMjhjM2I1YTMxMWNhN2IyZTI4YzJhNDE4NWU1YmIzNGMwIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2Ojg2ZGE4YWVlNjIxMTYxYmVhMmVmYWYyN2EyNzA5ZGRhYjVlN2Q0NGUzMGVjZGZkYTcyOGIwMmMwM2EyOGZkOTgifV19", + "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjItMDYtMDJUMTQ6MzQ6MzQuNzE5MTM1MTc0WiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIyLTA2LTAyVDE0OjM0OjM0LjY4NjkzMzI2M1oiLCJjcmVhdGVkX2J5IjoiQUREIGZpbGUtMS50eHQgL3NvbWVmaWxlLTEudHh0ICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9LHsiY3JlYXRlZCI6IjIwMjItMDYtMDJUMTQ6MzQ6MzQuNzE5MTM1MTc0WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6N2VmMjhlOWMyZDU2NDcxZWUwOTBiNTc4YTY3OGJkZjI4YzNiNWEzMTFjYTdiMmUyOGMyYTQxODVlNWJiMzRjMCIsInNoYTI1Njo4NmRhOGFlZTYyMTE2MWJlYTJlZmFmMjdhMjcwOWRkYWI1ZTdkNDRlMzBlY2RmZGE3MjhiMDJjMDNhMjhmZDk4Il19fQ==", + "repoDigests": [], + "architecture": "", + "os": "" + } + } + ], + "distros": [ + { + "prettyName": "debian", + "name": "debian", + "id": "debian", + "idLike": [ + "like!" ], - "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NzMsImRpZ2VzdCI6InNoYTI1Njo1ZGQ1ZjVmNDI0N2U0ZTk0NmY1NTVmMGRlNzY4MWE2MzFhNTI0MGI2MTRlNTI3MTdkMGFlZDA0ODA4ZThjNjVmIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1Njo3ZWYyOGU5YzJkNTY0NzFlZTA5MGI1NzhhNjc4YmRmMjhjM2I1YTMxMWNhN2IyZTI4YzJhNDE4NWU1YmIzNGMwIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2Ojg2ZGE4YWVlNjIxMTYxYmVhMmVmYWYyN2EyNzA5ZGRhYjVlN2Q0NGUzMGVjZGZkYTcyOGIwMmMwM2EyOGZkOTgifV19", - "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjItMDYtMDJUMTQ6MzQ6MzQuNzE5MTM1MTc0WiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIyLTA2LTAyVDE0OjM0OjM0LjY4NjkzMzI2M1oiLCJjcmVhdGVkX2J5IjoiQUREIGZpbGUtMS50eHQgL3NvbWVmaWxlLTEudHh0ICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9LHsiY3JlYXRlZCI6IjIwMjItMDYtMDJUMTQ6MzQ6MzQuNzE5MTM1MTc0WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6N2VmMjhlOWMyZDU2NDcxZWUwOTBiNTc4YTY3OGJkZjI4YzNiNWEzMTFjYTdiMmUyOGMyYTQxODVlNWJiMzRjMCIsInNoYTI1Njo4NmRhOGFlZTYyMTE2MWJlYTJlZmFmMjdhMjcwOWRkYWI1ZTdkNDRlMzBlY2RmZGE3MjhiMDJjMDNhMjhmZDk4Il19fQ==", - "repoDigests": [], - "architecture": "", - "os": "" + "version": "1.2.3", + "versionID": "1.2.3" } - }, - "distro": { - "prettyName": "debian", - "name": "debian", - "id": "debian", - "idLike": [ - "like!" - ], - "version": "1.2.3", - "versionID": "1.2.3" - }, + ], "descriptor": { "name": "syft", "version": "v0.42.0-bogus", diff --git a/syft/formats/syftjson/to_format_model.go b/syft/formats/syftjson/to_format_model.go index 9d4f4bfd492..e21e534fcb6 100644 --- a/syft/formats/syftjson/to_format_model.go +++ b/syft/formats/syftjson/to_format_model.go @@ -20,7 +20,7 @@ import ( // note: this is needed for anchore import functionality // TODO: unexport this when/if anchore import functionality is removed func ToFormatModel(s sbom.SBOM) model.Document { - src, err := toSourceModel(s.Source) + src, err := toSourcesModel(s.Sources) if err != nil { log.Warnf("unable to create syft-json source object: %+v", err) } @@ -30,8 +30,8 @@ func ToFormatModel(s sbom.SBOM) model.Document { ArtifactRelationships: toRelationshipModel(s.Relationships), Files: toFile(s), Secrets: toSecrets(s.Artifacts.Secrets), - Source: src, - Distro: toLinuxReleaser(s.Artifacts.LinuxDistribution), + Sources: src, + Distros: toLinuxReleases(s.Artifacts.LinuxDistributions), Descriptor: toDescriptor(s.Descriptor), Schema: model.Schema{ Version: internal.JSONSchemaVersion, @@ -40,29 +40,31 @@ func ToFormatModel(s sbom.SBOM) model.Document { } } -func toLinuxReleaser(d *linux.Release) model.LinuxRelease { - if d == nil { - return model.LinuxRelease{} - } - return model.LinuxRelease{ - PrettyName: d.PrettyName, - Name: d.Name, - ID: d.ID, - IDLike: d.IDLike, - Version: d.Version, - VersionID: d.VersionID, - VersionCodename: d.VersionCodename, - BuildID: d.BuildID, - ImageID: d.ImageID, - ImageVersion: d.ImageVersion, - Variant: d.Variant, - VariantID: d.VariantID, - HomeURL: d.HomeURL, - SupportURL: d.SupportURL, - BugReportURL: d.BugReportURL, - PrivacyPolicyURL: d.PrivacyPolicyURL, - CPEName: d.CPEName, +func toLinuxReleases(releases []linux.Release) []model.LinuxRelease { + var out []model.LinuxRelease + for _, d := range releases { + out = append(out, model.LinuxRelease{ + UID: string(d.ID()), + PrettyName: d.PrettyName, + Name: d.Name, + ID: d.OSID, + IDLike: d.IDLike, + Version: d.Version, + VersionID: d.VersionID, + VersionCodename: d.VersionCodename, + BuildID: d.BuildID, + ImageID: d.ImageID, + ImageVersion: d.ImageVersion, + Variant: d.Variant, + VariantID: d.VariantID, + HomeURL: d.HomeURL, + SupportURL: d.SupportURL, + BugReportURL: d.BugReportURL, + PrivacyPolicyURL: d.PrivacyPolicyURL, + CPEName: d.CPEName, + }) } + return out } func toDescriptor(d sbom.Descriptor) model.Descriptor { @@ -223,6 +225,17 @@ func toRelationshipModel(relationships []artifact.Relationship) []model.Relation return result } +func toSourcesModel(sources []source.Metadata) (out []model.Source, err error) { + out = make([]model.Source, len(sources)) + for i, s := range sources { + out[i], err = toSourceModel(s) + if err != nil { + return nil, err + } + } + return out, nil +} + // toSourceModel creates a new source object to be represented into JSON. func toSourceModel(src source.Metadata) (model.Source, error) { switch src.Scheme { @@ -236,16 +249,19 @@ func toSourceModel(src source.Metadata) (model.Source, error) { metadata.Tags = []string{} } return model.Source{ + ID: string(src.ID()), Type: "image", Target: metadata, }, nil case source.DirectoryScheme: return model.Source{ + ID: string(src.ID()), Type: "directory", Target: src.Path, }, nil case source.FileScheme: return model.Source{ + ID: string(src.ID()), Type: "file", Target: src.Path, }, nil diff --git a/syft/formats/syftjson/to_syft_model.go b/syft/formats/syftjson/to_syft_model.go index 8a97ae68fad..239942d6526 100644 --- a/syft/formats/syftjson/to_syft_model.go +++ b/syft/formats/syftjson/to_syft_model.go @@ -1,8 +1,6 @@ package syftjson import ( - "github.com/google/go-cmp/cmp" - "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/formats/syftjson/model" @@ -13,47 +11,57 @@ import ( ) func toSyftModel(doc model.Document) (*sbom.SBOM, error) { + // the idAliases map takes into account changes between the ID in the document and the ID of an object idAliases := make(map[string]string) catalog := toSyftCatalog(doc.Artifacts, idAliases) - return &sbom.SBOM{ + s := &sbom.SBOM{ + Descriptor: toSyftDescriptor(doc.Descriptor), + Sources: toSyftSourcesData(doc.Sources, idAliases), Artifacts: sbom.Artifacts{ - PackageCatalog: catalog, - LinuxDistribution: toSyftLinuxRelease(doc.Distro), + PackageCatalog: catalog, + LinuxDistributions: toSyftLinuxReleases(doc.Distros, idAliases), }, - Source: *toSyftSourceData(doc.Source), - Descriptor: toSyftDescriptor(doc.Descriptor), - Relationships: toSyftRelationships(&doc, catalog, doc.ArtifactRelationships, idAliases), - }, nil + } + + s.Relationships = toSyftRelationships(s, &doc, catalog, doc.ArtifactRelationships, idAliases) + + return s, nil } -func toSyftLinuxRelease(d model.LinuxRelease) *linux.Release { - if cmp.Equal(d, model.LinuxRelease{}) { - return nil - } - return &linux.Release{ - PrettyName: d.PrettyName, - Name: d.Name, - ID: d.ID, - IDLike: d.IDLike, - Version: d.Version, - VersionID: d.VersionID, - VersionCodename: d.VersionCodename, - BuildID: d.BuildID, - ImageID: d.ImageID, - ImageVersion: d.ImageVersion, - Variant: d.Variant, - VariantID: d.VariantID, - HomeURL: d.HomeURL, - SupportURL: d.SupportURL, - BugReportURL: d.BugReportURL, - PrivacyPolicyURL: d.PrivacyPolicyURL, - CPEName: d.CPEName, +func toSyftLinuxReleases(releases []model.LinuxRelease, idAliases map[string]string) []linux.Release { + var out []linux.Release + for _, d := range releases { + r := linux.Release{ + PrettyName: d.PrettyName, + Name: d.Name, + OSID: d.ID, + IDLike: d.IDLike, + Version: d.Version, + VersionID: d.VersionID, + VersionCodename: d.VersionCodename, + BuildID: d.BuildID, + ImageID: d.ImageID, + ImageVersion: d.ImageVersion, + Variant: d.Variant, + VariantID: d.VariantID, + HomeURL: d.HomeURL, + SupportURL: d.SupportURL, + BugReportURL: d.BugReportURL, + PrivacyPolicyURL: d.PrivacyPolicyURL, + CPEName: d.CPEName, + } + + idAliases[d.UID] = string(r.ID()) + + out = append(out, r) } + return out } -func toSyftRelationships(doc *model.Document, catalog *pkg.Catalog, relationships []model.Relationship, idAliases map[string]string) []artifact.Relationship { +func toSyftRelationships(s *sbom.SBOM, doc *model.Document, catalog *pkg.Catalog, relationships []model.Relationship, idAliases map[string]string) []artifact.Relationship { + // the idMap takes IDs and maps them to Syft objects for appropriate relationships idMap := make(map[string]interface{}) for _, p := range catalog.Sorted() { @@ -68,11 +76,18 @@ func toSyftRelationships(doc *model.Document, catalog *pkg.Catalog, relationship idMap[f.ID] = f.Location } + for i := range s.Sources { + src := &s.Sources[i] + idMap[string(src.ID())] = src + } + var out []artifact.Relationship for _, r := range relationships { syftRelationship := toSyftRelationship(idMap, r, idAliases) if syftRelationship != nil { out = append(out, *syftRelationship) + } else { + log.Debugf("nil relationship from: %s to: %s", r.Parent, r.Child) } } return out @@ -99,9 +114,9 @@ func toSyftRelationship(idMap map[string]interface{}, relationship model.Relatio typ := artifact.RelationshipType(relationship.Type) switch typ { - case artifact.OwnershipByFileOverlapRelationship: - fallthrough - case artifact.ContainsRelationship: + case artifact.OwnershipByFileOverlapRelationship, + artifact.ContainsRelationship, + artifact.SourceRelationship: default: log.Warnf("unknown relationship type: %s", typ) return nil @@ -122,25 +137,37 @@ func toSyftDescriptor(d model.Descriptor) sbom.Descriptor { } } -func toSyftSourceData(s model.Source) *source.Metadata { +func toSyftSourcesData(sources []model.Source, idAliases map[string]string) []source.Metadata { + out := make([]source.Metadata, len(sources)) + for i, s := range sources { + out[i] = *toSyftSourceData(s, idAliases) + } + return out +} + +func toSyftSourceData(s model.Source, idAliases map[string]string) *source.Metadata { + var out *source.Metadata switch s.Type { case "directory": - return &source.Metadata{ + out = &source.Metadata{ Scheme: source.DirectoryScheme, Path: s.Target.(string), } case "file": - return &source.Metadata{ + out = &source.Metadata{ Scheme: source.FileScheme, Path: s.Target.(string), } case "image": - return &source.Metadata{ + out = &source.Metadata{ Scheme: source.ImageScheme, ImageMetadata: s.Target.(source.ImageMetadata), } } - return nil + if out != nil { + idAliases[s.ID] = string(out.ID()) + } + return out } func toSyftCatalog(pkgs []model.Package, idAliases map[string]string) *pkg.Catalog { diff --git a/syft/formats/syftjson/to_syft_model_test.go b/syft/formats/syftjson/to_syft_model_test.go index 1e7b86c3d24..8bae58d327a 100644 --- a/syft/formats/syftjson/to_syft_model_test.go +++ b/syft/formats/syftjson/to_syft_model_test.go @@ -84,10 +84,10 @@ func Test_toSyftSourceData(t *testing.T) { func Test_idsHaveChanged(t *testing.T) { s, err := toSyftModel(model.Document{ - Source: model.Source{ + Sources: []model.Source{{ Type: "file", Target: "some/path", - }, + }}, Artifacts: []model.Package{ { PackageBasicData: model.PackageBasicData{ diff --git a/syft/formats/text/encoder.go b/syft/formats/text/encoder.go index 49619346e8b..88e84b4d8b3 100644 --- a/syft/formats/text/encoder.go +++ b/syft/formats/text/encoder.go @@ -14,22 +14,24 @@ func encoder(output io.Writer, s sbom.SBOM) error { w := new(tabwriter.Writer) w.Init(output, 0, 8, 0, '\t', tabwriter.AlignRight) - switch s.Source.Scheme { - case source.DirectoryScheme, source.FileScheme: - fmt.Fprintf(w, "[Path: %s]\n", s.Source.Path) - case source.ImageScheme: - fmt.Fprintln(w, "[Image]") - - for idx, l := range s.Source.ImageMetadata.Layers { - fmt.Fprintln(w, " Layer:\t", idx) - fmt.Fprintln(w, " Digest:\t", l.Digest) - fmt.Fprintln(w, " Size:\t", l.Size) - fmt.Fprintln(w, " MediaType:\t", l.MediaType) - fmt.Fprintln(w) - w.Flush() + for _, src := range s.Sources { + switch src.Scheme { + case source.DirectoryScheme, source.FileScheme: + fmt.Fprintf(w, "[Path: %s]\n", src.Path) + case source.ImageScheme: + fmt.Fprintln(w, "[Image]") + + for idx, l := range src.ImageMetadata.Layers { + fmt.Fprintln(w, " Layer:\t", idx) + fmt.Fprintln(w, " Digest:\t", l.Digest) + fmt.Fprintln(w, " Size:\t", l.Size) + fmt.Fprintln(w, " MediaType:\t", l.MediaType) + fmt.Fprintln(w) + w.Flush() + } + default: + return fmt.Errorf("unsupported source: %T", src.Scheme) } - default: - return fmt.Errorf("unsupported source: %T", s.Source.Scheme) } // populate artifacts... diff --git a/syft/lib.go b/syft/lib.go index 12aa339605c..2e2840e3af3 100644 --- a/syft/lib.go +++ b/syft/lib.go @@ -74,6 +74,14 @@ func CatalogPackages(src *source.Source, cfg cataloger.Config) (*pkg.Catalog, [] return nil, nil, nil, err } + for _, a := range catalog.Sorted() { + relationships = append(relationships, artifact.Relationship{ + From: a, + To: &src.Metadata, + Type: artifact.SourceRelationship, + }) + } + return catalog, relationships, release, nil } diff --git a/syft/linux/identify_release.go b/syft/linux/identify_release.go index 8b24cae8d88..031022020cc 100644 --- a/syft/linux/identify_release.go +++ b/syft/linux/identify_release.go @@ -108,7 +108,7 @@ func parseOsRelease(contents string) (*Release, error) { r := Release{ PrettyName: values["PRETTY_NAME"], Name: values["NAME"], - ID: values["ID"], + OSID: values["ID"], IDLike: idLike, Version: values["VERSION"], VersionID: values["VERSION_ID"], @@ -180,7 +180,7 @@ func simpleRelease(prettyName, name, version, cpe string) *Release { return &Release{ PrettyName: prettyName, Name: name, - ID: name, + OSID: name, IDLike: []string{name}, Version: version, VersionID: version, diff --git a/syft/linux/identify_release_test.go b/syft/linux/identify_release_test.go index 09b6b657da8..90ae44aa346 100644 --- a/syft/linux/identify_release_test.go +++ b/syft/linux/identify_release_test.go @@ -21,7 +21,7 @@ func TestIdentifyRelease(t *testing.T) { release: &Release{ PrettyName: "Alpine Linux v3.11", Name: "Alpine Linux", - ID: "alpine", + OSID: "alpine", IDLike: nil, VersionID: "3.11.6", HomeURL: "https://alpinelinux.org/", @@ -33,7 +33,7 @@ func TestIdentifyRelease(t *testing.T) { release: &Release{ PrettyName: "Amazon Linux 2", Name: "Amazon Linux", - ID: "amzn", + OSID: "amzn", IDLike: []string{ "centos", "rhel", @@ -50,7 +50,7 @@ func TestIdentifyRelease(t *testing.T) { release: &Release{ PrettyName: "BusyBox v1.31.1", Name: "busybox", - ID: "busybox", + OSID: "busybox", IDLike: []string{"busybox"}, Version: "1.31.1", VersionID: "1.31.1", @@ -61,7 +61,7 @@ func TestIdentifyRelease(t *testing.T) { release: &Release{ PrettyName: "CentOS Linux 8 (Core)", Name: "CentOS Linux", - ID: "centos", + OSID: "centos", IDLike: []string{"rhel", "fedora", }, @@ -77,7 +77,7 @@ func TestIdentifyRelease(t *testing.T) { release: &Release{ PrettyName: "Debian GNU/Linux 8 (jessie)", Name: "Debian GNU/Linux", - ID: "debian", + OSID: "debian", IDLike: nil, Version: "8 (jessie)", VersionID: "8", @@ -91,7 +91,7 @@ func TestIdentifyRelease(t *testing.T) { release: &Release{ PrettyName: "Fedora 31 (Container Image)", Name: "Fedora", - ID: "fedora", + OSID: "fedora", IDLike: nil, Version: "31 (Container Image)", VersionID: "31", @@ -109,7 +109,7 @@ func TestIdentifyRelease(t *testing.T) { release: &Release{ PrettyName: "Red Hat Enterprise Linux Server 7.3 (Maipo)", Name: "Red Hat Enterprise Linux Server", - ID: "rhel", + OSID: "rhel", IDLike: []string{"fedora"}, Version: "7.3 (Maipo)", VersionID: "7.3", @@ -123,7 +123,7 @@ func TestIdentifyRelease(t *testing.T) { release: &Release{ PrettyName: "Ubuntu 20.04 LTS", Name: "Ubuntu", - ID: "ubuntu", + OSID: "ubuntu", IDLike: []string{"debian"}, Version: "20.04 LTS (Focal Fossa)", VersionCodename: "focal", @@ -139,7 +139,7 @@ func TestIdentifyRelease(t *testing.T) { release: &Release{ PrettyName: "Oracle Linux Server 8.3", Name: "Oracle Linux Server", - ID: "ol", + OSID: "ol", IDLike: []string{"fedora"}, Version: "8.3", VersionID: "8.3", @@ -158,7 +158,7 @@ func TestIdentifyRelease(t *testing.T) { release: &Release{ PrettyName: "CentOS Linux 8 (Core)", Name: "Scientific Linux", - ID: "scientific", + OSID: "scientific", IDLike: []string{ "rhel", "fedora", @@ -175,7 +175,7 @@ func TestIdentifyRelease(t *testing.T) { release: &Release{ PrettyName: "openSUSE Leap 15.2", Name: "openSUSE Leap", - ID: "opensuse-leap", + OSID: "opensuse-leap", IDLike: []string{ "suse", "opensuse", @@ -192,7 +192,7 @@ func TestIdentifyRelease(t *testing.T) { release: &Release{ PrettyName: "SUSE Linux Enterprise Server 15 SP2", Name: "SLES", - ID: "sles", + OSID: "sles", IDLike: []string{"suse"}, Version: "15-SP2", VersionID: "15.2", @@ -204,7 +204,7 @@ func TestIdentifyRelease(t *testing.T) { release: &Release{ PrettyName: "VMware Photon OS/Linux", Name: "VMware Photon OS", - ID: "photon", + OSID: "photon", IDLike: nil, Version: "2.0", VersionID: "2.0", @@ -217,7 +217,7 @@ func TestIdentifyRelease(t *testing.T) { release: &Release{ PrettyName: "Arch Linux", Name: "Arch Linux", - ID: "arch", + OSID: "arch", IDLike: nil, BuildID: "rolling", HomeURL: "https://www.archlinux.org/", @@ -237,7 +237,7 @@ func TestIdentifyRelease(t *testing.T) { fixture: "test-fixtures/partial-fields/unknown-id", release: &Release{ Name: "Debian GNU/Linux", - ID: "my-awesome-distro", + OSID: "my-awesome-distro", IDLike: []string{"debian"}, VersionID: "8", }, @@ -254,7 +254,7 @@ func TestIdentifyRelease(t *testing.T) { release: &Release{ PrettyName: "centos", Name: "centos", - ID: "centos", + OSID: "centos", IDLike: []string{"centos"}, Version: "6", VersionID: "6", @@ -266,7 +266,7 @@ func TestIdentifyRelease(t *testing.T) { release: &Release{ PrettyName: "CentOS", Name: "centos", - ID: "centos", + OSID: "centos", IDLike: []string{"centos"}, Version: "5.7", VersionID: "5.7", @@ -277,7 +277,7 @@ func TestIdentifyRelease(t *testing.T) { release: &Release{ PrettyName: "CBL-Mariner/Linux", Name: "Common Base Linux Mariner", - ID: "mariner", + OSID: "mariner", IDLike: nil, Version: "1.0.20210901", VersionID: "1.0", @@ -291,7 +291,7 @@ func TestIdentifyRelease(t *testing.T) { release: &Release{ PrettyName: "Rocky Linux 8.4 (Green Obsidian)", Name: "Rocky Linux", - ID: "rocky", + OSID: "rocky", IDLike: []string{ "rhel", "fedora", @@ -308,7 +308,7 @@ func TestIdentifyRelease(t *testing.T) { release: &Release{ PrettyName: "AlmaLinux 8.4 (Electric Cheetah)", Name: "AlmaLinux", - ID: "almalinux", + OSID: "almalinux", IDLike: []string{ "rhel", "centos", @@ -348,7 +348,7 @@ func TestParseOsRelease(t *testing.T) { release: &Release{ PrettyName: "Ubuntu 20.04 LTS", Name: "Ubuntu", - ID: "ubuntu", + OSID: "ubuntu", IDLike: []string{"debian"}, Version: "20.04 LTS (Focal Fossa)", VersionID: "20.04", @@ -366,7 +366,7 @@ func TestParseOsRelease(t *testing.T) { release: &Release{ PrettyName: "Debian GNU/Linux 8 (jessie)", Name: "Debian GNU/Linux", - ID: "debian", + OSID: "debian", IDLike: nil, Version: "8 (jessie)", VersionID: "8", @@ -382,7 +382,7 @@ func TestParseOsRelease(t *testing.T) { release: &Release{ PrettyName: "CentOS Linux 8 (Core)", Name: "CentOS Linux", - ID: "centos", + OSID: "centos", IDLike: []string{ "rhel", "fedora", @@ -401,7 +401,7 @@ func TestParseOsRelease(t *testing.T) { release: &Release{ PrettyName: "Red Hat Enterprise Linux 8.1 (Ootpa)", Name: "Red Hat Enterprise Linux", - ID: "rhel", + OSID: "rhel", IDLike: []string{"fedora"}, Version: "8.1 (Ootpa)", VersionID: "8.1", @@ -417,7 +417,7 @@ func TestParseOsRelease(t *testing.T) { release: &Release{ PrettyName: "Debian GNU/Linux 8 (jessie)", Name: "Debian GNU/Linux", - ID: "debian", + OSID: "debian", IDLike: nil, Version: "8 (jessie)", VersionID: "8", @@ -454,7 +454,7 @@ func TestParseSystemReleaseCPE(t *testing.T) { release: &Release{ PrettyName: "centos", Name: "centos", - ID: "centos", + OSID: "centos", IDLike: []string{"centos"}, Version: "6", VersionID: "6", @@ -494,7 +494,7 @@ func TestParseRedhatRelease(t *testing.T) { release: &Release{ PrettyName: "CentOS", Name: "centos", - ID: "centos", + OSID: "centos", IDLike: []string{"centos"}, Version: "5.7", VersionID: "5.7", diff --git a/syft/linux/release.go b/syft/linux/release.go index ab68fbc5e6f..69568c98aaa 100644 --- a/syft/linux/release.go +++ b/syft/linux/release.go @@ -1,11 +1,13 @@ package linux +import "github.com/anchore/syft/syft/artifact" + // Release represents Linux Distribution release information as specified from https://www.freedesktop.org/software/systemd/man/os-release.html type Release struct { PrettyName string `cyclonedx:"prettyName"` // A pretty operating system name in a format suitable for presentation to the user. Name string // identifies the operating system, without a version component, and suitable for presentation to the user. - ID string `cyclonedx:"id"` // identifies the operating system, excluding any version information and suitable for processing by scripts or usage in generated filenames. - IDLike []string `cyclonedx:"idLike"` // list of operating system identifiers in the same syntax as the ID= setting. It should list identifiers of operating systems that are closely related to the local operating system in regards to packaging and programming interfaces. + OSID string `cyclonedx:"id"` // identifies the operating system, excluding any version information and suitable for processing by scripts or usage in generated filenames. + IDLike []string `cyclonedx:"idLike"` // list of operating system identifiers in the same syntax as the OSID= setting. It should list identifiers of operating systems that are closely related to the local operating system in regards to packaging and programming interfaces. Version string // identifies the operating system version, excluding any OS name information, possibly including a release code name, and suitable for presentation to the user. VersionID string `cyclonedx:"versionID"` // identifies the operating system version, excluding any OS name information or release code name, and suitable for processing by scripts or usage in generated filenames. VersionCodename string `cyclonedx:"versionCodename"` @@ -21,6 +23,13 @@ type Release struct { CPEName string // A CPE name for the operating system, in URI binding syntax } +func (r *Release) ID() artifact.ID { + id, _ := artifact.IDByHash(r) + return id +} + +var _ artifact.Identifiable = (*Release)(nil) + func (r *Release) String() string { if r == nil { return "unknown" @@ -32,11 +41,11 @@ func (r *Release) String() string { return r.Name } if r.Version != "" { - return r.ID + " " + r.Version + return r.OSID + " " + r.Version } if r.VersionID != "" { - return r.ID + " " + r.VersionID + return r.OSID + " " + r.VersionID } - return r.ID + " " + r.BuildID + return r.OSID + " " + r.BuildID } diff --git a/syft/pkg/alpm_metadata.go b/syft/pkg/alpm_metadata.go index 567b9fd811c..c95b760e290 100644 --- a/syft/pkg/alpm_metadata.go +++ b/syft/pkg/alpm_metadata.go @@ -52,7 +52,7 @@ func (m AlpmMetadata) PackageURL(distro *linux.Release) string { distroID := "" if distro != nil { - distroID = distro.ID + distroID = distro.OSID } return packageurl.NewPackageURL( diff --git a/syft/pkg/alpm_metadata_test.go b/syft/pkg/alpm_metadata_test.go index e31ad02a112..d81c1e3e807 100644 --- a/syft/pkg/alpm_metadata_test.go +++ b/syft/pkg/alpm_metadata_test.go @@ -24,7 +24,7 @@ func TestAlpmMetadata_pURL(t *testing.T) { Architecture: "a", }, distro: linux.Release{ - ID: "arch", + OSID: "arch", BuildID: "rolling", }, expected: "pkg:alpm/arch/p@v?arch=a&distro=arch-rolling", @@ -36,7 +36,7 @@ func TestAlpmMetadata_pURL(t *testing.T) { Version: "v", }, distro: linux.Release{ - ID: "arch", + OSID: "arch", }, expected: "pkg:alpm/arch/p@v?distro=arch", }, @@ -47,7 +47,7 @@ func TestAlpmMetadata_pURL(t *testing.T) { Architecture: "any", }, distro: linux.Release{ - ID: "arch", + OSID: "arch", BuildID: "rolling", }, expected: "pkg:alpm/arch/python@3.10.0?arch=any&distro=arch-rolling", @@ -59,7 +59,7 @@ func TestAlpmMetadata_pURL(t *testing.T) { Architecture: "x86_64", }, distro: linux.Release{ - ID: "arch", + OSID: "arch", BuildID: "rolling", }, expected: "pkg:alpm/arch/g%20plus%20plus@v84?arch=x86_64&distro=arch-rolling", @@ -73,7 +73,7 @@ func TestAlpmMetadata_pURL(t *testing.T) { BasePackage: "origin", }, distro: linux.Release{ - ID: "arch", + OSID: "arch", BuildID: "rolling", }, expected: "pkg:alpm/arch/p@v?arch=a&upstream=origin&distro=arch-rolling", diff --git a/syft/pkg/apk_metadata_test.go b/syft/pkg/apk_metadata_test.go index 05c463530d5..bdb1e518e00 100644 --- a/syft/pkg/apk_metadata_test.go +++ b/syft/pkg/apk_metadata_test.go @@ -26,7 +26,7 @@ func TestApkMetadata_pURL(t *testing.T) { Architecture: "a", }, distro: linux.Release{ - ID: "alpine", + OSID: "alpine", VersionID: "3.4.6", }, expected: "pkg:alpine/p@v?arch=a&distro=alpine-3.4.6", @@ -38,7 +38,7 @@ func TestApkMetadata_pURL(t *testing.T) { Version: "v", }, distro: linux.Release{ - ID: "alpine", + OSID: "alpine", VersionID: "3.4.6", }, expected: "pkg:alpine/p@v?distro=alpine-3.4.6", @@ -51,7 +51,7 @@ func TestApkMetadata_pURL(t *testing.T) { Architecture: "am86", }, distro: linux.Release{ - ID: "alpine", + OSID: "alpine", VersionID: "3.4.6", }, expected: "pkg:alpine/g++@v84?arch=am86&distro=alpine-3.4.6", @@ -63,7 +63,7 @@ func TestApkMetadata_pURL(t *testing.T) { Architecture: "am86", }, distro: linux.Release{ - ID: "alpine", + OSID: "alpine", VersionID: "3.15.0", }, expected: "pkg:alpine/g%20plus%20plus@v84?arch=am86&distro=alpine-3.15.0", @@ -77,7 +77,7 @@ func TestApkMetadata_pURL(t *testing.T) { OriginPackage: "origin", }, distro: linux.Release{ - ID: "alpine", + OSID: "alpine", VersionID: "3.4.6", }, expected: "pkg:alpine/p@v?arch=a&upstream=origin&distro=alpine-3.4.6", diff --git a/syft/pkg/dpkg_metadata.go b/syft/pkg/dpkg_metadata.go index fb405a85bdd..ac3c034eca9 100644 --- a/syft/pkg/dpkg_metadata.go +++ b/syft/pkg/dpkg_metadata.go @@ -43,7 +43,7 @@ type DpkgFileRecord struct { func (m DpkgMetadata) PackageURL(distro *linux.Release) string { var namespace string if distro != nil { - namespace = distro.ID + namespace = distro.OSID } qualifiers := map[string]string{ diff --git a/syft/pkg/dpkg_metadata_test.go b/syft/pkg/dpkg_metadata_test.go index 088e4100066..5e8c8488d73 100644 --- a/syft/pkg/dpkg_metadata_test.go +++ b/syft/pkg/dpkg_metadata_test.go @@ -20,7 +20,7 @@ func TestDpkgMetadata_pURL(t *testing.T) { { name: "go case", distro: &linux.Release{ - ID: "debian", + OSID: "debian", VersionID: "11", }, metadata: DpkgMetadata{ @@ -32,7 +32,7 @@ func TestDpkgMetadata_pURL(t *testing.T) { { name: "with arch info", distro: &linux.Release{ - ID: "ubuntu", + OSID: "ubuntu", VersionID: "16.04", }, metadata: DpkgMetadata{ @@ -53,7 +53,7 @@ func TestDpkgMetadata_pURL(t *testing.T) { { name: "with upstream qualifier with source pkg name info", distro: &linux.Release{ - ID: "debian", + OSID: "debian", VersionID: "11", }, metadata: DpkgMetadata{ @@ -66,7 +66,7 @@ func TestDpkgMetadata_pURL(t *testing.T) { { name: "with upstream qualifier with source pkg name and version info", distro: &linux.Release{ - ID: "debian", + OSID: "debian", VersionID: "11", }, metadata: DpkgMetadata{ diff --git a/syft/pkg/php_composer_json_metadata_test.go b/syft/pkg/php_composer_json_metadata_test.go index ee38ad87152..fbb726459ef 100644 --- a/syft/pkg/php_composer_json_metadata_test.go +++ b/syft/pkg/php_composer_json_metadata_test.go @@ -42,7 +42,7 @@ func TestPhpComposerJsonMetadata_pURL(t *testing.T) { { name: "ignores distro", distro: &linux.Release{ - ID: "rhel", + OSID: "rhel", VersionID: "8.4", }, metadata: PhpComposerJSONMetadata{ diff --git a/syft/pkg/python_package_metadata_test.go b/syft/pkg/python_package_metadata_test.go index 4798ef37311..0332d0112c4 100644 --- a/syft/pkg/python_package_metadata_test.go +++ b/syft/pkg/python_package_metadata_test.go @@ -33,7 +33,7 @@ func TestPythonPackageMetadata_pURL(t *testing.T) { { name: "should not respond to release info", distro: &linux.Release{ - ID: "rhel", + OSID: "rhel", VersionID: "8.4", }, metadata: PythonPackageMetadata{ diff --git a/syft/pkg/rpm_metadata.go b/syft/pkg/rpm_metadata.go index f2d8880d630..c204a1a4277 100644 --- a/syft/pkg/rpm_metadata.go +++ b/syft/pkg/rpm_metadata.go @@ -58,7 +58,7 @@ type RpmdbFileMode uint16 func (m RpmMetadata) PackageURL(distro *linux.Release) string { var namespace string if distro != nil { - namespace = distro.ID + namespace = distro.OSID } qualifiers := map[string]string{ diff --git a/syft/pkg/rpm_metadata_test.go b/syft/pkg/rpm_metadata_test.go index 84ddef60d3e..1dc2cd51a77 100644 --- a/syft/pkg/rpm_metadata_test.go +++ b/syft/pkg/rpm_metadata_test.go @@ -20,7 +20,7 @@ func TestRpmMetadata_pURL(t *testing.T) { { name: "go case", distro: &linux.Release{ - ID: "rhel", + OSID: "rhel", VersionID: "8.4", }, metadata: RpmMetadata{ @@ -34,7 +34,7 @@ func TestRpmMetadata_pURL(t *testing.T) { { name: "with arch and epoch", distro: &linux.Release{ - ID: "centos", + OSID: "centos", VersionID: "7", }, metadata: RpmMetadata{ @@ -59,7 +59,7 @@ func TestRpmMetadata_pURL(t *testing.T) { { name: "with upstream source rpm info", distro: &linux.Release{ - ID: "rhel", + OSID: "rhel", VersionID: "8.4", }, metadata: RpmMetadata{ diff --git a/syft/pkg/url.go b/syft/pkg/url.go index 35b8f9c061a..7324af710f3 100644 --- a/syft/pkg/url.go +++ b/syft/pkg/url.go @@ -91,8 +91,8 @@ func purlQualifiers(vars map[string]string, release *linux.Release) (q packageur return q } - if release.ID != "" { - distroQualifiers = append(distroQualifiers, release.ID) + if release.OSID != "" { + distroQualifiers = append(distroQualifiers, release.OSID) } if release.VersionID != "" { diff --git a/syft/pkg/url_test.go b/syft/pkg/url_test.go index e2a01af91a4..fc64a4c9730 100644 --- a/syft/pkg/url_test.go +++ b/syft/pkg/url_test.go @@ -97,7 +97,7 @@ func TestPackageURL(t *testing.T) { { name: "deb", distro: &linux.Release{ - ID: "ubuntu", + OSID: "ubuntu", VersionID: "20.04", }, pkg: Package{ @@ -115,7 +115,7 @@ func TestPackageURL(t *testing.T) { { name: "rpm", distro: &linux.Release{ - ID: "centos", + OSID: "centos", VersionID: "7", }, pkg: Package{ @@ -144,7 +144,7 @@ func TestPackageURL(t *testing.T) { { name: "apk", distro: &linux.Release{ - ID: "alpine", + OSID: "alpine", VersionID: "3.4.6", }, pkg: Package{ @@ -203,7 +203,7 @@ func TestPackageURL(t *testing.T) { { name: "alpm", distro: &linux.Release{ - ID: "arch", + OSID: "arch", BuildID: "rolling", }, pkg: Package{ diff --git a/syft/sbom/sbom.go b/syft/sbom/sbom.go index 0f77567fcc9..b54b28b7280 100644 --- a/syft/sbom/sbom.go +++ b/syft/sbom/sbom.go @@ -13,7 +13,7 @@ import ( type SBOM struct { Artifacts Artifacts Relationships []artifact.Relationship - Source source.Metadata + Sources []source.Metadata Descriptor Descriptor } @@ -24,7 +24,7 @@ type Artifacts struct { FileClassifications map[source.Coordinates][]file.Classification FileContents map[source.Coordinates]string Secrets map[source.Coordinates][]file.SearchResult - LinuxDistribution *linux.Release + LinuxDistributions []linux.Release } type Descriptor struct { @@ -33,6 +33,26 @@ type Descriptor struct { Configuration interface{} } +func (s SBOM) Source(p *pkg.Package) *source.Metadata { + for _, r := range s.Relationships { + if r.Type == artifact.SourceRelationship && r.From.ID() == p.ID() { + s, _ := r.To.(*source.Metadata) + return s + } + } + return nil +} + +func (s SBOM) Distro(p *pkg.Package) *linux.Release { + for _, r := range s.Relationships { + if r.Type == artifact.SourceRelationship && r.From.ID() == p.ID() { + s, _ := r.To.(*linux.Release) + return s + } + } + return nil +} + func (s SBOM) RelationshipsSorted() []artifact.Relationship { relationships := s.Relationships sort.SliceStable(relationships, func(i, j int) bool { diff --git a/syft/source/metadata.go b/syft/source/metadata.go index b9747362e26..ca8cb621753 100644 --- a/syft/source/metadata.go +++ b/syft/source/metadata.go @@ -1,8 +1,19 @@ package source +import ( + "github.com/anchore/syft/syft/artifact" +) + // Metadata represents any static source data that helps describe "what" was cataloged. type Metadata struct { Scheme Scheme // the source data scheme type (directory or image) ImageMetadata ImageMetadata // all image info (image only) Path string // the root path to be cataloged (directory only) } + +func (m *Metadata) ID() artifact.ID { + id, _ := artifact.IDByHash(m) + return id +} + +var _ artifact.Identifiable = (*Metadata)(nil) diff --git a/test/integration/distro_test.go b/test/integration/distro_test.go index d2159660ff8..c0205506771 100644 --- a/test/integration/distro_test.go +++ b/test/integration/distro_test.go @@ -12,14 +12,14 @@ import ( func TestDistroImage(t *testing.T) { sbom, _ := catalogFixtureImage(t, "image-distro-id", source.SquashedScope, nil) - expected := &linux.Release{ + expected := linux.Release{ PrettyName: "BusyBox v1.31.1", Name: "busybox", - ID: "busybox", + OSID: "busybox", IDLike: []string{"busybox"}, Version: "1.31.1", VersionID: "1.31.1", } - assert.Equal(t, expected, sbom.Artifacts.LinuxDistribution) + assert.Equal(t, expected, sbom.Artifacts.LinuxDistributions[0]) } diff --git a/test/integration/encode_decode_cycle_test.go b/test/integration/encode_decode_cycle_test.go index 5b9e82dfb85..a94b2c4ba7b 100644 --- a/test/integration/encode_decode_cycle_test.go +++ b/test/integration/encode_decode_cycle_test.go @@ -44,6 +44,7 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) { { formatOption: cyclonedxjson.ID, redactor: func(in []byte) []byte { + in = regexp.MustCompile("package-id=[a-z0-9]+").ReplaceAll(in, []byte{}) in = regexp.MustCompile("\"(timestamp|serialNumber|bom-ref)\": \"[^\"]+\",").ReplaceAll(in, []byte{}) return in }, @@ -52,6 +53,7 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) { { formatOption: cyclonedxxml.ID, redactor: func(in []byte) []byte { + in = regexp.MustCompile("package-id=[a-z0-9]+").ReplaceAll(in, []byte{}) in = regexp.MustCompile("(serialNumber|bom-ref)=\"[^\"]+\"").ReplaceAll(in, []byte{}) in = regexp.MustCompile("[^<]+").ReplaceAll(in, []byte{}) return in diff --git a/test/integration/utils_test.go b/test/integration/utils_test.go index c3f6abf461c..ffdb4c4e3c1 100644 --- a/test/integration/utils_test.go +++ b/test/integration/utils_test.go @@ -7,6 +7,7 @@ import ( "github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/syft/syft" + "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg/cataloger" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" @@ -31,13 +32,18 @@ func catalogFixtureImage(t *testing.T, fixtureImageName string, scope source.Sco t.Fatalf("failed to catalog image: %+v", err) } + distros := []linux.Release{} + if actualDistro != nil { + distros = append(distros, *actualDistro) + } + return sbom.SBOM{ Artifacts: sbom.Artifacts{ - PackageCatalog: pkgCatalog, - LinuxDistribution: actualDistro, + PackageCatalog: pkgCatalog, + LinuxDistributions: distros, }, Relationships: relationships, - Source: theSource.Metadata, + Sources: []source.Metadata{theSource.Metadata}, Descriptor: sbom.Descriptor{ Name: "syft", Version: "v0.42.0-bogus", @@ -66,12 +72,17 @@ func catalogDirectory(t *testing.T, dir string) (sbom.SBOM, *source.Source) { t.Fatalf("failed to catalog image: %+v", err) } + distros := []linux.Release{} + if actualDistro != nil { + distros = append(distros, *actualDistro) + } + return sbom.SBOM{ Artifacts: sbom.Artifacts{ - PackageCatalog: pkgCatalog, - LinuxDistribution: actualDistro, + PackageCatalog: pkgCatalog, + LinuxDistributions: distros, }, Relationships: relationships, - Source: theSource.Metadata, + Sources: []source.Metadata{theSource.Metadata}, }, theSource }