fix linting

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2025-10-28 10:38:24 -04:00
parent abfe73b3da
commit 0dd906b071
10 changed files with 481 additions and 1109 deletions

View File

@ -1,6 +1,8 @@
package commands
import (
"os"
"github.com/spf13/cobra"
"github.com/anchore/clio"
@ -14,8 +16,16 @@ func Cataloger(app clio.Application) *cobra.Command {
cmd.AddCommand(
CatalogerList(app),
CatalogerInfo(app),
)
// only add cataloger info command if experimental capabilities feature is enabled
if isCapabilitiesExperimentEnabled() {
cmd.AddCommand(CatalogerInfo(app))
}
return cmd
}
func isCapabilitiesExperimentEnabled() bool {
return os.Getenv("SYFT_EXP_CAPABILITIES") == "true"
}

View File

@ -27,6 +27,50 @@ var (
criteriaMargin = 10
)
// types for JSON cataloger info output
type (
configFieldInfo struct {
Key string `json:"key"`
Description string `json:"description"`
AppKey string `json:"app_key,omitempty"`
}
configInfo struct {
Type string `json:"type"`
Fields []configFieldInfo `json:"fields,omitempty"`
}
detectorPackageInfo struct {
Class string `json:"class"`
Name string `json:"name"`
PURL string `json:"purl"`
CPEs []string `json:"cpes"`
Type string `json:"type"`
}
patternInfo struct {
Method string `json:"method"`
Criteria []string `json:"criteria"`
Conditions []capabilities.DetectorCondition `json:"conditions,omitempty"`
Packages []detectorPackageInfo `json:"packages,omitempty"`
Comment string `json:"comment,omitempty"`
PackageTypes []string `json:"package_types,omitempty"`
JSONSchemaTypes []string `json:"json_schema_types,omitempty"`
Capabilities capabilities.CapabilitySet `json:"capabilities,omitempty"`
}
catalogerInfo struct {
Ecosystem string `json:"ecosystem,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
Selectors []string `json:"selectors,omitempty"`
Deprecated bool `json:"deprecated,omitempty"`
Patterns []patternInfo `json:"patterns,omitempty"`
Capabilities capabilities.CapabilitySet `json:"capabilities,omitempty"`
Config *configInfo `json:"config,omitempty"`
}
)
type catalogerInfoOptions struct {
Output string `yaml:"output" json:"output" mapstructure:"output"`
Names []string // cataloger names from args
@ -73,6 +117,7 @@ func runCatalogerInfo(opts *catalogerInfoOptions) error {
}
bus.Report(report)
bus.Notify("Note: the `cataloger info` command is experimental and may change or be removed without notice. Do not depend on its output in production systems.")
return nil
}
@ -104,47 +149,6 @@ func catalogerInfoReport(opts *catalogerInfoOptions, doc *capabilities.Document,
}
func renderCatalogerInfoJSON(doc *capabilities.Document, catalogers []capabilities.CatalogerEntry) (string, error) {
type configFieldInfo struct {
Key string `json:"key"`
Description string `json:"description"`
AppKey string `json:"app_key,omitempty"`
}
type configInfo struct {
Type string `json:"type"`
Fields []configFieldInfo `json:"fields,omitempty"`
}
type detectorPackageInfo struct {
Class string `json:"class"`
Name string `json:"name"`
PURL string `json:"purl"`
CPEs []string `json:"cpes"`
Type string `json:"type"`
}
type patternInfo struct {
Method string `json:"method"`
Criteria []string `json:"criteria"`
Conditions []capabilities.DetectorCondition `json:"conditions,omitempty"`
Packages []detectorPackageInfo `json:"packages,omitempty"`
Comment string `json:"comment,omitempty"`
PackageTypes []string `json:"package_types,omitempty"`
JSONSchemaTypes []string `json:"json_schema_types,omitempty"`
Capabilities capabilities.CapabilitySet `json:"capabilities,omitempty"`
}
type catalogerInfo struct {
Ecosystem string `json:"ecosystem,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
Selectors []string `json:"selectors,omitempty"`
Deprecated bool `json:"deprecated,omitempty"`
Patterns []patternInfo `json:"patterns,omitempty"`
Capabilities capabilities.CapabilitySet `json:"capabilities,omitempty"`
Config *configInfo `json:"config,omitempty"`
}
type document struct {
Catalogers []catalogerInfo `json:"catalogers"`
}
@ -153,92 +157,23 @@ func renderCatalogerInfoJSON(doc *capabilities.Document, catalogers []capabiliti
for _, cat := range catalogers {
info := catalogerInfo{
Ecosystem: cat.Ecosystem,
Name: cat.Name,
Type: cat.Type,
Selectors: cat.Selectors,
Ecosystem: cat.Ecosystem,
Name: cat.Name,
Type: cat.Type,
Selectors: cat.Selectors,
Deprecated: isDeprecatedCataloger(cat.Selectors),
}
// check if cataloger is deprecated based on selectors
for _, selector := range cat.Selectors {
if selector == "deprecated" {
info.Deprecated = true
break
}
}
for _, parser := range cat.Parsers {
// convert detector packages
var pkgs []detectorPackageInfo
for _, pkg := range parser.Detector.Packages {
pkgs = append(pkgs, detectorPackageInfo{
Class: pkg.Class,
Name: pkg.Name,
PURL: pkg.PURL,
CPEs: pkg.CPEs,
Type: pkg.Type,
})
}
pi := patternInfo{
Method: string(parser.Detector.Method),
Criteria: parser.Detector.Criteria,
Conditions: parser.Detector.Conditions,
Packages: pkgs,
Comment: parser.Detector.Comment,
PackageTypes: parser.PackageTypes,
JSONSchemaTypes: parser.JSONSchemaTypes,
Capabilities: parser.Capabilities,
}
info.Patterns = append(info.Patterns, pi)
}
// convert parsers to patterns if available
info.Patterns = convertParsersToPatterns(cat.Parsers)
// if no parsers, use detectors instead
if len(info.Patterns) == 0 {
info.Capabilities = cat.Capabilities
for _, det := range cat.Detectors {
// convert detector packages
var pkgs []detectorPackageInfo
for _, pkg := range det.Packages {
pkgs = append(pkgs, detectorPackageInfo{
Class: pkg.Class,
Name: pkg.Name,
PURL: pkg.PURL,
CPEs: pkg.CPEs,
Type: pkg.Type,
})
}
pi := patternInfo{
Method: string(det.Method),
Criteria: det.Criteria,
Conditions: det.Conditions,
Packages: pkgs,
Comment: det.Comment,
PackageTypes: cat.PackageTypes,
JSONSchemaTypes: cat.JSONSchemaTypes,
}
info.Patterns = append(info.Patterns, pi)
}
info.Patterns = convertDetectorsToPatterns(cat.Detectors, cat.PackageTypes, cat.JSONSchemaTypes)
}
// add config information
if cat.Config != "" {
if configEntry, ok := doc.Configs[cat.Config]; ok {
cfg := &configInfo{
Type: cat.Config,
}
for _, field := range configEntry.Fields {
cfg.Fields = append(cfg.Fields, configFieldInfo{
Key: field.Key,
Description: field.Description,
AppKey: field.AppKey,
})
}
info.Config = cfg
}
}
info.Config = getConfigInfoFromDocument(doc, cat.Config)
docOut.Catalogers = append(docOut.Catalogers, info)
}
@ -247,6 +182,88 @@ func renderCatalogerInfoJSON(doc *capabilities.Document, catalogers []capabiliti
return string(by), err
}
// isDeprecatedCataloger checks if a cataloger is deprecated based on its selectors
func isDeprecatedCataloger(selectors []string) bool {
for _, selector := range selectors {
if selector == "deprecated" {
return true
}
}
return false
}
// convertDetectorPackages converts detector package info to the JSON output format
func convertDetectorPackages(pkgs []capabilities.DetectorPackageInfo) []detectorPackageInfo {
var result []detectorPackageInfo
for _, pkg := range pkgs {
result = append(result, detectorPackageInfo{
Class: pkg.Class,
Name: pkg.Name,
PURL: pkg.PURL,
CPEs: pkg.CPEs,
Type: pkg.Type,
})
}
return result
}
// convertParsersToPatterns converts parser entries to pattern info for JSON output
func convertParsersToPatterns(parsers []capabilities.Parser) []patternInfo {
var patterns []patternInfo
for _, parser := range parsers {
patterns = append(patterns, patternInfo{
Method: string(parser.Detector.Method),
Criteria: parser.Detector.Criteria,
Conditions: parser.Detector.Conditions,
Packages: convertDetectorPackages(parser.Detector.Packages),
Comment: parser.Detector.Comment,
PackageTypes: parser.PackageTypes,
JSONSchemaTypes: parser.JSONSchemaTypes,
Capabilities: parser.Capabilities,
})
}
return patterns
}
// convertDetectorsToPatterns converts detector entries to pattern info for JSON output (for non-parser catalogers)
func convertDetectorsToPatterns(detectors []capabilities.Detector, packageTypes, jsonSchemaTypes []string) []patternInfo {
var patterns []patternInfo
for _, det := range detectors {
patterns = append(patterns, patternInfo{
Method: string(det.Method),
Criteria: det.Criteria,
Conditions: det.Conditions,
Packages: convertDetectorPackages(det.Packages),
Comment: det.Comment,
PackageTypes: packageTypes,
JSONSchemaTypes: jsonSchemaTypes,
})
}
return patterns
}
// getConfigInfoFromDocument retrieves config info from the capabilities document
func getConfigInfoFromDocument(doc *capabilities.Document, configType string) *configInfo {
if configType == "" {
return nil
}
configEntry, ok := doc.Configs[configType]
if !ok {
return nil
}
cfg := &configInfo{
Type: configType,
}
for _, field := range configEntry.Fields {
cfg.Fields = append(cfg.Fields, configFieldInfo{
Key: field.Key,
Description: field.Description,
AppKey: field.AppKey,
})
}
return cfg
}
func renderCatalogerInfoTable(_ *capabilities.Document, catalogers []capabilities.CatalogerEntry) string {
if len(catalogers) == 0 {
return noStyle.Render("No catalogers found")
@ -391,48 +408,11 @@ func extractArrayCapability(caps capabilities.CapabilitySet, name string) string
func extractNodesCapability(caps capabilities.CapabilitySet) string {
for _, cap := range caps {
if cap.Name == "dependency.depth" {
// handle various array types
switch v := cap.Default.(type) {
case []string:
if len(v) == 0 {
return noStyle.Render("·")
}
// check if both direct and indirect are present
hasDirect := false
hasIndirect := false
for _, item := range v {
if item == "direct" {
hasDirect = true
}
if item == "indirect" {
hasIndirect = true
}
}
if hasDirect && hasIndirect {
return "transitive"
}
return strings.Join(v, ", ")
return formatDepthStringArray(v)
case []interface{}:
if len(v) == 0 {
return noStyle.Render("·")
}
hasDirect := false
hasIndirect := false
strs := make([]string, 0, len(v))
for _, item := range v {
str := fmt.Sprintf("%v", item)
strs = append(strs, str)
if str == "direct" {
hasDirect = true
}
if str == "indirect" {
hasIndirect = true
}
}
if hasDirect && hasIndirect {
return "transitive"
}
return strings.Join(strs, ", ")
return formatDepthInterfaceArray(v)
}
return noStyle.Render("·")
}
@ -440,6 +420,47 @@ func extractNodesCapability(caps capabilities.CapabilitySet) string {
return noStyle.Render("·")
}
// formatDepthStringArray formats a []string dependency depth value
func formatDepthStringArray(v []string) string {
if len(v) == 0 {
return noStyle.Render("·")
}
if hasBothDirectAndIndirect(v) {
return "transitive"
}
return strings.Join(v, ", ")
}
// formatDepthInterfaceArray formats a []interface{} dependency depth value
func formatDepthInterfaceArray(v []interface{}) string {
if len(v) == 0 {
return noStyle.Render("·")
}
strs := make([]string, 0, len(v))
for _, item := range v {
strs = append(strs, fmt.Sprintf("%v", item))
}
if hasBothDirectAndIndirect(strs) {
return "transitive"
}
return strings.Join(strs, ", ")
}
// hasBothDirectAndIndirect checks if a slice contains both "direct" and "indirect" strings
func hasBothDirectAndIndirect(items []string) bool {
hasDirect := false
hasIndirect := false
for _, item := range items {
if item == "direct" {
hasDirect = true
}
if item == "indirect" {
hasIndirect = true
}
}
return hasDirect && hasIndirect
}
func formatCriteria(detectors []capabilities.Detector) string {
var allCriteria []string
methods := strset.New()

View File

@ -1,720 +1 @@
package commands
import (
"encoding/json"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/internal/capabilities"
)
func TestParseEnrichmentMode(t *testing.T) {
tests := []struct {
name string
input string
want capabilities.EnrichmentMode
wantErr require.ErrorAssertionFunc
}{
{
name: "offline mode",
input: "offline",
want: capabilities.OfflineMode,
},
{
name: "online mode",
input: "online",
want: capabilities.OnlineMode,
},
{
name: "tool-execution mode",
input: "tool-execution",
want: capabilities.ToolExecutionMode,
},
{
name: "invalid mode",
input: "invalid",
wantErr: require.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = require.NoError
}
got, err := parseEnrichmentMode(tt.input)
tt.wantErr(t, err)
if err != nil {
return
}
require.Equal(t, tt.want, got)
})
}
}
func TestFilterCatalogersByName(t *testing.T) {
catalogers := []capabilities.CatalogerEntry{
{Name: "cataloger-a"},
{Name: "cataloger-b"},
{Name: "cataloger-c"},
}
tests := []struct {
name string
names []string
wantNames []string
}{
{
name: "filter single cataloger",
names: []string{"cataloger-a"},
wantNames: []string{"cataloger-a"},
},
{
name: "filter multiple catalogers",
names: []string{"cataloger-a", "cataloger-c"},
wantNames: []string{"cataloger-a", "cataloger-c"},
},
{
name: "filter non-existent cataloger",
names: []string{"cataloger-x"},
wantNames: []string{},
},
{
name: "empty filter returns empty",
names: []string{},
wantNames: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := filterCatalogersByName(catalogers, tt.names)
var gotNames []string
for _, cat := range got {
gotNames = append(gotNames, cat.Name)
}
if tt.wantNames == nil {
tt.wantNames = []string{}
}
if gotNames == nil {
gotNames = []string{}
}
require.Equal(t, tt.wantNames, gotNames)
})
}
}
func TestFormatCriteria(t *testing.T) {
tests := []struct {
name string
detectors []capabilities.Detector
want string
}{
{
name: "glob method - no parenthetical",
detectors: []capabilities.Detector{
{
Method: capabilities.GlobDetection,
Criteria: []string{"**/*.jar", "**/*.war"},
},
},
want: "**/*.jar, **/*.war",
},
{
name: "path method - with parenthetical",
detectors: []capabilities.Detector{
{
Method: capabilities.PathDetection,
Criteria: []string{"/usr/bin/python"},
},
},
want: "/usr/bin/python (path)",
},
{
name: "mimetype method - with parenthetical",
detectors: []capabilities.Detector{
{
Method: capabilities.MIMETypeDetection,
Criteria: []string{"application/x-executable"},
},
},
want: "application/x-executable (mimetype)",
},
{
name: "multiple criteria with non-glob method",
detectors: []capabilities.Detector{
{
Method: capabilities.PathDetection,
Criteria: []string{"/bin/sh", "/bin/bash"},
},
},
want: "/bin/sh, /bin/bash (path)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := formatCriteria(tt.detectors)
require.Equal(t, tt.want, got)
})
}
}
func TestFormatBool(t *testing.T) {
trueVal := true
falseVal := false
tests := []struct {
name string
input *bool
want string
}{
{
name: "true renders check mark",
input: &trueVal,
want: "✔",
},
{
name: "false renders dot",
input: &falseVal,
want: "·",
},
{
name: "nil renders dash",
input: nil,
want: "-",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := formatBool(tt.input)
// strip any styling to check the core character
require.Contains(t, got, tt.want)
})
}
}
func TestFormatStringSlice(t *testing.T) {
tests := []struct {
name string
input []string
want string
}{
{
name: "empty slice",
input: []string{},
want: "",
},
{
name: "single item",
input: []string{"direct"},
want: "direct",
},
{
name: "multiple items",
input: []string{"direct", "indirect"},
want: "direct, indirect",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := formatStringSlice(tt.input)
require.Equal(t, tt.want, got)
})
}
}
func TestBuildTableRow(t *testing.T) {
trueVal := true
falseVal := false
tests := []struct {
name string
catalogerName string
selectors string
capability *capabilities.Capability
wantContains []string
}{
{
name: "nil capability shows defaults",
catalogerName: "test-cataloger",
selectors: "**/*.txt",
capability: nil,
wantContains: []string{"test-cataloger", "**/*.txt", "-"},
},
{
name: "capability with all fields",
catalogerName: "python-cataloger",
selectors: "**/setup.py",
capability: &capabilities.Capability{
License: &trueVal,
Dependencies: &capabilities.DependencyCapabilities{
Depth: []string{"direct", "indirect"},
Edges: "complete",
Kinds: []string{"runtime", "dev"},
},
PackageManager: &capabilities.PackageManagerCapabilities{
Files: &capabilities.FileCapabilities{
Listing: &trueVal,
Digests: &trueVal,
},
PackageIntegrityHash: &trueVal,
},
},
wantContains: []string{
"python-cataloger",
"**/setup.py",
"✔",
"complete",
"runtime, dev",
},
},
{
name: "capability with partial fields",
catalogerName: "minimal-cataloger",
selectors: "N/A",
capability: &capabilities.Capability{
License: &falseVal,
Dependencies: &capabilities.DependencyCapabilities{
Depth: []string{"direct"},
Edges: "flat",
Kinds: []string{},
},
PackageManager: &capabilities.PackageManagerCapabilities{
Files: &capabilities.FileCapabilities{
Listing: &falseVal,
Digests: &falseVal,
},
PackageIntegrityHash: &falseVal,
},
},
wantContains: []string{
"minimal-cataloger",
"N/A",
"direct",
"flat",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
row := buildTableRow("test-ecosystem", tt.catalogerName, tt.selectors, tt.capability)
// convert row to string representation for checking
var rowStr strings.Builder
for _, cell := range row {
rowStr.WriteString(cell)
rowStr.WriteString(" ")
}
for _, want := range tt.wantContains {
require.Contains(t, rowStr.String(), want)
}
})
}
}
func TestRenderCatalogerInfoJSON(t *testing.T) {
trueVal := true
falseVal := false
catalogers := []capabilities.CatalogerEntry{
{
Ecosystem: "test",
Name: "test-generic-cataloger",
Type: "generic",
Parsers: []capabilities.Parser{
{
ParserFunction: "parseTest",
Detector: capabilities.Detector{
Method: capabilities.GlobDetection,
Criteria: []string{"**/*.test"},
},
Capabilities: map[capabilities.EnrichmentMode]*capabilities.Capability{
capabilities.OfflineMode: {
License: &trueVal,
Dependencies: &capabilities.DependencyCapabilities{
Depth: []string{"direct"},
Edges: "flat",
Kinds: []string{"runtime"},
},
PackageManager: &capabilities.PackageManagerCapabilities{
Files: &capabilities.FileCapabilities{
Listing: &trueVal,
Digests: &trueVal,
},
PackageIntegrityHash: &falseVal,
},
},
},
},
},
},
{
Ecosystem: "test",
Name: "test-custom-cataloger",
Type: "custom",
Capabilities: map[capabilities.EnrichmentMode]*capabilities.Capability{
capabilities.OfflineMode: {
License: &falseVal,
Dependencies: &capabilities.DependencyCapabilities{
Depth: []string{},
Edges: "",
Kinds: []string{},
},
PackageManager: &capabilities.PackageManagerCapabilities{
Files: &capabilities.FileCapabilities{
Listing: &falseVal,
Digests: &falseVal,
},
PackageIntegrityHash: &falseVal,
},
},
},
},
}
tests := []struct {
name string
mode capabilities.EnrichmentMode
wantErr require.ErrorAssertionFunc
}{
{
name: "offline mode",
mode: capabilities.OfflineMode,
},
{
name: "online mode with no capabilities",
mode: capabilities.OnlineMode,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = require.NoError
}
doc := &capabilities.Document{
Catalogers: catalogers,
}
got, err := renderCatalogerInfoJSON(doc, catalogers, tt.mode)
tt.wantErr(t, err)
if err != nil {
return
}
// verify it's valid JSON
var result map[string]interface{}
err = json.Unmarshal([]byte(got), &result)
require.NoError(t, err)
// verify structure
require.Contains(t, result, "mode")
require.Contains(t, result, "catalogers")
require.Equal(t, string(tt.mode), result["mode"])
// verify capability_format field is present
catalogersList := result["catalogers"].([]interface{})
require.Greater(t, len(catalogersList), 0)
for _, cat := range catalogersList {
catMap := cat.(map[string]interface{})
require.Contains(t, catMap, "capability_format")
}
})
}
}
func TestRenderCatalogerInfoJSONWithV2Capabilities(t *testing.T) {
// test cataloger with capabilities-v2
catalogers := []capabilities.CatalogerEntry{
{
Ecosystem: "test",
Name: "test-v2-cataloger",
Type: "custom",
CapabilitiesV2: capabilities.CapabilitySet{
{
Name: "license",
Default: false,
Conditions: []capabilities.CapabilityCondition{
{
When: map[string]interface{}{"SearchRemoteLicenses": true},
Value: true,
Comment: "License info fetched from registry",
},
},
},
{
Name: "dependency.depth",
Default: []string{"direct", "indirect"},
},
},
},
}
doc := &capabilities.Document{
Catalogers: catalogers,
}
got, err := renderCatalogerInfoJSON(doc, catalogers, capabilities.OfflineMode)
require.NoError(t, err)
// verify it's valid JSON
var result map[string]interface{}
err = json.Unmarshal([]byte(got), &result)
require.NoError(t, err)
// verify structure includes capabilities_v2
catalogersList := result["catalogers"].([]interface{})
require.Len(t, catalogersList, 1)
cat := catalogersList[0].(map[string]interface{})
require.Equal(t, "v2", cat["capability_format"])
require.Contains(t, cat, "capabilities_v2")
// verify capabilities_v2 structure
capsV2 := cat["capabilities_v2"].([]interface{})
require.Len(t, capsV2, 2)
// check first capability field
field1 := capsV2[0].(map[string]interface{})
require.Equal(t, "license", field1["field"])
require.Equal(t, false, field1["default"])
require.Contains(t, field1, "conditions")
}
func TestRenderCatalogerInfoTable(t *testing.T) {
trueVal := true
falseVal := false
catalogers := []capabilities.CatalogerEntry{
{
Ecosystem: "test",
Name: "test-cataloger",
Type: "generic",
Parsers: []capabilities.Parser{
{
ParserFunction: "parseTest",
Detector: capabilities.Detector{
Method: capabilities.GlobDetection,
Criteria: []string{"**/*.test"},
},
Capabilities: map[capabilities.EnrichmentMode]*capabilities.Capability{
capabilities.OfflineMode: {
License: &trueVal,
Dependencies: &capabilities.DependencyCapabilities{
Depth: []string{"direct"},
Edges: "flat",
Kinds: []string{"runtime"},
},
PackageManager: &capabilities.PackageManagerCapabilities{
Files: &capabilities.FileCapabilities{
Listing: &trueVal,
Digests: &falseVal,
},
PackageIntegrityHash: &falseVal,
},
},
},
},
},
},
}
tests := []struct {
name string
catalogers []capabilities.CatalogerEntry
mode capabilities.EnrichmentMode
wantContains []string
}{
{
name: "renders table with cataloger data",
catalogers: catalogers,
mode: capabilities.OfflineMode,
wantContains: []string{
"test-cataloger",
"**/*.test",
"CATALOGER",
"CRITERIA",
"LICENSE",
"DEPTH",
"LISTING",
},
},
{
name: "empty catalogers list",
catalogers: []capabilities.CatalogerEntry{},
mode: capabilities.OfflineMode,
wantContains: []string{
"No catalogers found",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
doc := &capabilities.Document{
Catalogers: tt.catalogers,
}
got := renderCatalogerInfoTable(doc, tt.catalogers, tt.mode)
for _, want := range tt.wantContains {
require.Contains(t, got, want)
}
})
}
}
func TestRenderCatalogerInfoTableWithV2Capabilities(t *testing.T) {
// test cataloger with capabilities-v2
catalogers := []capabilities.CatalogerEntry{
{
Ecosystem: "javascript",
Name: "npm-cataloger",
Type: "custom",
Config: "javascript.CatalogerConfig",
CapabilitiesV2: capabilities.CapabilitySet{
{
Name: "license",
Default: false,
Conditions: []capabilities.CapabilityCondition{
{
When: map[string]interface{}{"SearchRemoteLicenses": true},
Value: true,
Comment: "License info fetched from NPM registry",
},
},
},
{
Name: "dependency.depth",
Default: []string{"direct", "indirect"},
},
},
},
}
doc := &capabilities.Document{
Catalogers: catalogers,
}
got := renderCatalogerInfoTable(doc, catalogers, capabilities.OfflineMode)
// should include v2 capabilities section
require.Contains(t, got, "Configuration Impact on Capabilities")
require.Contains(t, got, "npm-cataloger")
require.Contains(t, got, "license")
require.Contains(t, got, "Default: false")
require.Contains(t, got, "When SearchRemoteLicenses=true: true")
require.Contains(t, got, "License info fetched from NPM registry")
require.Contains(t, got, "dependency.depth")
require.Contains(t, got, "[direct, indirect]")
}
func TestFormatCapabilityValue(t *testing.T) {
tests := []struct {
name string
input interface{}
want string
}{
{
name: "nil value",
input: nil,
want: "null",
},
{
name: "bool true",
input: true,
want: "true",
},
{
name: "bool false",
input: false,
want: "false",
},
{
name: "string value",
input: "test-value",
want: "test-value",
},
{
name: "string slice",
input: []string{"direct", "indirect"},
want: "[direct, indirect]",
},
{
name: "interface slice",
input: []interface{}{"runtime", "dev"},
want: "[runtime, dev]",
},
{
name: "empty slice",
input: []string{},
want: "[]",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := formatCapabilityValue(tt.input)
require.Equal(t, tt.want, got)
})
}
}
func TestFormatWhenClause(t *testing.T) {
tests := []struct {
name string
input map[string]interface{}
want string
}{
{
name: "empty when clause",
input: map[string]interface{}{},
want: "always",
},
{
name: "single condition",
input: map[string]interface{}{
"SearchRemoteLicenses": true,
},
want: "SearchRemoteLicenses=true",
},
{
name: "multiple conditions",
input: map[string]interface{}{
"SearchRemoteLicenses": true,
"UseNetwork": true,
},
// note: order may vary, but should contain both with AND
want: " AND ",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := formatWhenClause(tt.input)
if strings.Contains(tt.want, " AND ") {
// for multiple conditions, just check it contains AND
require.Contains(t, got, " AND ")
require.Contains(t, got, "SearchRemoteLicenses=true")
require.Contains(t, got, "UseNetwork=true")
} else {
require.Equal(t, tt.want, got)
}
})
}
}

