Skip to content

Commit e9d63e1

Browse files
bomly-guyclaude
andcommitted
feat(matchers): declare supported ecosystems on built-in matchers
`sdk.MatcherDescriptor.SupportedEcosystems` existed but no built-in matcher populated it, and an empty list reads as "all ecosystems". That was wrong for deps.dev and Grype, which both silently skip packages they cannot map. - deps.dev now declares the seven ecosystems `depsDevSystem` accepts. - Grype declares its set per build tag: builtin mode mirrors the `ecosystemToSyftType` mapping (19 ecosystems), while external mode stays nil because the grype CLI derives coverage from PURLs and is unbounded. - OSV and Scorecard keep nil, now with comments explaining why that is accurate rather than unset: OSV queries by PURL so OSV.dev decides coverage, and Scorecard is bounded by GitHub source-repo resolution, not by ecosystem. - The three auditors document that they are ecosystem-agnostic by construction. Drift tests keep both declarations in step with the mappings they mirror. The generated matcher docs now carry an Ecosystems row, and `bomly plugins list` gains an ECOSYSTEMS column for matchers ("all" when unbounded). Noted while mapping Grype: apk, dpkg, and rpm packages are typed but reach Grype without a distro, and its OS matchers are distro-namespace driven, so they currently match nothing. They are declared because the mapping exists; plumbing the distro through is left for a separate change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent c31a69d commit e9d63e1

18 files changed

Lines changed: 227 additions & 14 deletions

File tree

docs/matchers/depsdev-license-matcher.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ Fetches package metadata from deps.dev to improve license coverage.
1313
| Cache behavior | Uses Bomly's matcher cache; cache failures are non-fatal. |
1414
| Output fields | license value, license source, matched package flag |
1515
| Tags | `license-enrichment`, `batch-http` |
16+
| Ecosystems | `npm`, `maven`, `go`, `python`, `dotnet`, `ruby`, `rust` |
1617

1718
## User notes
1819

1920
Run with `--enrich` when you want license metadata from deps.dev.
2021

2122
## What `depsdev-license-matcher` does
2223

