mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 08:23:15 +01:00
477 lines
12 KiB
Go
477 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// AppConfigField represents an application-level configuration field for catalogers
|
|
type AppConfigField struct {
|
|
Key string // e.g., "golang.search-local-mod-cache-licenses"
|
|
Description string // extracted from DescribeFields() method
|
|
DefaultValue interface{} // extracted from Default*() functions
|
|
}
|
|
|
|
// DiscoverAppConfigs discovers all application-level cataloger configuration fields
|
|
// from the options package
|
|
func DiscoverAppConfigs(repoRoot string) ([]AppConfigField, error) {
|
|
optionsDir := filepath.Join(repoRoot, "cmd", "syft", "internal", "options")
|
|
|
|
// parse all .go files in the options directory to extract configuration fields
|
|
configs := []AppConfigField{}
|
|
|
|
// define the config files we want to parse with their top-level keys
|
|
configFiles := map[string]string{
|
|
"dotnet.go": "dotnet",
|
|
"golang.go": "golang",
|
|
"java.go": "java",
|
|
"javascript.go": "javascript",
|
|
"linux_kernel.go": "linux-kernel",
|
|
"nix.go": "nix",
|
|
"python.go": "python",
|
|
}
|
|
|
|
for filename, topLevelKey := range configFiles {
|
|
filePath := filepath.Join(optionsDir, filename)
|
|
fields, err := extractAppConfigFields(filePath, topLevelKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to extract config from %s: %w", filename, err)
|
|
}
|
|
configs = append(configs, fields...)
|
|
}
|
|
|
|
// sort by key for consistent output
|
|
sort.Slice(configs, func(i, j int) bool {
|
|
return configs[i].Key < configs[j].Key
|
|
})
|
|
|
|
return configs, nil
|
|
}
|
|
|
|
// extractAppConfigFields extracts config fields from an options file
|
|
func extractAppConfigFields(filePath, topLevelKey string) ([]AppConfigField, error) {
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var configs []AppConfigField
|
|
|
|
// find the main config struct (not nested ones)
|
|
configStruct, descriptions := findAppConfigStructAndDescriptions(f, topLevelKey)
|
|
if configStruct == nil {
|
|
return nil, fmt.Errorf("no config struct found in %s", filePath)
|
|
}
|
|
|
|
// extract default values from the default function
|
|
defaults := extractAppDefaultValues(f)
|
|
|
|
// build config fields from struct fields
|
|
for _, field := range configStruct.Fields.List {
|
|
// extract yaml tag to get the field key
|
|
yamlKey := extractYAMLTag(field)
|
|
if yamlKey == "" || yamlKey == "-" {
|
|
continue
|
|
}
|
|
|
|
var fieldName string
|
|
if len(field.Names) > 0 {
|
|
fieldName = field.Names[0].Name
|
|
} else {
|
|
continue
|
|
}
|
|
|
|
// build full key path
|
|
fullKey := topLevelKey + "." + yamlKey
|
|
|
|
// handle nested structs (e.g., golang.MainModuleVersion)
|
|
if isNestedStruct(field.Type) {
|
|
nestedConfigs := extractNestedAppConfigs(f, fullKey, fieldName, field.Type, descriptions, defaults)
|
|
configs = append(configs, nestedConfigs...)
|
|
continue
|
|
}
|
|
|
|
// get description from DescribeFields
|
|
description := descriptions[fieldName]
|
|
|
|
// get default value
|
|
defaultValue := defaults[fieldName]
|
|
|
|
configs = append(configs, AppConfigField{
|
|
Key: fullKey,
|
|
Description: description,
|
|
DefaultValue: defaultValue,
|
|
})
|
|
}
|
|
|
|
return configs, nil
|
|
}
|
|
|
|
// findAppConfigStructAndDescriptions finds the main config struct and extracts field descriptions
|
|
// from the DescribeFields method
|
|
func findAppConfigStructAndDescriptions(f *ast.File, topLevelKey string) (*ast.StructType, map[string]string) {
|
|
expectedName := determineExpectedConfigName(topLevelKey)
|
|
configStruct := findConfigStruct(f, expectedName)
|
|
descriptions := extractDescriptionsFromDescribeFields(f)
|
|
return configStruct, descriptions
|
|
}
|
|
|
|
// determineExpectedConfigName maps the top-level key to the expected config struct name
|
|
func determineExpectedConfigName(topLevelKey string) string {
|
|
// handle special cases first
|
|
switch topLevelKey {
|
|
case "linux-kernel":
|
|
return "linuxKernelConfig"
|
|
case "javascript":
|
|
return "javaScriptConfig"
|
|
default:
|
|
return topLevelKey + "Config"
|
|
}
|
|
}
|
|
|
|
// findConfigStruct searches for the config struct with the expected name in the AST
|
|
func findConfigStruct(f *ast.File, expectedName string) *ast.StructType {
|
|
for _, decl := range f.Decls {
|
|
genDecl, ok := decl.(*ast.GenDecl)
|
|
if !ok || genDecl.Tok != token.TYPE {
|
|
continue
|
|
}
|
|
|
|
for _, spec := range genDecl.Specs {
|
|
typeSpec, ok := spec.(*ast.TypeSpec)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
structType, ok := typeSpec.Type.(*ast.StructType)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if typeSpec.Name.Name == expectedName {
|
|
return structType
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// extractDescriptionsFromDescribeFields extracts field descriptions from the DescribeFields method
|
|
func extractDescriptionsFromDescribeFields(f *ast.File) map[string]string {
|
|
descriptions := make(map[string]string)
|
|
|
|
for _, decl := range f.Decls {
|
|
funcDecl, ok := decl.(*ast.FuncDecl)
|
|
if !ok || funcDecl.Name.Name != "DescribeFields" {
|
|
continue
|
|
}
|
|
|
|
// extract descriptions from descriptions.Add calls
|
|
ast.Inspect(funcDecl.Body, func(n ast.Node) bool {
|
|
callExpr, ok := n.(*ast.CallExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
// check if this is a descriptions.Add call
|
|
selector, ok := callExpr.Fun.(*ast.SelectorExpr)
|
|
if !ok || selector.Sel.Name != "Add" {
|
|
return true
|
|
}
|
|
|
|
// first argument should be a field reference (&o.FieldName or &o.Parent.FieldName)
|
|
if len(callExpr.Args) < 2 {
|
|
return true
|
|
}
|
|
|
|
fieldPath := extractFieldPathFromRef(callExpr.Args[0])
|
|
if fieldPath == "" {
|
|
return true
|
|
}
|
|
|
|
// second argument is the description string
|
|
description := extractStringLiteral(callExpr.Args[1])
|
|
if description != "" {
|
|
description = cleanDescription(description)
|
|
descriptions[fieldPath] = description
|
|
}
|
|
|
|
return true
|
|
})
|
|
}
|
|
|
|
return descriptions
|
|
}
|
|
|
|
// extractNestedAppConfigs handles nested config structs like golang.MainModuleVersion
|
|
func extractNestedAppConfigs(f *ast.File, parentKey, parentFieldName string, fieldType ast.Expr, descriptions map[string]string, defaults map[string]interface{}) []AppConfigField {
|
|
var configs []AppConfigField
|
|
|
|
// find the nested struct type
|
|
var nestedStructName string
|
|
switch t := fieldType.(type) {
|
|
case *ast.Ident:
|
|
nestedStructName = t.Name
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
// find the struct definition
|
|
var nestedStruct *ast.StructType
|
|
for _, decl := range f.Decls {
|
|
genDecl, ok := decl.(*ast.GenDecl)
|
|
if !ok || genDecl.Tok != token.TYPE {
|
|
continue
|
|
}
|
|
|
|
for _, spec := range genDecl.Specs {
|
|
typeSpec, ok := spec.(*ast.TypeSpec)
|
|
if !ok || typeSpec.Name.Name != nestedStructName {
|
|
continue
|
|
}
|
|
|
|
var structOk bool
|
|
nestedStruct, structOk = typeSpec.Type.(*ast.StructType)
|
|
if structOk {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if nestedStruct == nil {
|
|
return nil
|
|
}
|
|
|
|
// extract fields from nested struct
|
|
for _, field := range nestedStruct.Fields.List {
|
|
yamlKey := extractYAMLTag(field)
|
|
if yamlKey == "" || yamlKey == "-" {
|
|
continue
|
|
}
|
|
|
|
var fieldName string
|
|
if len(field.Names) > 0 {
|
|
fieldName = field.Names[0].Name
|
|
} else {
|
|
continue
|
|
}
|
|
|
|
fullKey := parentKey + "." + yamlKey
|
|
|
|
// get description using the nested path (e.g., "MainModuleVersion.FromLDFlags")
|
|
nestedPath := parentFieldName + "." + fieldName
|
|
description := descriptions[nestedPath]
|
|
|
|
// try to get default value from nested defaults
|
|
var defaultValue interface{}
|
|
if nestedDefaults, ok := defaults[parentFieldName].(map[string]interface{}); ok {
|
|
defaultValue = nestedDefaults[fieldName]
|
|
}
|
|
|
|
configs = append(configs, AppConfigField{
|
|
Key: fullKey,
|
|
Description: description,
|
|
DefaultValue: defaultValue,
|
|
})
|
|
}
|
|
|
|
return configs
|
|
}
|
|
|
|
// extractAppDefaultValues extracts default values from the default*Config function
|
|
func extractAppDefaultValues(f *ast.File) map[string]interface{} {
|
|
defaults := make(map[string]interface{})
|
|
|
|
for _, decl := range f.Decls {
|
|
funcDecl, ok := decl.(*ast.FuncDecl)
|
|
if !ok || !strings.HasPrefix(funcDecl.Name.Name, "default") {
|
|
continue
|
|
}
|
|
|
|
// look for return statements that construct the config struct
|
|
ast.Inspect(funcDecl.Body, func(n ast.Node) bool {
|
|
// look for return statements
|
|
returnStmt, ok := n.(*ast.ReturnStmt)
|
|
if !ok || len(returnStmt.Results) == 0 {
|
|
return true
|
|
}
|
|
|
|
// check if returning a struct literal
|
|
compositeLit, ok := returnStmt.Results[0].(*ast.CompositeLit)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
// extract field values from the composite literal
|
|
for _, elt := range compositeLit.Elts {
|
|
kvExpr, ok := elt.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// get field name
|
|
ident, ok := kvExpr.Key.(*ast.Ident)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
fieldName := ident.Name
|
|
|
|
// extract the value
|
|
value := extractAppValue(kvExpr.Value)
|
|
if value != nil {
|
|
defaults[fieldName] = value
|
|
}
|
|
}
|
|
|
|
return true
|
|
})
|
|
}
|
|
|
|
return defaults
|
|
}
|
|
|
|
// extractAppValue extracts a Go value from an AST expression
|
|
func extractAppValue(expr ast.Expr) interface{} {
|
|
switch v := expr.(type) {
|
|
case *ast.BasicLit:
|
|
// string, int, bool literals
|
|
switch v.Kind {
|
|
case token.STRING:
|
|
return strings.Trim(v.Value, `"`)
|
|
case token.INT:
|
|
return v.Value
|
|
case token.FLOAT:
|
|
return v.Value
|
|
}
|
|
case *ast.Ident:
|
|
// boolean values
|
|
if v.Name == "true" {
|
|
return true
|
|
}
|
|
if v.Name == "false" {
|
|
return false
|
|
}
|
|
if v.Name == "nil" {
|
|
return nil
|
|
}
|
|
case *ast.CompositeLit:
|
|
// nested struct literal
|
|
nested := make(map[string]interface{})
|
|
for _, elt := range v.Elts {
|
|
kvExpr, ok := elt.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
continue
|
|
}
|
|
ident, ok := kvExpr.Key.(*ast.Ident)
|
|
if !ok {
|
|
continue
|
|
}
|
|
nested[ident.Name] = extractAppValue(kvExpr.Value)
|
|
}
|
|
if len(nested) > 0 {
|
|
return nested
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// extractYAMLTag extracts the yaml tag value from a struct field
|
|
func extractYAMLTag(field *ast.Field) string {
|
|
if field.Tag == nil {
|
|
return ""
|
|
}
|
|
|
|
tag := strings.Trim(field.Tag.Value, "`")
|
|
tags := reflect.StructTag(tag)
|
|
yamlTag := tags.Get("yaml")
|
|
|
|
// handle tags like "field-name,omitempty"
|
|
if idx := strings.Index(yamlTag, ","); idx != -1 {
|
|
yamlTag = yamlTag[:idx]
|
|
}
|
|
|
|
return yamlTag
|
|
}
|
|
|
|
// extractFieldPathFromRef extracts field path from & o.FieldName or &o.Parent.FieldName expression
|
|
func extractFieldPathFromRef(expr ast.Expr) string {
|
|
unaryExpr, ok := expr.(*ast.UnaryExpr)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
// handle nested field references like &o.MainModuleVersion.FromLDFlags
|
|
var parts []string
|
|
current := unaryExpr.X
|
|
|
|
for {
|
|
selectorExpr, ok := current.(*ast.SelectorExpr)
|
|
if !ok {
|
|
break
|
|
}
|
|
|
|
// add this selector to the path
|
|
parts = append([]string{selectorExpr.Sel.Name}, parts...)
|
|
|
|
// move to the next level
|
|
current = selectorExpr.X
|
|
}
|
|
|
|
// join the parts with dots to create the full path
|
|
// e.g., ["MainModuleVersion", "FromLDFlags"] -> "MainModuleVersion.FromLDFlags"
|
|
if len(parts) > 0 {
|
|
return strings.Join(parts, ".")
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// extractStringLiteral extracts a string value from a BasicLit node
|
|
func extractStringLiteral(expr ast.Expr) string {
|
|
lit, ok := expr.(*ast.BasicLit)
|
|
if !ok || lit.Kind != token.STRING {
|
|
return ""
|
|
}
|
|
|
|
// remove backticks or quotes
|
|
value := strings.Trim(lit.Value, "`\"")
|
|
return value
|
|
}
|
|
|
|
// cleanDescription cleans up multi-line descriptions
|
|
func cleanDescription(desc string) string {
|
|
// replace multiple whitespace with single space
|
|
desc = strings.Join(strings.Fields(desc), " ")
|
|
return strings.TrimSpace(desc)
|
|
}
|
|
|
|
// isNestedStruct checks if a field type is a nested struct (not a pointer or basic type)
|
|
func isNestedStruct(expr ast.Expr) bool {
|
|
switch t := expr.(type) {
|
|
case *ast.Ident:
|
|
// check if it's a struct type (not a basic type)
|
|
// basic types would be: string, int, bool, etc.
|
|
basicTypes := map[string]bool{
|
|
"string": true, "int": true, "int8": true, "int16": true, "int32": true, "int64": true,
|
|
"uint": true, "uint8": true, "uint16": true, "uint32": true, "uint64": true,
|
|
"float32": true, "float64": true, "bool": true, "byte": true, "rune": true,
|
|
}
|
|
return !basicTypes[t.Name]
|
|
case *ast.StarExpr:
|
|
// pointer types are not nested structs for our purposes
|
|
return false
|
|
case *ast.ArrayType, *ast.MapType:
|
|
return false
|
|
default:
|
|
return false
|
|
}
|
|
}
|