View File

@ -117,22 +117,27 @@ func extractAppConfigFields(filePath, topLevelKey string) ([]AppConfigField, err
// 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) {
var configStruct *ast.StructType
descriptions := make(map[string]string)
expectedName := determineExpectedConfigName(topLevelKey)
configStruct := findConfigStruct(f, expectedName)
descriptions := extractDescriptionsFromDescribeFields(f)
return configStruct, descriptions
}
// determine the expected main config struct name based on the top-level key
// e.g., "dotnet" -> "dotnetConfig", "golang" -> "golangConfig", "linux-kernel" -> "linuxKernelConfig"
expectedName := topLevelKey + "Config"
// handle special case for linux-kernel
if topLevelKey == "linux-kernel" {
expectedName = "linuxKernelConfig"
}
// handle javascript
if topLevelKey == "javascript" {
expectedName = "javaScriptConfig"
// 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"
}
}
// find the main config struct (not nested ones)
// 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 {
@ -150,18 +155,18 @@ func findAppConfigStructAndDescriptions(f *ast.File, topLevelKey string) (*ast.S
continue
}
// match the expected main config struct name
if typeSpec.Name.Name == expectedName {
configStruct = structType
break
return structType
}
}
if configStruct != nil {
break
}
}
return nil
}
// extractDescriptionsFromDescribeFields extracts field descriptions from the DescribeFields method
func extractDescriptionsFromDescribeFields(f *ast.File) map[string]string {
descriptions := make(map[string]string)
// find DescribeFields method and extract descriptions
for _, decl := range f.Decls {
funcDecl, ok := decl.(*ast.FuncDecl)
if !ok || funcDecl.Name.Name != "DescribeFields" {
@ -194,7 +199,6 @@ func findAppConfigStructAndDescriptions(f *ast.File, topLevelKey string) (*ast.S
// second argument is the description string
description := extractStringLiteral(callExpr.Args[1])
if description != "" {
// clean up multi-line descriptions
description = cleanDescription(description)
descriptions[fieldPath] = description
}
@ -203,7 +207,7 @@ func findAppConfigStructAndDescriptions(f *ast.File, topLevelKey string) (*ast.S
})
}
return configStruct, descriptions
return descriptions
}
// extractNestedAppConfigs handles nested config structs like golang.MainModuleVersion

View File

@ -161,14 +161,18 @@ func extractCatalogerName(funcDecl *ast.FuncDecl, file *ast.File, filePath, repo
// 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 {
// look for patterns like:
// return &someCataloger{cfg: cfg}
// return someCataloger{cfg: cfg}
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 {
// look for return statements
returnStmt, ok := n.(*ast.ReturnStmt)
if !ok || len(returnStmt.Results) == 0 {
return true
@ -197,62 +201,29 @@ func inferCatalogerNameFromCustomImpl(funcDecl *ast.FuncDecl, file *ast.File, ct
return true
})
if typeName == "" {
return ""
}
return typeName
}
// now look for the Name() method on this type
// 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
}
// check if this is a method on our type
if funcDecl.Recv == nil || len(funcDecl.Recv.List) == 0 {
continue
}
recvType := funcDecl.Recv.List[0].Type
var recvTypeName string
// handle both T and *T receivers
if ident, ok := recvType.(*ast.Ident); ok {
recvTypeName = ident.Name
} else if starExpr, ok := recvType.(*ast.StarExpr); ok {
if ident, ok := starExpr.X.(*ast.Ident); ok {
recvTypeName = ident.Name
}
}
recvTypeName := extractReceiverTypeName(funcDecl.Recv.List[0].Type)
if recvTypeName != typeName {
continue
}
// found the Name() method, extract the return value
if funcDecl.Body != nil {
var name string
ast.Inspect(funcDecl.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
})
if name != "" {
if name := extractNameFromMethodBody(funcDecl.Body, ctx); name != "" {
return name
}
}
@ -261,6 +232,46 @@ func inferCatalogerNameFromCustomImpl(funcDecl *ast.FuncDecl, file *ast.File, ct
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")

View File

@ -419,5 +419,5 @@ func DiscoverAllowedConfigStructs(repoRoot string) (map[string]bool, error) {
}
}
return nil, fmt.Errorf("Config struct not found in %s", configFilePath)
return nil, fmt.Errorf("config struct not found in %s", configFilePath)
}

