package main import ( "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" "github.com/anchore/syft/internal/capabilities" "github.com/anchore/syft/syft/pkg/cataloger/binary" ) func TestMergeConfigSections(t *testing.T) { tests := []struct { name string existingDoc *capabilities.Document newConfigs map[string]capabilities.CatalogerConfigEntry newAppConfigs []capabilities.ApplicationConfigField expectedConfigs map[string]capabilities.CatalogerConfigEntry expectedAppConfigs []capabilities.ApplicationConfigField description string }{ { name: "new configs replace existing configs", description: "configs and app-config are AUTO-GENERATED, so new data should completely replace old", existingDoc: &capabilities.Document{ Configs: map[string]capabilities.CatalogerConfigEntry{ "golang.OldConfig": { Fields: []capabilities.CatalogerConfigFieldEntry{ {Key: "OldField", Description: "old field"}, }, }, }, ApplicationConfig: []capabilities.ApplicationConfigField{ {Key: "golang.old-config", Description: "old config"}, }, Catalogers: []capabilities.CatalogerEntry{}, }, newConfigs: map[string]capabilities.CatalogerConfigEntry{ "golang.CatalogerConfig": { Fields: []capabilities.CatalogerConfigFieldEntry{ {Key: "SearchLocalModCacheLicenses", Description: "search local mod cache", AppKey: "golang.search-local-mod-cache-licenses"}, }, }, }, newAppConfigs: []capabilities.ApplicationConfigField{ {Key: "golang.search-local-mod-cache-licenses", Description: "Search licenses in local mod cache", DefaultValue: false}, }, expectedConfigs: map[string]capabilities.CatalogerConfigEntry{ "golang.CatalogerConfig": { Fields: []capabilities.CatalogerConfigFieldEntry{ {Key: "SearchLocalModCacheLicenses", Description: "search local mod cache", AppKey: "golang.search-local-mod-cache-licenses"}, }, }, }, expectedAppConfigs: []capabilities.ApplicationConfigField{ {Key: "golang.search-local-mod-cache-licenses", Description: "Search licenses in local mod cache", DefaultValue: false}, }, }, { name: "empty new configs clears existing configs", description: "if no configs are discovered, the sections should be empty (not nil)", existingDoc: &capabilities.Document{ Configs: map[string]capabilities.CatalogerConfigEntry{ "golang.OldConfig": { Fields: []capabilities.CatalogerConfigFieldEntry{ {Key: "OldField", Description: "old field"}, }, }, }, ApplicationConfig: []capabilities.ApplicationConfigField{ {Key: "golang.old-config", Description: "old config"}, }, Catalogers: []capabilities.CatalogerEntry{}, }, newConfigs: map[string]capabilities.CatalogerConfigEntry{}, newAppConfigs: []capabilities.ApplicationConfigField{}, expectedConfigs: map[string]capabilities.CatalogerConfigEntry{}, expectedAppConfigs: []capabilities.ApplicationConfigField{}, }, { name: "nil existing configs are replaced with new configs", description: "first-time generation should populate configs", existingDoc: &capabilities.Document{ Catalogers: []capabilities.CatalogerEntry{}, }, newConfigs: map[string]capabilities.CatalogerConfigEntry{ "python.CatalogerConfig": { Fields: []capabilities.CatalogerConfigFieldEntry{ {Key: "GuessUnpinnedRequirements", Description: "guess unpinned reqs", AppKey: "python.guess-unpinned-requirements"}, }, }, }, newAppConfigs: []capabilities.ApplicationConfigField{ {Key: "python.guess-unpinned-requirements", Description: "Guess unpinned requirements", DefaultValue: false}, }, expectedConfigs: map[string]capabilities.CatalogerConfigEntry{ "python.CatalogerConfig": { Fields: []capabilities.CatalogerConfigFieldEntry{ {Key: "GuessUnpinnedRequirements", Description: "guess unpinned reqs", AppKey: "python.guess-unpinned-requirements"}, }, }, }, expectedAppConfigs: []capabilities.ApplicationConfigField{ {Key: "python.guess-unpinned-requirements", Description: "Guess unpinned requirements", DefaultValue: false}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // use mergeDiscoveredWithExisting to properly test the integration updated, _, _ := mergeDiscoveredWithExisting( map[string]DiscoveredCataloger{}, map[string][]string{}, map[string][]string{}, []binary.Classifier{}, []capabilities.CatalogerInfo{}, tt.existingDoc, tt.newConfigs, tt.newAppConfigs, map[string]string{}, ) // verify configs were replaced (not merged) if diff := cmp.Diff(tt.expectedConfigs, updated.Configs); diff != "" { t.Errorf("Configs mismatch (-want +got):\n%s", diff) } // verify app-configs were replaced (not merged) if diff := cmp.Diff(tt.expectedAppConfigs, updated.ApplicationConfig); diff != "" { t.Errorf("ApplicationConfig mismatch (-want +got):\n%s", diff) } }) } } func TestMergeCatalogerConfigField(t *testing.T) { tests := []struct { name string existingEntry capabilities.CatalogerEntry discoveredInfo DiscoveredCataloger catalogerConfigMappings map[string]string expectedConfig string }{ { name: "config field is updated from discovered data", existingEntry: capabilities.CatalogerEntry{ Name: "go-module-binary-cataloger", Config: "", // was empty }, discoveredInfo: DiscoveredCataloger{ Name: "go-module-binary-cataloger", Type: "generic", }, catalogerConfigMappings: map[string]string{ "go-module-binary-cataloger": "golang.CatalogerConfig", }, expectedConfig: "golang.CatalogerConfig", }, { name: "config field is replaced if different", existingEntry: capabilities.CatalogerEntry{ Name: "go-module-binary-cataloger", Config: "golang.OldConfig", }, discoveredInfo: DiscoveredCataloger{ Name: "go-module-binary-cataloger", Type: "generic", }, catalogerConfigMappings: map[string]string{ "go-module-binary-cataloger": "golang.NewConfig", }, expectedConfig: "golang.NewConfig", }, { name: "config field is cleared if no mapping exists", existingEntry: capabilities.CatalogerEntry{ Name: "go-module-binary-cataloger", Config: "golang.OldConfig", }, discoveredInfo: DiscoveredCataloger{ Name: "go-module-binary-cataloger", Type: "generic", }, catalogerConfigMappings: map[string]string{}, expectedConfig: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // simulate updateEntry which should update the Config field updated, _, _ := updateEntry(&tt.existingEntry, tt.discoveredInfo, nil, tt.catalogerConfigMappings) require.Equal(t, tt.expectedConfig, updated.Config) }) } } func TestMergePreservesManualCapabilities(t *testing.T) { // ensure that while we update configs (AUTO-GENERATED), // we still preserve capabilities (MANUAL) existingDoc := &capabilities.Document{ Configs: map[string]capabilities.CatalogerConfigEntry{ "golang.OldConfig": { Fields: []capabilities.CatalogerConfigFieldEntry{ {Key: "OldField", Description: "old"}, }, }, }, Catalogers: []capabilities.CatalogerEntry{ { Name: "test-cataloger", Type: "generic", Parsers: []capabilities.Parser{ { ParserFunction: "parseTest", Capabilities: capabilities.CapabilitySet{ {Name: "license", Default: true}, // manual value }, }, }, }, }, } discovered := map[string]DiscoveredCataloger{ "test-cataloger": { Name: "test-cataloger", Type: "generic", Parsers: []DiscoveredParser{ { ParserFunction: "parseTest", Method: "glob", Criteria: []string{"**/*.test"}, }, }, }, } newConfigs := map[string]capabilities.CatalogerConfigEntry{ "golang.NewConfig": { Fields: []capabilities.CatalogerConfigFieldEntry{ {Key: "NewField", Description: "new"}, }, }, } updated, _, _ := mergeDiscoveredWithExisting( discovered, map[string][]string{}, map[string][]string{}, []binary.Classifier{}, []capabilities.CatalogerInfo{ {Name: "test-cataloger", Selectors: []string{"test"}}, }, existingDoc, newConfigs, []capabilities.ApplicationConfigField{}, map[string]string{}, ) // verify configs were replaced require.Len(t, updated.Configs, 1) _, hasOld := updated.Configs["golang.OldConfig"] require.False(t, hasOld, "old config should be removed") _, hasNew := updated.Configs["golang.NewConfig"] require.True(t, hasNew, "new config should be present") // verify capabilities were preserved require.Len(t, updated.Catalogers, 1) require.Len(t, updated.Catalogers[0].Parsers, 1) parser := updated.Catalogers[0].Parsers[0] require.Len(t, parser.Capabilities, 1) require.Equal(t, "license", parser.Capabilities[0].Name, "manual capability field should be preserved") require.Equal(t, true, parser.Capabilities[0].Default, "manual capability value should be preserved") // verify AUTO-GENERATED parser fields were updated require.Equal(t, "glob", string(parser.Detector.Method)) require.Equal(t, []string{"**/*.test"}, parser.Detector.Criteria) } func TestCatalogerConfigFieldUpdatedForNewCatalogers(t *testing.T) { tests := []struct { name string catalogerName string catalogerType string catalogerConfigMappings map[string]string expectedConfig string }{ { name: "new generic cataloger gets config from mapping", catalogerName: "go-module-binary-cataloger", catalogerType: "generic", catalogerConfigMappings: map[string]string{ "go-module-binary-cataloger": "golang.CatalogerConfig", }, expectedConfig: "golang.CatalogerConfig", }, { name: "new custom cataloger gets config from mapping", catalogerName: "java-archive-cataloger", catalogerType: "custom", catalogerConfigMappings: map[string]string{ "java-archive-cataloger": "java.CatalogerConfig", }, expectedConfig: "java.CatalogerConfig", }, { name: "new cataloger without mapping has empty config", catalogerName: "python-cataloger", catalogerType: "generic", catalogerConfigMappings: map[string]string{}, expectedConfig: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // test for generic catalogers if tt.catalogerType == "generic" { discovered := map[string]DiscoveredCataloger{ tt.catalogerName: { Name: tt.catalogerName, Type: "generic", Parsers: []DiscoveredParser{ { ParserFunction: "parseTest", Method: "glob", Criteria: []string{"**/*.test"}, }, }, }, } updated, _, _ := mergeDiscoveredWithExisting( discovered, map[string][]string{}, map[string][]string{}, []binary.Classifier{}, []capabilities.CatalogerInfo{ {Name: tt.catalogerName, Selectors: []string{"test"}}, }, &capabilities.Document{Catalogers: []capabilities.CatalogerEntry{}}, map[string]capabilities.CatalogerConfigEntry{}, []capabilities.ApplicationConfigField{}, tt.catalogerConfigMappings, ) require.Len(t, updated.Catalogers, 1) require.Equal(t, tt.expectedConfig, updated.Catalogers[0].Config) } // test for custom catalogers if tt.catalogerType == "custom" { updated, _, _ := mergeDiscoveredWithExisting( map[string]DiscoveredCataloger{}, map[string][]string{}, map[string][]string{}, []binary.Classifier{}, []capabilities.CatalogerInfo{ {Name: tt.catalogerName, Selectors: []string{"test"}}, }, &capabilities.Document{Catalogers: []capabilities.CatalogerEntry{}}, map[string]capabilities.CatalogerConfigEntry{}, []capabilities.ApplicationConfigField{}, tt.catalogerConfigMappings, ) require.Len(t, updated.Catalogers, 1) require.Equal(t, tt.expectedConfig, updated.Catalogers[0].Config) } }) } } func TestStripPURLVersion(t *testing.T) { tests := []struct { name string input string want string }{ { name: "purl with version", input: "pkg:generic/python@1.0.0", want: "pkg:generic/python", }, { name: "purl without version", input: "pkg:generic/python", want: "pkg:generic/python", }, { name: "purl with multiple @ signs", input: "pkg:generic/py@thon@1.0.0", want: "pkg:generic/py@thon", }, { name: "empty string", input: "", want: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := stripPURLVersion(tt.input) require.Equal(t, tt.want, got) }) } } func TestInferEcosystem(t *testing.T) { tests := []struct { name string catalogerName string want string }{ { name: "go module cataloger", catalogerName: "go-module-binary-cataloger", want: "go", }, { name: "python cataloger", catalogerName: "python-package-cataloger", want: "python", }, { name: "java archive cataloger", catalogerName: "java-archive-cataloger", want: "java", }, { name: "rust cargo cataloger", catalogerName: "rust-cargo-lock-cataloger", want: "rust", }, { name: "javascript npm cataloger", catalogerName: "javascript-package-cataloger", want: "javascript", }, { name: "ruby gem cataloger", catalogerName: "ruby-gemspec-cataloger", want: "ruby", }, { name: "debian dpkg cataloger", catalogerName: "dpkg-db-cataloger", want: "debian", }, { name: "alpine apk cataloger", catalogerName: "apk-db-cataloger", want: "alpine", }, { name: "linux kernel cataloger", catalogerName: "linux-kernel-cataloger", want: "linux", }, { name: "binary classifier cataloger", catalogerName: "binary-classifier-cataloger", want: "binary", }, { name: "github actions cataloger", catalogerName: "github-actions-usage-cataloger", want: "github-actions", }, { name: "unknown cataloger defaults to other", catalogerName: "unknown-custom-cataloger", want: "other", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := inferEcosystem(tt.catalogerName) require.Equal(t, tt.want, got) }) } } func TestConvertToJSONSchemaTypesFromMetadata(t *testing.T) { tests := []struct { name string metadataTypes []string want []string }{ { name: "empty slice returns nil", metadataTypes: []string{}, want: nil, }, { name: "nil slice returns nil", metadataTypes: nil, want: nil, }, { name: "single metadata type", metadataTypes: []string{"pkg.AlpmDBEntry"}, want: []string{"AlpmDbEntry"}, }, { name: "multiple metadata types", metadataTypes: []string{"pkg.ApkDBEntry", "pkg.BinarySignature"}, want: []string{"ApkDbEntry", "BinarySignature"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := convertToJSONSchemaTypesFromMetadata(tt.metadataTypes) if diff := cmp.Diff(tt.want, got); diff != "" { t.Errorf("convertToJSONSchemaTypesFromMetadata() mismatch (-want +got):\n%s", diff) } }) } }