diff --git a/internal/task/package_tasks.go b/internal/task/package_tasks.go index b3c4be036..a38ad2316 100644 --- a/internal/task/package_tasks.go +++ b/internal/task/package_tasks.go @@ -18,6 +18,7 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/haskell" "github.com/anchore/syft/syft/pkg/cataloger/java" "github.com/anchore/syft/syft/pkg/cataloger/javascript" + "github.com/anchore/syft/syft/pkg/cataloger/kernel" "github.com/anchore/syft/syft/pkg/cataloger/nix" "github.com/anchore/syft/syft/pkg/cataloger/php" "github.com/anchore/syft/syft/pkg/cataloger/python" @@ -112,9 +113,16 @@ func DefaultPackageTaskFactories() PackageTaskFactories { func(cfg CatalogingFactoryConfig) pkg.Cataloger { return binary.NewCataloger(cfg.PackagesConfig.Binary) }, - pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "binary"), + pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "binary", + ), newSimplePackageTaskFactory(githubactions.NewActionUsageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "github", "github-actions"), newSimplePackageTaskFactory(githubactions.NewWorkflowUsageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "github", "github-actions"), + newPackageTaskFactory( + func(cfg CatalogingFactoryConfig) pkg.Cataloger { + return kernel.NewLinuxKernelCataloger(cfg.PackagesConfig.LinuxKernel) + }, + pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "linux", "kernel", + ), newSimplePackageTaskFactory(sbomCataloger.NewCataloger, pkgcataloging.ImageTag, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "sbom"), // note: not evidence of installed packages } } diff --git a/test/cli/scan_cmd_test.go b/test/cli/scan_cmd_test.go index 05503bffd..b3d3e3b4b 100644 --- a/test/cli/scan_cmd_test.go +++ b/test/cli/scan_cmd_test.go @@ -9,7 +9,7 @@ import ( const ( // this is the number of packages that should be found in the image-pkg-coverage fixture image // when analyzed with the squashed scope. - coverageImageSquashedPackageCount = 25 + coverageImageSquashedPackageCount = 27 ) func TestPackagesCmdFlags(t *testing.T) { diff --git a/test/cli/trait_assertions_test.go b/test/cli/trait_assertions_test.go index 97404443c..ca01c9cfd 100644 --- a/test/cli/trait_assertions_test.go +++ b/test/cli/trait_assertions_test.go @@ -122,6 +122,7 @@ func assertPackageCount(length uint) traitAssertion { type NameAndVersion struct { Name string `json:"name"` Version string `json:"version"` + Type string `json:"type"` } type partial struct { Artifacts []NameAndVersion `json:"artifacts"` @@ -136,7 +137,7 @@ func assertPackageCount(length uint) traitAssertion { tb.Errorf("expected package count of %d, but found %d", length, len(data.Artifacts)) debugArtifacts := make([]string, len(data.Artifacts)) for i, a := range data.Artifacts { - debugArtifacts[i] = fmt.Sprintf("%s:%s", a.Name, a.Version) + debugArtifacts[i] = fmt.Sprintf("%s@%s (%s)", a.Name, a.Version, a.Type) } sort.Strings(debugArtifacts) for i, a := range debugArtifacts { diff --git a/test/integration/package_catalogers_represented_test.go b/test/integration/package_catalogers_represented_test.go new file mode 100644 index 000000000..27a07d372 --- /dev/null +++ b/test/integration/package_catalogers_represented_test.go @@ -0,0 +1,201 @@ +package integration + +import ( + "bytes" + "go/ast" + "go/parser" + "go/token" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/scylladb/go-set/strset" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/internal/task" + "github.com/anchore/syft/syft/cataloging/pkgcataloging" +) + +func TestAllPackageCatalogersReachableInTasks(t *testing.T) { + // we want to see if we can get a task for all package catalogers. This is a bit tricky since we + // don't have a nice way to find all cataloger names in the codebase. Instead, we'll look at the + // count of unique task names from the package task factory set and compare that with the known constructors + // from a source analysis... they should match. + + // additionally, at this time they should either have a "directory" or "image" tag as well. If there is no tag + // on a cataloger task then the test should fail. + + taskFactories := task.DefaultPackageTaskFactories() + taskTagsByName := make(map[string][]string) + for _, factory := range taskFactories { + tsk := factory(task.DefaultCatalogingFactoryConfig()) + if taskTagsByName[tsk.Name()] != nil { + t.Fatalf("duplicate task name: %q", tsk.Name()) + } + + require.NotNil(t, tsk) + if sel, ok := tsk.(task.Selector); ok { + taskTagsByName[tsk.Name()] = sel.Selectors() + } else { + taskTagsByName[tsk.Name()] = []string{} + } + } + + var constructorCount int + constructorsPerPackage := getCatalogerConstructors(t) + for _, constructors := range constructorsPerPackage { + constructorCount += constructors.Size() + } + + assert.Equal(t, len(taskTagsByName), constructorCount, "mismatch in number of cataloger constructors and task names") + + for taskName, tags := range taskTagsByName { + if !strset.New(tags...).HasAny(pkgcataloging.ImageTag, pkgcataloging.DirectoryTag) { + t.Errorf("task %q is missing 'directory' or 'image' a tag", taskName) + } + } + +} + +func TestAllPackageCatalogersRepresentedInSource(t *testing.T) { + // find all functions in syft/pkg/cataloger/** that either: + // - match the name glob "New*Cataloger" + // - are in cataloger.go and match the name glob "New*" + // + // Then: + // - keep track of all packages with cataloger constructors + // - keep track of all constructors + constructorsPerPackage := getCatalogerConstructors(t) + + // look at the source file in internal/task/package_tasks.go: + // - ensure all go packages that have constructors are imported + // - ensure there is a reference to all package constructors + assertAllPackageCatalogersRepresented(t, constructorsPerPackage) +} + +func getCatalogerConstructors(t *testing.T) map[string]*strset.Set { + t.Helper() + root := repoRoot(t) + catalogerPath := filepath.Join(root, "syft", "pkg", "cataloger") + + constructorsPerPackage := make(map[string]*strset.Set) + + err := filepath.Walk(catalogerPath, func(path string, info os.FileInfo, err error) error { + require.NoError(t, err) + + // ignore directories and test files... + if info.IsDir() || strings.HasSuffix(info.Name(), "_test.go") { + return nil + } + + partialResults := getConstructorsFromExpectedFile(t, path, info) + + constructorsPerPackage = mergeConstructors(constructorsPerPackage, partialResults) + + partialResults = getCatalogerConstructorsFromPackage(t, path, info) + + constructorsPerPackage = mergeConstructors(constructorsPerPackage, partialResults) + + return nil + }) + + require.NoError(t, err) + + // remove some exceptions + delete(constructorsPerPackage, "generic") // this is not an actual cataloger + + return constructorsPerPackage +} + +func getConstructorsFromExpectedFile(t *testing.T, path string, info os.FileInfo) map[string][]string { + constructorsPerPackage := make(map[string][]string) + + if !strings.HasSuffix(info.Name(), "cataloger.go") && !strings.HasSuffix(info.Name(), "catalogers.go") { + return nil + } + + fset := token.NewFileSet() + node, err := parser.ParseFile(fset, path, nil, parser.ParseComments) + require.NoError(t, err) + + for _, f := range node.Decls { + fn, ok := f.(*ast.FuncDecl) + if !ok || fn.Recv != nil || !strings.HasPrefix(fn.Name.Name, "New") { + continue + } + + pkg := node.Name.Name + constructorsPerPackage[pkg] = append(constructorsPerPackage[pkg], fn.Name.Name) + } + + return constructorsPerPackage +} + +func getCatalogerConstructorsFromPackage(t *testing.T, path string, info os.FileInfo) map[string][]string { + constructorsPerPackage := make(map[string][]string) + + if info.IsDir() || !strings.HasSuffix(info.Name(), ".go") { + return nil + } + + fset := token.NewFileSet() + node, err := parser.ParseFile(fset, path, nil, parser.ParseComments) + require.NoError(t, err) + + for _, f := range node.Decls { + fn, ok := f.(*ast.FuncDecl) + if !ok || fn.Recv != nil || !strings.HasPrefix(fn.Name.Name, "New") || !strings.HasSuffix(fn.Name.Name, "Cataloger") { + continue + } + + pkg := node.Name.Name + constructorsPerPackage[pkg] = append(constructorsPerPackage[pkg], fn.Name.Name) + } + + return constructorsPerPackage +} + +func assertAllPackageCatalogersRepresented(t *testing.T, constructorsPerPackage map[string]*strset.Set) { + t.Helper() + + contents, err := os.ReadFile(filepath.Join(repoRoot(t), "internal", "task", "package_tasks.go")) + require.NoError(t, err) + + // ensure all packages (keys) are represented in the package_tasks.go file + for pkg, constructors := range constructorsPerPackage { + if !assert.True(t, bytes.Contains(contents, []byte(pkg)), "missing package %q", pkg) { + continue + } + for _, constructor := range constructors.List() { + assert.True(t, bytes.Contains(contents, []byte(constructor)), "missing constructor %q for package %q", constructor, pkg) + } + } + +} + +func repoRoot(t testing.TB) string { + t.Helper() + root, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() + if err != nil { + t.Fatalf("unable to find repo root dir: %+v", err) + } + absRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root))) + if err != nil { + t.Fatal("unable to get abs path to repo root:", err) + } + return absRepoRoot +} + +func mergeConstructors(constructorsPerPackage map[string]*strset.Set, partialResults map[string][]string) map[string]*strset.Set { + for pkg, constructors := range partialResults { + if _, ok := constructorsPerPackage[pkg]; !ok { + constructorsPerPackage[pkg] = strset.New() + } + constructorsPerPackage[pkg].Add(constructors...) + } + + return constructorsPerPackage +}