Skip to content
Open
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
97 changes: 97 additions & 0 deletions vulnfeeds/cmd/combine-to-osv/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import (
"log/slog"
"os"
"path"
"sort"
"path/filepath"
"strings"

"slices"

"cloud.google.com/go/storage"
"github.com/google/osv/vulnfeeds/cves"
gitpurl "github.com/google/osv/vulnfeeds/git"
"github.com/google/osv/vulnfeeds/utility/logger"
"github.com/ossf/osv-schema/bindings/go/osvschema"
"google.golang.org/api/iterator"
Expand Down Expand Up @@ -191,6 +193,7 @@ func combineIntoOSV(cve5osv map[cves.CVEID]osvschema.Vulnerability, nvdosv map[c
if len(nvd.Affected) == 0 {
continue
}
enrichRepoPURLs(convertedCve)
vulns[cveID] = nvd
}

Expand Down Expand Up @@ -366,3 +369,97 @@ func writeOSVFile(osvData map[cves.CVEID]osvschema.Vulnerability, osvOutputPath

logger.Info("Successfully written OSV files", slog.Int("count", len(osvData)))
}

// repoURLFromRanges returns the first repo URL from a GIT-type range, if present.
func repoURLFromRanges(ranges []osvschema.Range) string {
for _, r := range ranges {
if r.Type == "GIT" && r.Repo != "" {
return r.Repo
}
}

return ""
}

// enrichRepoPURLs sets affected.package.purl to an unversioned pkg:generic repo pURL
// when a GIT range with a repo URL exists and purl is currently empty.
func enrichRepoPURLs(v *vulns.Vulnerability) {
if v == nil || len(v.Affected) == 0 {
return
}
for i := range v.Affected {
aff := &v.Affected[i]

// Ensure base purl is set (unversioned).
if aff.Package.Purl == "" {
if repo := repoURLFromRanges(aff.Ranges); repo != "" {
if p, err := gitpurl.BuildGenericRepoPURL(repo); err == nil && p != "" {
aff.Package.Purl = p
}
}
}

// Add versioned repo pURLs when possible.
if repo := repoURLFromRanges(aff.Ranges); repo != "" {
addVersionedRepoPURLs(aff, repo)
}
}
}

var repoTagsCache = make(gitpurl.RepoTagsCache)

// addVersionedRepoPURLs populates affected.database_specific["repo_purls"]
// with pkg:generic/...@<tag> entries, using affected.versions if available.
func addVersionedRepoPURLs(aff *osvschema.Affected, repo string) {
if aff == nil || repo == "" {
return
}

var tags []string
if len(aff.Versions) > 0 {
tags = append(tags, aff.Versions...)
} else if os.Getenv("ENABLE_REPO_PURL_TAGS") == "1" {
norm, err := gitpurl.NormalizeRepoTags(repo, repoTagsCache)
if err == nil && len(norm) > 0 {
for tag := range norm {
tags = append(tags, tag)
}
sort.Strings(tags)
const maxTags = 200
if len(tags) > maxTags {
tags = tags[:maxTags]
}
}
}

if len(tags) == 0 {
return
}

base, err := gitpurl.BuildGenericRepoPURL(repo)
if err != nil || base == "" {
return
}

// Dedup and format.
seen := make(map[string]struct{}, len(tags))
vPURLs := make([]string, 0, len(tags))
for _, t := range tags {
if t == "" {
continue
}
if _, ok := seen[t]; ok {
continue
}
seen[t] = struct{}{}
vPURLs = append(vPURLs, base+"@"+t)
}
if len(vPURLs) == 0 {
return
}

if aff.DatabaseSpecific == nil {
aff.DatabaseSpecific = map[string]any{}
}
aff.DatabaseSpecific["repo_purls"] = vPURLs
}
77 changes: 77 additions & 0 deletions vulnfeeds/cmd/combine-to-osv/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/osv/vulnfeeds/cves"
gitpurl "github.com/google/osv/vulnfeeds/git"
"github.com/google/osv/vulnfeeds/utility"
"github.com/ossf/osv-schema/bindings/go/osvschema"
)

Expand Down Expand Up @@ -427,3 +429,78 @@ func TestCombineTwoOSVRecords(t *testing.T) {
t.Errorf("combineTwoOSVRecords() mismatch (-want +got):\n%s", diff)
}
}

func TestRepoURLFromRanges_GIT(t *testing.T) {
t.Parallel()

ranges := []osvschema.Range{
{
Type: "GIT",
Repo: "https://github.com/eclipse-openj9/openj9",
Events: []osvschema.Event{
{Introduced: "0"},
},
},
}
got := repoURLFromRanges(ranges)
want := "https://github.com/eclipse-openj9/openj9"
if got != want {
t.Fatalf("repoURLFromRanges() = %q, want %q", got, want)
}
}

