From 0dd906b071ece3be0291ea3a5d8a21b04cb1d9a5 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 28 Oct 2025 10:38:24 -0400 Subject: [PATCH] fix linting Signed-off-by: Alex Goodman --- cmd/syft/internal/commands/cataloger.go | 12 +- cmd/syft/internal/commands/cataloger_info.go | 339 +++++---- .../internal/commands/cataloger_info_test.go | 719 ------------------ .../generate/app_config_discovery.go | 48 +- .../generate/cataloger_config_linking.go | 97 +-- .../capabilities/generate/config_discovery.go | 2 +- internal/capabilities/generate/discover.go | 2 +- internal/capabilities/generate/io.go | 81 +- internal/capabilities/generate/merge.go | 288 ++++--- internal/capabilities/packages.yaml | 2 +- 10 files changed, 481 insertions(+), 1109 deletions(-) diff --git a/cmd/syft/internal/commands/cataloger.go b/cmd/syft/internal/commands/cataloger.go index ce1290c3c..2a3d665a4 100644 --- a/cmd/syft/internal/commands/cataloger.go +++ b/cmd/syft/internal/commands/cataloger.go @@ -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" +} diff --git a/cmd/syft/internal/commands/cataloger_info.go b/cmd/syft/internal/commands/cataloger_info.go index d982d3071..1125ff344 100644 --- a/cmd/syft/internal/commands/cataloger_info.go +++ b/cmd/syft/internal/commands/cataloger_info.go @@ -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() diff --git a/cmd/syft/internal/commands/cataloger_info_test.go b/cmd/syft/internal/commands/cataloger_info_test.go index 78da955a9..cdff10da7 100644 --- a/cmd/syft/internal/commands/cataloger_info_test.go +++ b/cmd/syft/internal/commands/cataloger_info_test.go @@ -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) - } - }) - } -} diff --git a/internal/capabilities/generate/app_config_discovery.go b/internal/capabilities/generate/app_config_discovery.go index f32d6dd0c..6a9c07e13 100644 --- a/internal/capabilities/generate/app_config_discovery.go +++ b/internal/capabilities/generate/app_config_discovery.go @@ -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 diff --git a/internal/capabilities/generate/cataloger_config_linking.go b/internal/capabilities/generate/cataloger_config_linking.go index 601cb738e..b9dadc698 100644 --- a/internal/capabilities/generate/cataloger_config_linking.go +++ b/internal/capabilities/generate/cataloger_config_linking.go @@ -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") diff --git a/internal/capabilities/generate/config_discovery.go b/internal/capabilities/generate/config_discovery.go index 69a221e51..14747ba4b 100644 --- a/internal/capabilities/generate/config_discovery.go +++ b/internal/capabilities/generate/config_discovery.go @@ -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) } diff --git a/internal/capabilities/generate/discover.go b/internal/capabilities/generate/discover.go index 8e05c202e..111142133 100644 --- a/internal/capabilities/generate/discover.go +++ b/internal/capabilities/generate/discover.go @@ -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) diff --git a/internal/capabilities/generate/io.go b/internal/capabilities/generate/io.go index c4cb7fa87..8b96e55f6 100644 --- a/internal/capabilities/generate/io.go +++ b/internal/capabilities/generate/io.go @@ -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 diff --git a/internal/capabilities/generate/merge.go b/internal/capabilities/generate/merge.go index 444cb3844..d17a32308 100644 --- a/internal/capabilities/generate/merge.go +++ b/internal/capabilities/generate/merge.go @@ -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, diff --git a/internal/capabilities/packages.yaml b/internal/capabilities/packages.yaml index 93df13a6b..3ecf6ad9f 100644 --- a/internal/capabilities/packages.yaml +++ b/internal/capabilities/packages.yaml @@ -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