mirror of
https://github.com/anchore/syft.git
synced 2026-11-03 16:06:36 +01:00
Add experimental cataloger capabilities command (#4317)
* add info command from generated capabilities Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * correct gentoo and arch ecosystems Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * rename os pkg types Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * better binary cataloger description Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * expose metadata and pacakge types in json Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * expose json schema types Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add completeness tests for metadata types Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * latest generation Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix linting Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * improve testing a docs Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix tests and linting Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * restore goreleaser config Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * tweak diagram Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix pdm Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * chore: java binary data Signed-off-by: Keith Zantow <kzantow@gmail.com> * new capability descriptions for gguf and python Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * correct poetry lock integrity hash claim Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix compile error Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: remove purl version from overrides Signed-off-by: Keith Zantow <kzantow@gmail.com> * fix lua deps ref Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * keep gguf as ai ecosystem Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * split packages.yaml to multiple files by go package Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * ensure tests do not use go test cache Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * sort json output for info command Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * docs: fix ocaml, php, and portage capabilities yaml Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com> * chore: update erlang capabilities Signed-off-by: Keith Zantow <kzantow@gmail.com> * chore: update java capabilities Signed-off-by: Keith Zantow <kzantow@gmail.com> * chore: update javascript capabilities Signed-off-by: Keith Zantow <kzantow@gmail.com> * chore: update linux kernel capabilities Signed-off-by: Keith Zantow <kzantow@gmail.com> * remove missing tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix package.yaml references Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * revert license list change Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * check for drift in capability descriptions Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * regenerate capabilities Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * test cleanup Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * use fixture cache in static analysis Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * claim fixtures pre-req for cap generation Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * update documentation with correct regeneration procedure Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * chore: ruby-gemspec-cataloger finds no dependencies Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com> * chore: fix python docs and config comment Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com> * chore: commit re-generated java yaml Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com> * add cataloger selection to caps command Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * re-generate cap yamls Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix tests for cataloger selection Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix cli test Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add missing tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix linting Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * rename cmd to `cataloger info` Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * [wip] change capability description locations Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * [wip] continued Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * [wip] adjust for import cycles Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * correct docs Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix linting Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> Signed-off-by: Keith Zantow <kzantow@gmail.com> Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com> Co-authored-by: Keith Zantow <kzantow@gmail.com> Co-authored-by: Will Murphy <willmurphyscode@users.noreply.github.com>
This commit is contained in:
co-authored by
Keith Zantow
Will Murphy
parent
ae1a247f3d
commit
b3c70da3ea
@@ -0,0 +1,339 @@
|
||||
// this file links catalogers to their configuration structs by analyzing constructor function signatures to determine which config struct each cataloger uses.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LinkCatalogersToConfigs analyzes cataloger constructor functions to determine which config struct
|
||||
// each cataloger uses. Returns a map where key is the cataloger name (e.g., "go-module-binary-cataloger")
|
||||
// and value is the config struct reference (e.g., "golang.CatalogerConfig").
|
||||
// Returns empty string for catalogers that don't take a config parameter.
|
||||
func LinkCatalogersToConfigs(repoRoot string) (map[string]string, error) {
|
||||
catalogerRoot := filepath.Join(repoRoot, "syft", "pkg", "cataloger")
|
||||
return LinkCatalogersToConfigsFromPath(catalogerRoot, repoRoot)
|
||||
}
|
||||
|
||||
// LinkCatalogersToConfigsFromPath analyzes cataloger constructor functions in the specified directory
|
||||
// to determine which config struct each cataloger uses. This is the parameterized version that allows
|
||||
// testing with custom fixture directories.
|
||||
// Returns a map where key is the cataloger name (e.g., "go-module-binary-cataloger")
|
||||
// and value is the config struct reference (e.g., "golang.CatalogerConfig").
|
||||
// Returns empty string for catalogers that don't take a config parameter.
|
||||
// The baseRoot parameter is used for relative path calculation to determine package names.
|
||||
func LinkCatalogersToConfigsFromPath(catalogerRoot, baseRoot string) (map[string]string, error) {
|
||||
// find all .go files under the cataloger root recursively
|
||||
var files []string
|
||||
err := filepath.Walk(catalogerRoot, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() && strings.HasSuffix(path, ".go") && !strings.HasSuffix(path, "_test.go") {
|
||||
files = append(files, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to walk cataloger directory: %w", err)
|
||||
}
|
||||
|
||||
linkages := make(map[string]string)
|
||||
|
||||
for _, file := range files {
|
||||
links, err := linkCatalogersInFile(file, baseRoot)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse %s: %w", file, err)
|
||||
}
|
||||
|
||||
for catalogerName, configType := range links {
|
||||
if existing, ok := linkages[catalogerName]; ok && existing != configType {
|
||||
return nil, fmt.Errorf("conflicting config types for cataloger %q: %s vs %s", catalogerName, existing, configType)
|
||||
}
|
||||
linkages[catalogerName] = configType
|
||||
}
|
||||
}
|
||||
|
||||
return linkages, nil
|
||||
}
|
||||
|
||||
func linkCatalogersInFile(path, repoRoot string) (map[string]string, error) {
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// extract package name from file path
|
||||
relPath, err := filepath.Rel(repoRoot, path)
|
||||
if err != nil {
|
||||
relPath = path
|
||||
}
|
||||
packageName := extractPackageNameFromPath(relPath)
|
||||
if packageName == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
linkages := make(map[string]string)
|
||||
|
||||
// find all function declarations
|
||||
for _, decl := range f.Decls {
|
||||
funcDecl, ok := decl.(*ast.FuncDecl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// check if function name matches New*Cataloger pattern
|
||||
if !strings.HasPrefix(funcDecl.Name.Name, "New") || !strings.HasSuffix(funcDecl.Name.Name, "Cataloger") {
|
||||
continue
|
||||
}
|
||||
|
||||
// check if return type is pkg.Cataloger
|
||||
if !returnsPackageCataloger(funcDecl) {
|
||||
continue
|
||||
}
|
||||
|
||||
// extract the cataloger name from the function body
|
||||
catalogerName := extractCatalogerName(funcDecl, f, path, repoRoot)
|
||||
if catalogerName == "" {
|
||||
// couldn't determine cataloger name, skip
|
||||
continue
|
||||
}
|
||||
|
||||
// extract config type from function parameters
|
||||
configType := extractConfigParameter(funcDecl, packageName)
|
||||
|
||||
// store the linkage (empty string means no config)
|
||||
linkages[catalogerName] = configType
|
||||
}
|
||||
|
||||
return linkages, nil
|
||||
}
|
||||
|
||||
// extractCatalogerName extracts the cataloger name from the function body.
|
||||
// It looks for:
|
||||
// 1. generic.NewCataloger("name") calls
|
||||
// 2. Cataloger implementations that define Name() method
|
||||
// 3. Hardcoded name constants
|
||||
func extractCatalogerName(funcDecl *ast.FuncDecl, file *ast.File, filePath, repoRoot string) string {
|
||||
if funcDecl.Body == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
ctx := &parseContext{
|
||||
file: file,
|
||||
filePath: filePath,
|
||||
repoRoot: repoRoot,
|
||||
}
|
||||
|
||||
var catalogerName string
|
||||
|
||||
// walk the function body to find generic.NewCataloger call or name assignment
|
||||
ast.Inspect(funcDecl.Body, func(n ast.Node) bool {
|
||||
// look for call expressions
|
||||
callExpr, ok := n.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// check if this is a call to generic.NewCataloger
|
||||
if isGenericNewCatalogerCall(callExpr) {
|
||||
// extract the cataloger name from the first argument
|
||||
if len(callExpr.Args) > 0 {
|
||||
// handle string literals
|
||||
if lit, ok := callExpr.Args[0].(*ast.BasicLit); ok && lit.Kind == token.STRING {
|
||||
catalogerName = strings.Trim(lit.Value, `"`)
|
||||
return false // found it, stop searching
|
||||
}
|
||||
// handle constant identifiers
|
||||
if ident, ok := callExpr.Args[0].(*ast.Ident); ok {
|
||||
catalogerName = resolveLocalConstant(ident.Name, ctx)
|
||||
return false // found it, stop searching
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// if we didn't find a generic.NewCataloger call, try to infer from custom cataloger
|
||||
if catalogerName == "" {
|
||||
catalogerName = inferCatalogerNameFromCustomImpl(funcDecl, file, ctx)
|
||||
}
|
||||
|
||||
return catalogerName
|
||||
}
|
||||
|
||||
// inferCatalogerNameFromCustomImpl tries to infer the cataloger name from custom cataloger implementations
|
||||
// by looking for Name() method implementations or hardcoded name variables
|
||||
func inferCatalogerNameFromCustomImpl(funcDecl *ast.FuncDecl, file *ast.File, ctx *parseContext) string {
|
||||
typeName := extractReturnTypeName(funcDecl)
|
||||
if typeName == "" {
|
||||
return ""
|
||||
}
|
||||
return findNameMethodReturn(file, typeName, ctx)
|
||||
}
|
||||
|
||||
// extractReturnTypeName extracts the type name from the return statement of a constructor function
|
||||
func extractReturnTypeName(funcDecl *ast.FuncDecl) string {
|
||||
var typeName string
|
||||
|
||||
ast.Inspect(funcDecl.Body, func(n ast.Node) bool {
|
||||
returnStmt, ok := n.(*ast.ReturnStmt)
|
||||
if !ok || len(returnStmt.Results) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
result := returnStmt.Results[0]
|
||||
|
||||
// handle &Type{...}
|
||||
if unaryExpr, ok := result.(*ast.UnaryExpr); ok && unaryExpr.Op == token.AND {
|
||||
if compLit, ok := unaryExpr.X.(*ast.CompositeLit); ok {
|
||||
if ident, ok := compLit.Type.(*ast.Ident); ok {
|
||||
typeName = ident.Name
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle Type{...}
|
||||
if compLit, ok := result.(*ast.CompositeLit); ok {
|
||||
if ident, ok := compLit.Type.(*ast.Ident); ok {
|
||||
typeName = ident.Name
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return typeName
|
||||
}
|
||||
|
||||
// findNameMethodReturn finds the Name() method for the given type and extracts its return value
|
||||
func findNameMethodReturn(file *ast.File, typeName string, ctx *parseContext) string {
|
||||
for _, decl := range file.Decls {
|
||||
funcDecl, ok := decl.(*ast.FuncDecl)
|
||||
if !ok || funcDecl.Name.Name != "Name" {
|
||||
continue
|
||||
}
|
||||
|
||||
if funcDecl.Recv == nil || len(funcDecl.Recv.List) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
recvTypeName := extractReceiverTypeName(funcDecl.Recv.List[0].Type)
|
||||
if recvTypeName != typeName {
|
||||
continue
|
||||
}
|
||||
|
||||
// found the Name() method, extract the return value
|
||||
if funcDecl.Body != nil {
|
||||
if name := extractNameFromMethodBody(funcDecl.Body, ctx); name != "" {
|
||||
return name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractReceiverTypeName extracts the type name from a receiver type expression
|
||||
func extractReceiverTypeName(recvType ast.Expr) string {
|
||||
// handle both T and *T receivers
|
||||
if ident, ok := recvType.(*ast.Ident); ok {
|
||||
return ident.Name
|
||||
}
|
||||
if starExpr, ok := recvType.(*ast.StarExpr); ok {
|
||||
if ident, ok := starExpr.X.(*ast.Ident); ok {
|
||||
return ident.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractNameFromMethodBody extracts the cataloger name from a Name() method body
|
||||
func extractNameFromMethodBody(body *ast.BlockStmt, ctx *parseContext) string {
|
||||
var name string
|
||||
ast.Inspect(body, func(n ast.Node) bool {
|
||||
returnStmt, ok := n.(*ast.ReturnStmt)
|
||||
if !ok || len(returnStmt.Results) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// handle string literal
|
||||
if lit, ok := returnStmt.Results[0].(*ast.BasicLit); ok && lit.Kind == token.STRING {
|
||||
name = strings.Trim(lit.Value, `"`)
|
||||
return false
|
||||
}
|
||||
|
||||
// handle constant identifier (e.g., pomCatalogerName)
|
||||
if ident, ok := returnStmt.Results[0].(*ast.Ident); ok {
|
||||
name = resolveLocalConstant(ident.Name, ctx)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
return name
|
||||
}
|
||||
|
||||
// extractConfigParameter extracts the config type from the first parameter of a cataloger constructor.
|
||||
// Returns empty string if no config parameter is found.
|
||||
// Returns format: "packageName.StructName" (e.g., "golang.CatalogerConfig")
|
||||
func extractConfigParameter(funcDecl *ast.FuncDecl, localPackageName string) string {
|
||||
if funcDecl.Type.Params == nil || len(funcDecl.Type.Params.List) == 0 {
|
||||
// no parameters, no config
|
||||
return ""
|
||||
}
|
||||
|
||||
// check the first parameter
|
||||
firstParam := funcDecl.Type.Params.List[0]
|
||||
if firstParam.Type == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// extract the type name
|
||||
configType := extractConfigTypeName(firstParam.Type, localPackageName)
|
||||
|
||||
// filter out non-config types
|
||||
if configType != "" && !looksLikeConfigType(configType) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return configType
|
||||
}
|
||||
|
||||
// extractConfigTypeName extracts the full type name from a parameter type expression
|
||||
func extractConfigTypeName(typeExpr ast.Expr, localPackageName string) string {
|
||||
switch t := typeExpr.(type) {
|
||||
case *ast.Ident:
|
||||
// local type (e.g., CatalogerConfig)
|
||||
return localPackageName + "." + t.Name
|
||||
case *ast.SelectorExpr:
|
||||
// imported type (e.g., java.ArchiveCatalogerConfig)
|
||||
if pkgIdent, ok := t.X.(*ast.Ident); ok {
|
||||
return pkgIdent.Name + "." + t.Sel.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// looksLikeConfigType checks if a type name looks like a configuration struct
|
||||
func looksLikeConfigType(typeName string) bool {
|
||||
// remove package prefix
|
||||
parts := strings.Split(typeName, ".")
|
||||
if len(parts) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
structName := parts[len(parts)-1]
|
||||
|
||||
// check for common config patterns
|
||||
return strings.Contains(structName, "Config")
|
||||
}
|
||||
Reference in New Issue
Block a user