func TestRepoURLFromRanges_NoGIT(t *testing.T) {
t.Parallel()

ranges := []osvschema.Range{
{
Type: "ECOSYSTEM",
Events: []osvschema.Event{
{Introduced: "0"},
{Fixed: "1.2.3"},
},
},
}
if got := repoURLFromRanges(ranges); got != "" {
t.Fatalf("repoURLFromRanges() = %q, want empty", got)
}
}

func TestAddVersionedRepoPURLs_FromVersions(t *testing.T) {
t.Setenv("ENABLE_REPO_PURL_TAGS", "") // ensure derivation path is off

repo := "https://github.com/chriskohlhoff/asio"
aff := &osvschema.Affected{
Package: osvschema.Package{Ecosystem: "GIT", Name: "asio"},
Versions: []string{"asio-1-13-0", "asio-1-12-0"},
Ranges: []osvschema.Range{{Type: "GIT", Repo: repo, Events: []osvschema.Event{{Introduced: "0"}}}},
}

addVersionedRepoPURLs(aff, repo)

base, err := gitpurl.BuildGenericRepoPURL(repo)
if err != nil || base == "" {
t.Fatalf("failed to build base purl: %v", err)
}

ds := aff.DatabaseSpecific
list, ok := ds["repo_purls"].([]string)
if !ok || len(list) == 0 {
t.Fatalf("repo_purls missing/empty: %#v", ds)
}

want1 := base + "@asio-1-13-0"
want2 := base + "@asio-1-12-0"
found1, found2 := false, false
for _, p := range list {
if p == want1 {
found1 = true
}
if p == want2 {
found2 = true
}
}
if !found1 || !found2 {
t.Fatalf("missing expected entries, got %#v", list)
}
}
33 changes: 33 additions & 0 deletions vulnfeeds/git/purl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package git

import (
"fmt"
"net/url"
"strings"

packageurl "github.com/package-url/packageurl-go"
)

// BuildGenericRepoPURL returns an unversioned generic purl
// Example: pkg:generic/github.com/owner/repo
func BuildGenericRepoPURL(repoURL string) (string, error) {
u, err := url.Parse(repoURL)
if err != nil {
return "", fmt.Errorf("invalid repo url: %w", err)
}

host := strings.ToLower(u.Hostname())
path := strings.Trim(strings.TrimSuffix(u.EscapedPath(), ".git"), "/")
parts := strings.Split(path, "/")
if len(parts) < 2 {
return "", fmt.Errorf("invalid repo path in %q", repoURL)
}

// Namespace is host + all path segments except the last; name is the last segment.
ns := strings.Join(append([]string{host}, parts[:len(parts)-1]...), "/")
name := parts[len(parts)-1]

p := packageurl.NewPackageURL("generic", ns, name, "", nil, "")

return p.ToString(), nil
}
57 changes: 57 additions & 0 deletions vulnfeeds/git/repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,3 +383,60 @@ func TestInvalidRepos(t *testing.T) {
t.Errorf("These redundant repos are in InvalidRepos: %s", diff)
}
}

func TestBuildGenericRepoPURL(t *testing.T) {
t.Parallel()

tests := []struct {
desc string
inputURL string
wantPURL string
wantError bool
}{
{
desc: "GitHub repo",
inputURL: "https://github.com/eclipse-openj9/openj9",
wantPURL: "pkg:generic/github.com/eclipse-openj9/openj9",
},
{
desc: "GitHub repo with .git suffix",
inputURL: "https://github.com/torvalds/linux.git",
wantPURL: "pkg:generic/github.com/torvalds/linux",
},
{
desc: "GitLab subgroup repo",
inputURL: "https://gitlab.com/group/subgroup/repo",
wantPURL: "pkg:generic/gitlab.com/group/subgroup/repo",
},
{
desc: "Self-hosted cgit repo with .git",
inputURL: "https://git.libssh.org/projects/libssh.git",
wantPURL: "pkg:generic/git.libssh.org/projects/libssh",
},
{
desc: "Insufficient path segments",
inputURL: "https://github.com/onlyowner",
wantError: true,
},
}

for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()
got, err := BuildGenericRepoPURL(tc.inputURL)
if tc.wantError {
if err == nil {
t.Fatalf("BuildGenericRepoPURL(%q) = %q, want error", tc.inputURL, got)
}

return
}
if err != nil {
t.Fatalf("BuildGenericRepoPURL(%q) unexpected error: %v", tc.inputURL, err)
}
if got != tc.wantPURL {
t.Fatalf("BuildGenericRepoPURL(%q) = %q, want %q", tc.inputURL, got, tc.wantPURL)
}
})
}
}
2 changes: 1 addition & 1 deletion vulnfeeds/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/google/osv-scanner v1.9.2
github.com/knqyf263/go-cpe v0.0.0-20230627041855-cb0794d06872
github.com/ossf/osv-schema/bindings/go v0.0.0-20250926044009-f6ae0b6bae32
github.com/package-url/packageurl-go v0.1.3
github.com/sethvargo/go-retry v0.3.0
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b
google.golang.org/api v0.247.0
Expand Down Expand Up @@ -65,7 +66,6 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/package-url/packageurl-go v0.1.3 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
Expand Down