23-
`depsdev-license-matcher` fetches package metadata from [deps.dev](https://deps.dev) (Google's open package metadata service) and attaches license information to packages that the detector did not resolve a license for. deps.dev coverage is strongest for npm, Go, Maven, NuGet, PyPI, Cargo, and RubyGems.
24+
`depsdev-license-matcher` fetches package metadata from [deps.dev](https://deps.dev) (Google's open package metadata service) and attaches license information to packages that the detector did not resolve a license for. It covers the ecosystems listed in the table above; packages from any other ecosystem are skipped.
2425

2526
## When to use it
2627

docs/matchers/grype.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Uses Grype vulnerability matching against the resolved dependency graph.
1212
| Uses network | No |
1313
| Cache behavior | Relies on Grype's local vulnerability database behavior. |
1414
| Output fields | vulnerability ID, severity, CVSS, fixed version, fix state, EPSS, CWE, known exploitation, risk score, references |
15+
| Ecosystems | `npm`, `maven`, `scala`, `go`, `python`, `dotnet`, `ruby`, `rust`, `dart`, `elixir`, `erlang`, `php`, `swift`, `haskell`, `r`, `lua`, `apk`, `dpkg`, `rpm` |
1516

1617
## User notes
1718

docs/plugins/how-to-implement-matcher.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ func (m *matcher) Descriptor(context.Context) (*sdk.MatcherDescriptor, error) {
3030
DisplayName: "ClearlyDefined License Matcher",
3131
Aliases: []string{"clearlydefined", "licenses"},
3232
Tags: []string{"license-enrichment", "http", "cache"},
33+
// Declare the ecosystems your matcher can actually enrich. Leave this
34+
// empty only when the matcher works for every ecosystem — an empty
35+
// list reads as "all". `bomly plugins list` and the generated docs
36+
// show whatever you put here.
37+
SupportedEcosystems: []sdk.Ecosystem{
38+
sdk.EcosystemNPM,
39+
sdk.EcosystemMaven,
40+
sdk.EcosystemGo,
41+
},
3342
}, nil
3443
}
3544

internal/auditors/license/auditor.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ type Auditor struct {
3030

3131
func (a Auditor) Descriptor() sdk.AuditorDescriptor {
3232
return sdk.AuditorDescriptor{
33+
// No SupportedEcosystems: policy is evaluated over SPDX license
34+
// expressions, whichever ecosystem produced them. Discovering the license
35+
// is the matchers' job.
3336
Name: auditorName,
3437
}
3538
}

internal/auditors/package/auditor.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ type Auditor struct {
2222

2323
func (a Auditor) Descriptor() sdk.AuditorDescriptor {
2424
return sdk.AuditorDescriptor{
25+
// No SupportedEcosystems: deny rules match on package name and group,
26+
// and the typosquat corpus is the baseline graph itself rather than a
27+
// per-ecosystem package list.
2528
Name: auditorName,
2629
}
2730
}

internal/auditors/vulnerability/auditor.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ type Auditor struct {
1717

1818
func (a Auditor) Descriptor() sdk.AuditorDescriptor {
1919
return sdk.AuditorDescriptor{
20+
// No SupportedEcosystems: this auditor reads the vulnerability findings
21+
// matchers already attached and applies severity and allowlist policy, so
22+
// it is ecosystem-agnostic by construction.
2023
Name: auditorName,
2124
}
2225
}

internal/cli/plugin_cmd.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1091,7 +1091,7 @@ func renderPluginListTables(items []managedplugin.Info, kindFilter pluginKindFil
10911091
appendTable("Detectors", []string{"ECOSYSTEMS", "PACKAGE MANAGERS", "NAME", "TYPE", "STATE"}, detectorPluginRows(detectors))
10921092
}
10931093
if kindFilter.includes(plugschema.PluginKindMatcher) {
1094-
appendTable("Matchers", []string{"NAME", "TYPE", "STATE"}, basicPluginRows(matchers))
1094+
appendTable("Matchers", []string{"ECOSYSTEMS", "NAME", "TYPE", "STATE"}, matcherPluginRows(matchers))
10951095
}
10961096
if kindFilter.includes(plugschema.PluginKindAuditor) {
10971097
appendTable("Auditors", []string{"NAME", "TYPE", "STATE"}, basicPluginRows(auditors))
@@ -1120,6 +1120,19 @@ func detectorPluginRows(items []managedplugin.Info) [][]string {
11201120
return rows
11211121
}
11221122

1123+
func matcherPluginRows(items []managedplugin.Info) [][]string {
1124+
rows := make([][]string, 0, len(items))
1125+
for _, info := range items {
1126+
rows = append(rows, []string{
1127+
nonEmptyString(summarizePluginListValue(pluginMatcherEcosystems(info), 6), "-"),
1128+
pluginListName(info),
1129+
colorPluginType(pluginTypeValue(info), info),
1130+
colorPluginState(pluginStateValue(info)),
1131+
})
1132+
}
1133+
return rows
1134+
}
1135+
11231136
func basicPluginRows(items []managedplugin.Info) [][]string {
11241137
rows := make([][]string, 0, len(items))
11251138
for _, info := range items {
@@ -1389,6 +1402,30 @@ func pluginAnalyzerEcosystems(info managedplugin.Info) string {
13891402
return strings.Join(items, ", ")
13901403
}
13911404

1405+
// pluginMatcherEcosystems lists the ecosystems a matcher can enrich. Matchers
1406+
// that are not ecosystem-bound declare nothing, which reads as "all" rather
1407+
// than an empty cell.
1408+
func pluginMatcherEcosystems(info managedplugin.Info) string {
1409+
if info.MatcherDescriptor == nil {
1410+
return ""
1411+
}
1412+
items := make([]string, 0, len(info.MatcherDescriptor.SupportedEcosystems))
1413+
for _, ecosystem := range info.MatcherDescriptor.SupportedEcosystems {
1414+
name := strings.TrimSpace(string(ecosystem))
1415+
if name == "" {
1416+
continue
1417+
}
1418+
if !containsPluginValue(items, name) {
1419+
items = append(items, name)
1420+
}
1421+
}
1422+
if len(items) == 0 {
1423+
return "all"
1424+
}
1425+
sort.Strings(items)
1426+
return strings.Join(items, ", ")
1427+
}
1428+
13921429
func pluginAnalyzerPackageManagers(info managedplugin.Info) string {
13931430
if info.AnalyzerDescriptor == nil {
13941431
return ""

internal/cli/plugin_cmd_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,13 @@ func TestPluginList_KindFilterMatchers(t *testing.T) {
6969
t.Fatalf("expected matcher section in output, got:\n%s", text)
7070
}
7171
matcherHeader := tableHeaderLine(t, render.StripANSI(text), "NAME")
72-
assertInOrder(t, matcherHeader, []string{"NAME", "TYPE", "STATE"})
73-
for _, omitted := range []string{"ECOSYSTEMS", "PACKAGE MANAGERS", "VERSION"} {
72+
assertInOrder(t, matcherHeader, []string{"ECOSYSTEMS", "NAME", "TYPE", "STATE"})
73+
// A matcher that declares no ecosystems is not ecosystem-bound, which the
74+
// table shows as "all" rather than leaving the cell empty.
75+
if !strings.Contains(render.StripANSI(text), "all") {
76+
t.Fatalf("expected unrestricted matchers to render ecosystems as %q, got:\n%s", "all", text)
77+
}
78+
for _, omitted := range []string{"PACKAGE MANAGERS", "VERSION"} {
7479
if strings.Contains(text, omitted) {
7580
t.Fatalf("expected matcher table to omit %q, got:\n%s", omitted, text)
7681
}

internal/matchers/depsdev/matcher.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,19 @@ func (c *Checker) Descriptor() sdk.MatcherDescriptor {
138138
DisplayName: "deps.dev License Matcher",
139139
Aliases: []string{"deps.dev"},
140140
Tags: []string{"license-enrichment", "batch-http"},
141+
// Kept in step with depsDevSystem, which is the set of ecosystems
142+
// deps.dev exposes a package system for. Packages from anything else
143+
// are skipped, so declaring the list keeps the generated docs and
144+
// `bomly plugins list` honest instead of implying full coverage.
145+
SupportedEcosystems: []sdk.Ecosystem{
146+
sdk.EcosystemNPM,
147+
sdk.EcosystemMaven,
148+
sdk.EcosystemGo,
149+
sdk.EcosystemPython,
150+
sdk.EcosystemDotNet,
151+
sdk.EcosystemRuby,
152+
sdk.EcosystemRust,
153+
},
141154
}
142155
}
143156

internal/matchers/depsdev/matcher_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,3 +355,36 @@ func TestCheckerMatch_EnrichesMissingOnly(t *testing.T) {
355355
}
356356
}
357357
}
358+
359+
// The descriptor's ecosystem list is what the generated docs and
360+
// `bomly plugins list` show, so it has to stay in step with the systems
361+
// depsDevSystem actually accepts. This catches a new case being added to one
362+
// without the other.
363+
func TestDescriptorEcosystemsMatchSupportedSystems(t *testing.T) {
364+
all := []sdk.Ecosystem{
365+
sdk.EcosystemNPM, sdk.EcosystemMaven, sdk.EcosystemGo, sdk.EcosystemPython,
366+
sdk.EcosystemALPM, sdk.EcosystemAPK, sdk.EcosystemCPP, sdk.EcosystemConda,
367+
sdk.EcosystemDart, sdk.EcosystemDPKG, sdk.EcosystemElixir, sdk.EcosystemErlang,
368+
sdk.EcosystemGitHub, sdk.EcosystemHaskell, sdk.EcosystemHomebrew, sdk.EcosystemLua,
369+
sdk.EcosystemDotNet, sdk.EcosystemNix, sdk.EcosystemOCaml, sdk.EcosystemPHP,
370+
sdk.EcosystemPortage, sdk.EcosystemProlog, sdk.EcosystemR, sdk.EcosystemRPM,
371+
sdk.EcosystemRuby, sdk.EcosystemRust, sdk.EcosystemScala, sdk.EcosystemSBOM,
372+
sdk.EcosystemSnap, sdk.EcosystemSwift, sdk.EcosystemTerraform,
373+
sdk.EcosystemWordPress, sdk.EcosystemOther,
374+
}
375+
376+
declared := make(map[sdk.Ecosystem]bool)
377+
for _, eco := range (&Checker{}).Descriptor().SupportedEcosystems {
378+
declared[eco] = true
379+
}
380+
381+
for _, eco := range all {
382+
_, supported := depsDevSystem(string(eco))
383+
if supported && !declared[eco] {
384+
t.Errorf("depsDevSystem accepts %q but the descriptor does not declare it", eco)
385+
}
386+
if !supported && declared[eco] {
387+
t.Errorf("descriptor declares %q but depsDevSystem rejects it", eco)
388+
}
389+
}
390+
}

0 commit comments

Comments
 (0)