Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
module github.com/bomly-dev/bomly-plugin-scorecard-matcher

go 1.26.3
go 1.27.0

require (
github.com/anchore/packageurl-go v0.2.0
github.com/bomly-dev/bomly-sdk v0.3.0
github.com/bomly-dev/bomly-sdk v0.8.0
go.uber.org/zap v1.28.0
)

require (
github.com/CycloneDX/cyclonedx-go v0.11.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/github/go-spdx/v2 v2.7.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-plugin v1.8.0 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/package-url/packageurl-go v0.1.7 // indirect
github.com/spdx/tools-golang v0.6.0-rc4 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/sys v0.47.0 // indirect
Expand Down
28 changes: 22 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
github.com/anchore/packageurl-go v0.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T/EAErk=
github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE=
github.com/bomly-dev/bomly-sdk v0.3.0 h1:JtC7qZ9yq3r4fUYyq7e/Os4f9wGMa4Qot8U0l9MepFA=
github.com/bomly-dev/bomly-sdk v0.3.0/go.mod h1:yn1LBkoHG9gDBXKyRj0UNJo0BlXl8Bj9Ymb3WKLIh78=
github.com/CycloneDX/cyclonedx-go v0.11.0 h1:GokP8FiRC+foiuwWhSSLpSD5H4hSWtGnR3wo7apkBFI=
github.com/CycloneDX/cyclonedx-go v0.11.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByXjCZ2wuleUlwGEO8=
github.com/bomly-dev/bomly-sdk v0.8.0 h1:XpIr0yS5vg2B2oMt9xPornZ+krgy2AYlm/ts/At4JA0=
github.com/bomly-dev/bomly-sdk v0.8.0/go.mod h1:7RJLUANK8xHMZ5/r45zYxGyKUHC8yQiVem22yM0MyZw=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
Expand All @@ -11,6 +13,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/github/go-spdx/v2 v2.7.0 h1:GzfXx4wFdlilARxmFRXW/mgUy3A4vSqZocCMFV6XFdQ=
github.com/github/go-spdx/v2 v2.7.0/go.mod h1:Ftc45YYG1WzpzwEPKRVm9Jv8vDqOrN4gWoCkK+bHer0=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
Expand Down Expand Up @@ -38,12 +42,24 @@ github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPn
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/package-url/packageurl-go v0.1.7 h1:iFWg6tzAjLA6F/qX3M5nZaiMHJgc+p2zxVyr/fY+sZY=
github.com/package-url/packageurl-go v0.1.7/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spdx/tools-golang v0.6.0-rc4 h1:2GkvNr0DcnJHY9BDm3OYHo229jZS/h4qYDK+tHYXPOo=
github.com/spdx/tools-golang v0.6.0-rc4/go.mod h1:ruCHu3shgy7bVbZ7gtEU4Gq4fI08n2SdXtgV5PoN/OM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo=
github.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
Expand Down
25 changes: 13 additions & 12 deletions plugin/matcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"testing"

"github.com/bomly-dev/bomly-sdk"
"github.com/bomly-dev/bomly-sdk/testkit"
)