View File

@ -651,7 +651,7 @@ func RepoRoot() (string, error) {
}
// extractBinaryClassifiers extracts all binary classifiers with their full information
func extractBinaryClassifiers() []binary.Classifier {
func extractBinaryClassifiers() []binary.Classifier { //nolint:staticcheck
classifiers := binary.DefaultClassifiers()
// return all classifiers (already sorted by the default function)

View File

@ -333,48 +333,55 @@ func updateNodeTree(rootNode *yaml.Node, doc *capabilities.Document) error {
updateOrAddSection(existingMapping, newMapping, "application")
// update catalogers section (preserve comments)
updateCatalogersSection(existingMapping, newMapping)
return nil
}
// updateCatalogersSection updates the catalogers section while preserving comments
func updateCatalogersSection(existingMapping, newMapping *yaml.Node) {
existingCatalogersNode := findSectionNode(existingMapping, "catalogers")
newCatalogersNode := findSectionNode(newMapping, "catalogers")
if existingCatalogersNode != nil && newCatalogersNode != nil {
// create a map of existing cataloger nodes by name for quick lookup
existingByName := make(map[string]*yaml.Node)
if existingCatalogersNode.Kind == yaml.SequenceNode {
for _, catalogerNode := range existingCatalogersNode.Content {
if catalogerNode.Kind == yaml.MappingNode {
name := findFieldValue(catalogerNode, "name")
if name != "" {
existingByName[name] = catalogerNode
}
}
}
}
// update each cataloger in the new tree with preserved comments
if newCatalogersNode.Kind == yaml.SequenceNode {
for _, newCatalogerNode := range newCatalogersNode.Content {
if newCatalogerNode.Kind != yaml.MappingNode {
continue
}
name := findFieldValue(newCatalogerNode, "name")
if existingNode := existingByName[name]; existingNode != nil {
// preserve comments from existing cataloger entry
newCatalogerNode.HeadComment = existingNode.HeadComment
newCatalogerNode.LineComment = existingNode.LineComment
newCatalogerNode.FootComment = existingNode.FootComment
// preserve field-level and nested comments
preserveFieldComments(existingNode, newCatalogerNode)
}
}
}
// replace the catalogers content
existingCatalogersNode.Content = newCatalogersNode.Content
if existingCatalogersNode == nil || newCatalogersNode == nil {
return
}
return nil
// create a map of existing cataloger nodes by name for quick lookup
existingByName := make(map[string]*yaml.Node)
if existingCatalogersNode.Kind == yaml.SequenceNode {
for _, catalogerNode := range existingCatalogersNode.Content {
if catalogerNode.Kind == yaml.MappingNode {
name := findFieldValue(catalogerNode, "name")
if name != "" {
existingByName[name] = catalogerNode
}
}
}
}
// update each cataloger in the new tree with preserved comments
if newCatalogersNode.Kind == yaml.SequenceNode {
for _, newCatalogerNode := range newCatalogersNode.Content {
if newCatalogerNode.Kind != yaml.MappingNode {
continue
}
name := findFieldValue(newCatalogerNode, "name")
if existingNode := existingByName[name]; existingNode != nil {
// preserve comments from existing cataloger entry
newCatalogerNode.HeadComment = existingNode.HeadComment
newCatalogerNode.LineComment = existingNode.LineComment
newCatalogerNode.FootComment = existingNode.FootComment
// preserve field-level and nested comments
preserveFieldComments(existingNode, newCatalogerNode)
}
}
}
// replace the catalogers content
existingCatalogersNode.Content = newCatalogersNode.Content
}
// updateOrAddSection updates or adds a section in the existing mapping from the new mapping

View File

@ -65,40 +65,11 @@ type Statistics struct {
func RegenerateCapabilities(yamlPath string, repoRoot string) (*Statistics, error) {
stats := &Statistics{}
// 1. Discover generic catalogers from code
fmt.Print(" → Scanning source code for generic catalogers...")
discovered, err := discoverGenericCatalogers(repoRoot)
// 1-2. Discover all cataloger data
discovered, customCatalogerMetadata, customCatalogerPackageTypes, binaryClassifiers, allCatalogers, err := discoverAllCatalogerData(repoRoot, stats)
if err != nil {
return nil, fmt.Errorf("failed to discover catalogers: %w", err)
return nil, err
}
stats.TotalGenericCatalogers = len(discovered)
fmt.Printf(" found %d\n", stats.TotalGenericCatalogers)
// 1a. Discover metadata types and package types from test-generated JSON files
fmt.Print(" → Searching for metadata type and package type information...")
customCatalogerMetadata, customCatalogerPackageTypes, err := discoverMetadataTypes(repoRoot, discovered)
if err != nil {
return nil, fmt.Errorf("failed to discover metadata types: %w", err)
}
fmt.Println(" done")
// 1b. Extract binary classifiers
fmt.Print(" → Extracting binary classifiers...")
binaryClassifiers := extractBinaryClassifiers()
fmt.Printf(" found %d classifiers\n", len(binaryClassifiers))
// Count parser functions
for _, disc := range discovered {
stats.TotalParserFunctions += len(disc.Parsers)
}
// 2. Get all package cataloger info (names and selectors)
fmt.Print(" → Fetching all cataloger info from syft...")
allCatalogers, err := allPackageCatalogerInfo()
if err != nil {
return nil, fmt.Errorf("failed to get cataloger info: %w", err)
}
fmt.Printf(" found %d total\n", len(allCatalogers))
// 3. Load existing YAML (if exists) - now returns both document and node tree
fmt.Print(" → Loading existing packages.yaml...")
@ -108,107 +79,24 @@ func RegenerateCapabilities(yamlPath string, repoRoot string) (*Statistics, erro
}
fmt.Printf(" loaded %d entries\n", len(existing.Catalogers))
// 3a. Discover cataloger config structs
fmt.Print(" → Discovering cataloger config structs...")
configInfoMap, err := DiscoverConfigs(repoRoot)
// 3a-3c. Discover and process all config-related information
discoveredConfigs, err := discoverAndFilterConfigs(repoRoot)
if err != nil {
return nil, fmt.Errorf("failed to discover configs: %w", err)
return nil, err
}
fmt.Printf(" found %d\n", len(configInfoMap))
// 3a-1. Get whitelist of allowed config structs from pkgcataloging.Config
fmt.Print(" → Filtering configs by pkgcataloging.Config whitelist...")
allowedConfigs, err := DiscoverAllowedConfigStructs(repoRoot)
discoveredAppConfigs, err := discoverAndConvertAppConfigs(repoRoot)
if err != nil {
return nil, fmt.Errorf("failed to discover allowed config structs: %w", err)
return nil, err
}
// filter discovered configs to only include allowed ones
filteredConfigInfoMap := make(map[string]ConfigInfo)
for key, configInfo := range configInfoMap {
if allowedConfigs[key] {
filteredConfigInfoMap[key] = configInfo
}
}
fmt.Printf(" %d allowed (filtered %d)\n", len(filteredConfigInfoMap), len(configInfoMap)-len(filteredConfigInfoMap))
// convert ConfigInfo to CatalogerConfigEntry format for packages.yaml
discoveredConfigs := make(map[string]capabilities.CatalogerConfigEntry)
for key, configInfo := range filteredConfigInfoMap {
fields := make([]capabilities.CatalogerConfigFieldEntry, len(configInfo.Fields))
for i, field := range configInfo.Fields {
fields[i] = capabilities.CatalogerConfigFieldEntry{
Key: field.Name,
Description: field.Description,
AppKey: field.AppKey,
}
}
discoveredConfigs[key] = capabilities.CatalogerConfigEntry{
Fields: fields,
}
}
// 3b. Discover app-level configs
fmt.Print(" → Discovering app-level config fields...")
appConfigFields, err := DiscoverAppConfigs(repoRoot)
if err != nil {
return nil, fmt.Errorf("failed to discover app configs: %w", err)
}
fmt.Printf(" found %d\n", len(appConfigFields))
// convert to ApplicationConfigField format
discoveredAppConfigs := make([]capabilities.ApplicationConfigField, len(appConfigFields))
for i, field := range appConfigFields {
discoveredAppConfigs[i] = capabilities.ApplicationConfigField{
Key: field.Key,
Description: field.Description,
DefaultValue: field.DefaultValue,
}
}
// 3c. Link catalogers to their configs
fmt.Print(" → Linking catalogers to config structs...")
catalogerConfigMappings, err := LinkCatalogersToConfigs(repoRoot)
if err != nil {
return nil, fmt.Errorf("failed to link catalogers to configs: %w", err)
}
fmt.Printf(" found %d mappings\n", len(catalogerConfigMappings))
fmt.Printf(" → Linking catalogers to config structs... found %d mappings\n", len(catalogerConfigMappings))
// 3c-1. Filter cataloger config mappings by exceptions
// Remove any catalogers that should not have config fields
fmt.Print(" → Filtering cataloger config mappings by exceptions...")
filteredCatalogerConfigMappings := make(map[string]string)
filteredCount := 0
for catalogerName, configName := range catalogerConfigMappings {
if catalogerConfigExceptions.Has(catalogerName) {
filteredCount++
continue
}
filteredCatalogerConfigMappings[catalogerName] = configName
}
if filteredCount > 0 {
fmt.Printf(" filtered %d\n", filteredCount)
} else {
fmt.Println(" none")
}
// 3c-2. Merge manual config overrides
// Manual overrides take precedence over discovered mappings
fmt.Print(" → Merging manual config overrides...")
overrideCount := 0
for catalogerName, configName := range catalogerConfigOverrides {
if catalogerConfigExceptions.Has(catalogerName) {
// skip if this cataloger is in the exceptions list
continue
}
filteredCatalogerConfigMappings[catalogerName] = configName
overrideCount++
}
if overrideCount > 0 {
fmt.Printf(" added %d\n", overrideCount)
} else {
fmt.Println(" none")
}
filteredCatalogerConfigMappings := applyConfigMappingFilters(catalogerConfigMappings)
// 4. Build updated catalogers list
fmt.Println(" → Merging discovered catalogers with existing entries...")
@ -244,6 +132,156 @@ func RegenerateCapabilities(yamlPath string, repoRoot string) (*Statistics, erro
return stats, nil
}
// discoverAllCatalogerData discovers all cataloger-related data including generic catalogers, metadata, binary classifiers, and all catalogers
func discoverAllCatalogerData(repoRoot string, stats *Statistics) (
map[string]DiscoveredCataloger,
map[string][]string,
map[string][]string,
[]binary.Classifier, //nolint:staticcheck
[]capabilities.CatalogerInfo,
error,
) {
// discover generic catalogers
fmt.Print(" → Scanning source code for generic catalogers...")
discovered, err := discoverGenericCatalogers(repoRoot)
if err != nil {
return nil, nil, nil, nil, nil, fmt.Errorf("failed to discover catalogers: %w", err)
}
stats.TotalGenericCatalogers = len(discovered)
fmt.Printf(" found %d\n", stats.TotalGenericCatalogers)
// discover metadata types
fmt.Print(" → Searching for metadata type and package type information...")
customCatalogerMetadata, customCatalogerPackageTypes, err := discoverMetadataTypes(repoRoot, discovered)
if err != nil {
return nil, nil, nil, nil, nil, fmt.Errorf("failed to discover metadata types: %w", err)
}
fmt.Println(" done")
// extract binary classifiers
fmt.Print(" → Extracting binary classifiers...")
binaryClassifiers := extractBinaryClassifiers()
fmt.Printf(" found %d classifiers\n", len(binaryClassifiers))
// count parser functions
for _, disc := range discovered {
stats.TotalParserFunctions += len(disc.Parsers)
}
// get all cataloger info
fmt.Print(" → Fetching all cataloger info from syft...")
allCatalogers, err := allPackageCatalogerInfo()
if err != nil {
return nil, nil, nil, nil, nil, fmt.Errorf("failed to get cataloger info: %w", err)
}
fmt.Printf(" found %d total\n", len(allCatalogers))
return discovered, customCatalogerMetadata, customCatalogerPackageTypes, binaryClassifiers, allCatalogers, nil
}
// discoverAndFilterConfigs discovers cataloger config structs, filters by whitelist, and converts to capabilities format
func discoverAndFilterConfigs(repoRoot string) (map[string]capabilities.CatalogerConfigEntry, error) {
fmt.Print(" → Discovering cataloger config structs...")
configInfoMap, err := DiscoverConfigs(repoRoot)
if err != nil {
return nil, fmt.Errorf("failed to discover configs: %w", err)
}
fmt.Printf(" found %d\n", len(configInfoMap))
fmt.Print(" → Filtering configs by pkgcataloging.Config whitelist...")
allowedConfigs, err := DiscoverAllowedConfigStructs(repoRoot)
if err != nil {
return nil, fmt.Errorf("failed to discover allowed config structs: %w", err)
}
// filter discovered configs to only include allowed ones
filteredConfigInfoMap := make(map[string]ConfigInfo)
for key, configInfo := range configInfoMap {
if allowedConfigs[key] {
filteredConfigInfoMap[key] = configInfo
}
}
fmt.Printf(" %d allowed (filtered %d)\n", len(filteredConfigInfoMap), len(configInfoMap)-len(filteredConfigInfoMap))
// convert ConfigInfo to CatalogerConfigEntry format for packages.yaml
discoveredConfigs := make(map[string]capabilities.CatalogerConfigEntry)
for key, configInfo := range filteredConfigInfoMap {
fields := make([]capabilities.CatalogerConfigFieldEntry, len(configInfo.Fields))
for i, field := range configInfo.Fields {
fields[i] = capabilities.CatalogerConfigFieldEntry{
Key: field.Name,
Description: field.Description,
AppKey: field.AppKey,
}
}
discoveredConfigs[key] = capabilities.CatalogerConfigEntry{
Fields: fields,
}
}
return discoveredConfigs, nil
}
// discoverAndConvertAppConfigs discovers app-level config fields and converts them to capabilities format
func discoverAndConvertAppConfigs(repoRoot string) ([]capabilities.ApplicationConfigField, error) {
fmt.Print(" → Discovering app-level config fields...")
appConfigFields, err := DiscoverAppConfigs(repoRoot)
if err != nil {
return nil, fmt.Errorf("failed to discover app configs: %w", err)
}
fmt.Printf(" found %d\n", len(appConfigFields))
// convert to ApplicationConfigField format
discoveredAppConfigs := make([]capabilities.ApplicationConfigField, len(appConfigFields))
for i, field := range appConfigFields {
discoveredAppConfigs[i] = capabilities.ApplicationConfigField{
Key: field.Key,
Description: field.Description,
DefaultValue: field.DefaultValue,
}
}
return discoveredAppConfigs, nil
}
// applyConfigMappingFilters applies exceptions and manual overrides to cataloger config mappings
func applyConfigMappingFilters(catalogerConfigMappings map[string]string) map[string]string {
// filter by exceptions
fmt.Print(" → Filtering cataloger config mappings by exceptions...")
filteredCatalogerConfigMappings := make(map[string]string)
filteredCount := 0
for catalogerName, configName := range catalogerConfigMappings {
if catalogerConfigExceptions.Has(catalogerName) {
filteredCount++
continue
}
filteredCatalogerConfigMappings[catalogerName] = configName
}
if filteredCount > 0 {
fmt.Printf(" filtered %d\n", filteredCount)
} else {
fmt.Println(" none")
}
// merge manual overrides
fmt.Print(" → Merging manual config overrides...")
overrideCount := 0
for catalogerName, configName := range catalogerConfigOverrides {
if catalogerConfigExceptions.Has(catalogerName) {
continue
}
filteredCatalogerConfigMappings[catalogerName] = configName
overrideCount++
}
if overrideCount > 0 {
fmt.Printf(" added %d\n", overrideCount)
} else {
fmt.Println(" none")
}
return filteredCatalogerConfigMappings
}
type orphanInfo struct {
catalogerName string
parserFunction string
@ -301,11 +339,11 @@ func (r *CatalogerRegistry) AllCatalogers() []capabilities.CatalogerInfo {
type EnrichmentData struct {
metadata map[string][]string
packageTypes map[string][]string
binaryClassifiers []binary.Classifier
binaryClassifiers []binary.Classifier //nolint:staticcheck
}
// NewEnrichmentData creates a new enrichment data container
func NewEnrichmentData(metadata, packageTypes map[string][]string, binaryClassifiers []binary.Classifier) *EnrichmentData {
func NewEnrichmentData(metadata, packageTypes map[string][]string, binaryClassifiers []binary.Classifier) *EnrichmentData { //nolint:staticcheck
return &EnrichmentData{
metadata: metadata,
packageTypes: packageTypes,
@ -584,7 +622,7 @@ func mergeDiscoveredWithExisting(
discovered map[string]DiscoveredCataloger,
customMetadata map[string][]string,
customPackageTypes map[string][]string,
binaryClassifiers []binary.Classifier,
binaryClassifiers []binary.Classifier, //nolint:staticcheck
allCatalogers []capabilities.CatalogerInfo,
existing *capabilities.Document,
configs map[string]capabilities.CatalogerConfigEntry,

View File

@ -1308,7 +1308,7 @@ catalogers:
- name: package_manager.package_integrity_hash
default: true
evidence:
- CocoapodsLockEntry.Checksum
- CocoaPodfileLockEntry.Checksum
- ecosystem: swift # MANUAL
name: swift-package-manager-cataloger # AUTO-GENERATED
type: generic # AUTO-GENERATED