diff --git a/internal/task/file_tasks.go b/internal/task/file_tasks.go index b12202f07..53a776674 100644 --- a/internal/task/file_tasks.go +++ b/internal/task/file_tasks.go @@ -48,7 +48,7 @@ func newFileDigestCatalogerTask(selection file.Selection, hashers []crypto.Hash, result, err := filedigest.NewCataloger(hashers).Catalog(ctx, resolver, coordinates...) accessor.WriteToSBOM(func(sbom *sbom.SBOM) { - sbom.Artifacts.FileDigests = result + mergeInto(&sbom.Artifacts.FileDigests, result) }) return err @@ -79,7 +79,7 @@ func newFileMetadataCatalogerTask(selection file.Selection, tags ...string) Task result, err := filemetadata.NewCataloger().Catalog(ctx, resolver, coordinates...) accessor.WriteToSBOM(func(sbom *sbom.SBOM) { - sbom.Artifacts.FileMetadata = result + mergeInto(&sbom.Artifacts.FileMetadata, result) }) return err @@ -105,7 +105,7 @@ func newFileContentCatalogerTask(cfg filecontent.Config, tags ...string) Task { result, err := filecontent.NewCataloger(cfg).Catalog(ctx, resolver) accessor.WriteToSBOM(func(sbom *sbom.SBOM) { - sbom.Artifacts.FileContents = result + mergeInto(&sbom.Artifacts.FileContents, result) }) return err @@ -131,7 +131,7 @@ func newExecutableCatalogerTask(selection file.Selection, cfg executable.Config, result, err := executable.NewCataloger(cfg).CatalogCtx(ctx, resolver) accessor.WriteToSBOM(func(sbom *sbom.SBOM) { - sbom.Artifacts.Executables = result + mergeInto(&sbom.Artifacts.Executables, result) }) return err diff --git a/syft/archive_cataloger_test.go b/syft/archive_cataloger_test.go index 129ada767..21dbba0e9 100644 --- a/syft/archive_cataloger_test.go +++ b/syft/archive_cataloger_test.go @@ -360,6 +360,27 @@ func TestArchiveCataloger_jarReachedThroughALinkIsIdentified(t *testing.T) { assert.Contains(t, s.Artifacts.FileMetadata, manifest) } +func TestArchiveCataloger_defaultFileSelectionCoversRootJars(t *testing.T) { + // with the default owned-by-package selection, a jar at the root is described by the archive task, + // so the root file catalogers must run after it or the jar gets no file entries at all + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "app.jar"), jarBytes(t, "app", "1.0"), 0o644)) + + for name, depth := range map[string]int{"feature off": 0, "feature on": 1} { + t.Run(name, func(t *testing.T) { + cfg := DefaultCreateSBOMConfig(). + WithCatalogerSelection(cataloging.NewSelectionRequest().WithDefaults("java")). + WithArchiveConfig(cataloging.DefaultArchiveSearchConfig().WithMaxDepth(depth)) + s := scanDirWithExclusions(t, dir, cfg) + + require.Equal(t, []string{"|app.jar"}, packageLocations(s, "app")) + jar := file.Coordinates{RealPath: "app.jar"} + assert.Contains(t, s.Artifacts.FileMetadata, jar) + assert.Contains(t, s.Artifacts.FileDigests, jar) + }) + } +} + func TestArchiveCataloger_descriptorRecordsArchiveConfig(t *testing.T) { // a consumer must be able to tell from the SBOM alone how deep archive contents were searched cfg := DefaultCreateSBOMConfig(). diff --git a/syft/create_sbom_config.go b/syft/create_sbom_config.go index 423a5973e..e872fbd6d 100644 --- a/syft/create_sbom_config.go +++ b/syft/create_sbom_config.go @@ -211,19 +211,23 @@ func (c *CreateSBOMConfig) makeTaskGroups(src source.Description, exclusions []s return nil, nil, err } - // combine the user-provided and configured tasks - if c.Files.Selection == file.FilesOwnedByPackageSelection { - // special case: we need the package info when we are cataloging files owned by packages - taskGroups = append(taskGroups, pkgTasks, fileTasks) - } else { - taskGroups = append(taskGroups, append(pkgTasks, fileTasks...)) - } - // the same package and file catalogers run against the contents of every archive, after the scan // root and before relationship and unknowns post-processing archiveTask := task.NewArchiveCatalogerTask(c.Archive, append(slices.Clone(pkgTasks), fileTasks...), exclusions) - if archiveTask != nil { - taskGroups = append(taskGroups, []task.Task{archiveTask}) + + // combine the user-provided and configured tasks + switch { + case c.Files.Selection == file.FilesOwnedByPackageSelection && archiveTask != nil: + // the archive task describes archives at the root too (java leaves them to it), so the root file + // catalogers must run after it to see those packages + taskGroups = append(taskGroups, pkgTasks, []task.Task{archiveTask}, fileTasks) + case c.Files.Selection == file.FilesOwnedByPackageSelection: + // special case: we need the package info when we are cataloging files owned by packages + taskGroups = append(taskGroups, pkgTasks, fileTasks) + case archiveTask != nil: + taskGroups = append(taskGroups, append(pkgTasks, fileTasks...), []task.Task{archiveTask}) + default: + taskGroups = append(taskGroups, append(pkgTasks, fileTasks...)) } // all scope work must be done after all nodes (files and packages) have been cataloged and before the relationship