const sampleResponse = `{
Expand Down Expand Up @@ -45,7 +46,7 @@ func newMatcher(t *testing.T, base string) *Matcher {
return m
}

func newGraph(t *testing.T, deps ...*sdk.Dependency) *sdk.Graph {
func newGraph(t *testing.T, deps ...*sdk.DependencyNode) *sdk.Graph {
t.Helper()
g := sdk.New()
for _, d := range deps {
Expand All @@ -57,8 +58,8 @@ func newGraph(t *testing.T, deps ...*sdk.Dependency) *sdk.Graph {
}

// scorecardOf returns the enriched scorecard for a dependency from the registry.
func scorecardOf(reg *sdk.PackageRegistry, dep *sdk.Dependency) *sdk.PackageScorecard {
pkg, ok := reg.Get(sdk.CanonicalPackageURLFromDependency(dep))
func scorecardOf(reg *sdk.PackageRegistry, dep *sdk.DependencyNode) *sdk.PackageScorecard {
pkg, ok := reg.Get(dep.NodeID())
if !ok || pkg == nil {
return nil
}
Expand All @@ -76,7 +77,7 @@ func TestMatch_AttachesScorecardToPackages(t *testing.T) {
})

matcher := newMatcher(t, base)
dep := sdk.NewDependency(sdk.Dependency{Coordinates: sdk.Coordinates{Name: "scorecard", Version: "v5.0.0", PURL: "pkg:github/ossf/scorecard@v5.0.0"}})
dep := testkit.MustDependencyNode(t, "pkg:github/ossf/scorecard@v5.0.0")
g := newGraph(t, dep)
registry := sdk.NewPackageRegistry()

Expand Down Expand Up @@ -116,7 +117,7 @@ func TestMatch_CacheHitSkipsAPI(t *testing.T) {
if err != nil {
t.Fatalf("New: %v", err)
}
dep1 := sdk.NewDependency(sdk.Dependency{Coordinates: sdk.Coordinates{PURL: "pkg:github/ossf/scorecard@v5.0.0", Version: "v5.0.0"}})
dep1 := testkit.MustDependencyNode(t, "pkg:github/ossf/scorecard@v5.0.0")
reg1 := sdk.NewPackageRegistry()
if _, err := matcher1.Match(context.Background(), sdk.MatchRequest{Graph: newGraph(t, dep1), Registry: reg1}); err != nil {
t.Fatalf("first Match: %v", err)
Expand All @@ -127,7 +128,7 @@ func TestMatch_CacheHitSkipsAPI(t *testing.T) {
if err != nil {
t.Fatalf("New2: %v", err)
}
dep2 := sdk.NewDependency(sdk.Dependency{Coordinates: sdk.Coordinates{PURL: "pkg:github/ossf/scorecard@v5.0.0", Version: "v5.0.0"}})
dep2 := testkit.MustDependencyNode(t, "pkg:github/ossf/scorecard@v5.0.0")
reg2 := sdk.NewPackageRegistry()
if _, err := matcher2.Match(context.Background(), sdk.MatchRequest{Graph: newGraph(t, dep2), Registry: reg2}); err != nil {
t.Fatalf("second Match: %v", err)
Expand All @@ -146,7 +147,7 @@ func TestMatch_NotFoundCachedAsSentinel(t *testing.T) {
})

dir := t.TempDir()
dep1 := sdk.NewDependency(sdk.Dependency{Coordinates: sdk.Coordinates{PURL: "pkg:github/unscored/repo@1.0.0", Version: "1.0.0"}})
dep1 := testkit.MustDependencyNode(t, "pkg:github/unscored/repo@1.0.0")
reg1 := sdk.NewPackageRegistry()
matcher, err := New(Config{APIBase: base, CacheDir: dir})
if err != nil {
Expand All @@ -160,7 +161,7 @@ func TestMatch_NotFoundCachedAsSentinel(t *testing.T) {
}

// Second invocation should hit the sentinel cache (no extra API call).
dep2 := sdk.NewDependency(sdk.Dependency{Coordinates: sdk.Coordinates{PURL: "pkg:github/unscored/repo@1.0.0", Version: "1.0.0"}})
dep2 := testkit.MustDependencyNode(t, "pkg:github/unscored/repo@1.0.0")
reg2 := sdk.NewPackageRegistry()
if _, err := matcher.Match(context.Background(), sdk.MatchRequest{Graph: newGraph(t, dep2), Registry: reg2}); err != nil {
t.Fatalf("second Match: %v", err)
Expand All @@ -176,7 +177,7 @@ func TestMatch_ServerErrorIsNonFatal(t *testing.T) {
})

matcher := newMatcher(t, base)
dep := sdk.NewDependency(sdk.Dependency{Coordinates: sdk.Coordinates{PURL: "pkg:github/example/repo@1.0.0", Version: "1.0.0"}})
dep := testkit.MustDependencyNode(t, "pkg:github/example/repo@1.0.0")
reg := sdk.NewPackageRegistry()
if _, err := matcher.Match(context.Background(), sdk.MatchRequest{Graph: newGraph(t, dep), Registry: reg}); err != nil {
t.Fatalf("Match must not return an error on transport failure; got %v", err)
Expand All @@ -194,7 +195,7 @@ func TestMatch_SkipsPackagesWithoutResolvableRepo(t *testing.T) {
t.Cleanup(srv.Close)

matcher := newMatcher(t, srv.URL)
dep := sdk.NewDependency(sdk.Dependency{Coordinates: sdk.Coordinates{PURL: "pkg:npm/internal-only@1.0.0", Version: "1.0.0"}})
dep := testkit.MustDependencyNode(t, "pkg:npm/internal-only@1.0.0")
reg := sdk.NewPackageRegistry()
if _, err := matcher.Match(context.Background(), sdk.MatchRequest{Graph: newGraph(t, dep), Registry: reg}); err != nil {
t.Fatalf("Match: %v", err)
Expand All @@ -214,8 +215,8 @@ func TestMatch_ComponentModeOnlyEnrichesTarget(t *testing.T) {
})

matcher := newMatcher(t, base)
target := sdk.NewDependency(sdk.Dependency{Coordinates: sdk.Coordinates{PURL: "pkg:github/ossf/scorecard@v5.0.0", Version: "v5.0.0"}})
other := sdk.NewDependency(sdk.Dependency{Coordinates: sdk.Coordinates{PURL: "pkg:golang/github.com/sirupsen/logrus@v1.9.0", Version: "v1.9.0"}})
target := testkit.MustDependencyNode(t, "pkg:github/ossf/scorecard@v5.0.0")
other := testkit.MustDependencyNode(t, "pkg:golang/github.com/sirupsen/logrus@v1.9.0")
g := newGraph(t, target, other)
reg := sdk.NewPackageRegistry()

Expand Down
7 changes: 4 additions & 3 deletions plugin/module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

sdk "github.com/bomly-dev/bomly-sdk"
"github.com/bomly-dev/bomly-sdk/conformance"
"github.com/bomly-dev/bomly-sdk/testkit"
"go.uber.org/zap"
)

Expand Down Expand Up @@ -74,10 +75,10 @@ func newScorecardFixtureServer(t *testing.T) *httptest.Server {

func newDeltaGraphAndRegistry(t *testing.T) (*sdk.Graph, *sdk.PackageRegistry) {
t.Helper()
scored := sdk.NewDependency(sdk.Dependency{Coordinates: sdk.Coordinates{Name: "scorecard", Version: "v5.0.0", PURL: "pkg:github/ossf/scorecard@v5.0.0"}})
unscored := sdk.NewDependency(sdk.Dependency{Coordinates: sdk.Coordinates{Name: "left-pad", Version: "1.3.0", PURL: "pkg:npm/left-pad@1.3.0", Ecosystem: sdk.EcosystemNPM}})
scored := testkit.MustDependencyNode(t, "pkg:github/ossf/scorecard@v5.0.0")
unscored := testkit.MustDependencyNode(t, "pkg:npm/left-pad@1.3.0")
graph := sdk.New()
for _, dep := range []*sdk.Dependency{scored, unscored} {
for _, dep := range []*sdk.DependencyNode{scored, unscored} {
if err := graph.AddNode(dep); err != nil {
t.Fatalf("AddNode: %v", err)
}
Expand Down
47 changes: 39 additions & 8 deletions plugin/reporesolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import (
"regexp"
"strings"

"github.com/anchore/packageurl-go"
"github.com/bomly-dev/bomly-sdk"
"github.com/bomly-dev/bomly-sdk/purlkit"
)

// githubRepoPattern matches an org/repo segment in any github.com URL form
Expand All @@ -22,12 +22,22 @@ var githubRepoPattern = regexp.MustCompile(`github\.com[/:]([A-Za-z0-9_.-]+)/([A
// owner/repo, so it is safe to append to the api.scorecard.dev URL.
//
// Resolution order, cheapest first:
// 1. PURL `repository_url` / `vcs_url` qualifier (set by Syft for some
// ecosystems).
// 2. PURL of type `golang` — module path is the repo for github.com modules.
// 3. PURL of type `github` — `pkg:github/{owner}/{repo}`.
// 4. PackageResolvedURL — common for npm/pnpm/yarn tarballs hosted on GitHub.
// 5. NPM metadata `repository` link.
// 1. The package's detected origins — the vetted ADR-0033 repository or
// artifact URL the detector recorded.
// 2. PURL `repository_url` / `vcs_url` qualifier, for a PURL that arrived
// from outside the graph and still carries them.
// 3. PURL of type `golang` — module path is the repo for github.com modules.
// 4. PURL of type `github` — `pkg:github/{owner}/{repo}`.
// 5. PackageResolvedURL — common for npm/pnpm/yarn tarballs hosted on GitHub.
// 6. NPM metadata `repository` link.
//
// Origins lead because ADR-0041 moved the evidence there. The URL-valued
// qualifiers this used to read first — repository_url, download_url, vcs_url —
// are relocated out of a package PURL and into origins when a node is
// constructed, so the qualifier step alone would now find nothing for any
// package that came through the graph. That failure is silent: resolution
// returns "" and the package is simply never scored, which looks like a
// package with no GitHub source rather than a bug.
//
// Multiple packages frequently resolve to the same repo (a monorepo's npm
// packages all point at one source); the matcher dedupes by the returned
Expand All @@ -37,6 +47,9 @@ func resolveRepo(pkg *sdk.Package) string {
return ""
}

if repo := repoFromOrigins(pkg.DetectedOrigins); repo != "" {
return repo
}
if repo := repoFromPURL(pkg.PURL); repo != "" {
return repo
}
Expand All @@ -49,12 +62,30 @@ func resolveRepo(pkg *sdk.Package) string {
return ""
}

// repoFromOrigins reads the vetted origin evidence. A repository claim is
// preferred over an artifact URL: the first names a source repository
// outright, while the second is a download location that merely happens to
// be hosted on GitHub.
func repoFromOrigins(origins []sdk.DependencyOrigin) string {
for _, origin := range origins {
if repo := extractGithubRepo(origin.Repository); repo != "" {
return repo
}
}
for _, origin := range origins {
if repo := extractGithubRepo(origin.ArtifactURL); repo != "" {
return repo
}
}
return ""
}

func repoFromPURL(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
parsed, err := packageurl.FromString(raw)
parsed, err := purlkit.Parse(raw)
if err != nil {
return ""
}
Expand Down
79 changes: 79 additions & 0 deletions plugin/reporesolve_origins_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package plugin

import (
"testing"

sdk "github.com/bomly-dev/bomly-sdk"
)

// TestResolveRepoReadsDetectedOrigins pins the fix for a silent regression the
// SDK v0.7.0 bump would otherwise have introduced.
//
// This matcher used to read the repository out of the package PURL's
// repository_url / vcs_url / download_url qualifiers. ADR-0041 relocates those
// out of a package PURL and into origins at node construction, so that path
// now finds nothing for any package that came through the graph — and the
// failure is silent. Resolution returns "", the package is never scored, and
// the result is indistinguishable from a package that genuinely has no GitHub
// source.
func TestResolveRepoReadsDetectedOrigins(t *testing.T) {
cases := []struct {
name string
pkg *sdk.Package
want string
}{
{
name: "repository origin",
pkg: &sdk.Package{
PURL: "pkg:npm/left-pad@1.3.0",
DetectedOrigins: []sdk.DependencyOrigin{{Repository: "https://github.com/ossf/scorecard", Revision: "abc123"}},
},
want: "github.com/ossf/scorecard",
},
{
name: "artifact origin hosted on github",
pkg: &sdk.Package{
PURL: "pkg:npm/left-pad@1.3.0",
DetectedOrigins: []sdk.DependencyOrigin{{ArtifactURL: "https://github.com/ossf/scorecard/archive/v5.0.0.tar.gz"}},
},
want: "github.com/ossf/scorecard",
},
{
name: "a repository claim outranks an artifact URL",
pkg: &sdk.Package{
PURL: "pkg:npm/left-pad@1.3.0",
DetectedOrigins: []sdk.DependencyOrigin{
{ArtifactURL: "https://github.com/mirror/copy/archive/v1.tar.gz"},
{Repository: "https://github.com/ossf/scorecard"},
},
},
want: "github.com/ossf/scorecard",
},
{
name: "no github source anywhere",
pkg: &sdk.Package{
PURL: "pkg:npm/left-pad@1.3.0",
DetectedOrigins: []sdk.DependencyOrigin{{Repository: "https://gitlab.com/owner/repo"}},
},
want: "",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := resolveRepo(tc.pkg); got != tc.want {
t.Errorf("resolveRepo = %q, want %q", got, tc.want)
}
})
}
}

// TestResolveRepoStillReadsQualifiers pins that the qualifier path is kept as
// a fallback. A PURL that arrived from outside the graph — an ingested SBOM
// that was never through a node constructor — can still carry them, and
// dropping the step would lose a source the document did state.
func TestResolveRepoStillReadsQualifiers(t *testing.T) {
pkg := &sdk.Package{PURL: "pkg:npm/left-pad@1.3.0?repository_url=https://github.com/ossf/scorecard"}
if got := resolveRepo(pkg); got != "github.com/ossf/scorecard" {
t.Errorf("resolveRepo = %q, want the qualifier fallback to still work", got)
}
}