fix: run root file catalogers after the archive task with owned-by-package selection

with nested cataloging on, java leaves root-level archives to the archive task, so a root `app.jar` package only exists once that task has run. With the default `FilesOwnedByPackageSelection` the root file catalogers ran in an earlier group, never saw the package, and `app.jar` got no file metadata or digests. It did get them with the feature off.

- in owned-by-package mode the groups are now packages, then the archive task, then the root file tasks. Other modes are unchanged
- the file tasks merge their results into the SBOM instead of replacing the maps, since the archive task has already written nested files into them

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman
2026-09-24 13:34:09 -04:00
parent 9d820c6d94
commit 23de18918e
3 changed files with 39 additions and 14 deletions
+4 -4
View File
@@ -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
+21
View File
@@ -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().
+14 -10
View File
@@ -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