Skip to content

Commit

Permalink
Re-introduce linux kernel cataloger (#2526)
Browse files Browse the repository at this point in the history
* re-add linux kernel cataloger

Signed-off-by: Alex Goodman <[email protected]>

* ensure there is at least a directory or image tag on each task

Signed-off-by: Alex Goodman <[email protected]>

* fix CLI tests to account for kernel finding (+2)

Signed-off-by: Alex Goodman <[email protected]>

---------

Signed-off-by: Alex Goodman <[email protected]>
  • Loading branch information
wagoodman authored Jan 22, 2024
1 parent c6ce1de commit 4c77783
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 3 deletions.
10 changes: 9 additions & 1 deletion internal/task/package_tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
}
2 changes: 1 addition & 1 deletion test/cli/scan_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion test/cli/trait_assertions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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 {
Expand Down
201 changes: 201 additions & 0 deletions test/integration/package_catalogers_represented_test.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 4c77783

Please sign in to comment.