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
108 changes: 96 additions & 12 deletions vulnfeeds/conversion/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ func GitVersionsToCommits(versionRanges []models.RangeWithMetadata, repos []stri
canonicalRepo, err := git.FindCanonicalLink(vr.Range.GetRepo(), httpClient, cache)
if err != nil {
if git.IsRateLimit(err) {
metrics.Outcome = models.Error
metrics.SetError(err)
return nil, nil, nil
}
} else {
Expand All @@ -209,7 +209,7 @@ func GitVersionsToCommits(versionRanges []models.RangeWithMetadata, repos []stri
if err != nil {
metrics.AddNotef("Failed to find canonical link - %s %v", repo, err)
if git.IsRateLimit(err) {
metrics.Outcome = models.Error
metrics.SetError(err)
return nil, nil, nil
}

Expand All @@ -218,11 +218,11 @@ func GitVersionsToCommits(versionRanges []models.RangeWithMetadata, repos []stri

normalizedTags, err := git.NormalizeRepoTags(repo, cache, httpClient)
if err != nil {
metrics.AddNotef("Failed to normalize tags - %s: %v", repo, err)
if git.IsRateLimit(err) {
metrics.Outcome = models.Error
metrics.SetError(err)
return nil, nil, nil
}
metrics.AddNotef("Failed to normalize tags - %s", repo)

continue
}
Expand All @@ -234,7 +234,7 @@ func GitVersionsToCommits(versionRanges []models.RangeWithMetadata, repos []stri
canonicalVRepo, err := git.FindCanonicalLink(vRepo, httpClient, cache)
if err != nil {
if git.IsRateLimit(err) {
metrics.Outcome = models.Error
metrics.SetError(err)
return nil, nil, nil
}
} else {
Expand Down Expand Up @@ -727,21 +727,105 @@ func AddFieldToDatabaseSpecific(ds *structpb.Struct, field string, value any) er
return nil
}

// IsGitCommitSHA checks whether a string is a valid 40-character (SHA-1) or 64-character (SHA-256) hexadecimal Git commit hash.
func IsGitCommitSHA(s string) bool {
s = strings.TrimSpace(s)
if len(s) != 40 && len(s) != 64 {
return false
}
for _, c := range s {
if (c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F') {
return false
}
}

return true
}

// IsDirectGitRange determines if a range is already composed of Git commit hashes rather than tag/version names.
func IsDirectGitRange(vr models.RangeWithMetadata) bool {
if vr.Range == nil {
return false
}
events := vr.Range.GetEvents()
if len(events) == 0 {
return false
}

hasCommit := false
for _, e := range events {
intro := e.GetIntroduced()
if intro != "" && intro != "0" {
if !IsGitCommitSHA(intro) {
return false
}
hasCommit = true
}
fixed := e.GetFixed()
if fixed != "" {
if !IsGitCommitSHA(fixed) {
return false
}
hasCommit = true
}
lastAffected := e.GetLastAffected()
if lastAffected != "" {
if !IsGitCommitSHA(lastAffected) {
return false
}
hasCommit = true
}
}

return hasCommit
}

// ProcessRanges attempts to resolve the given ranges to commits and updates the metrics accordingly.
func ProcessRanges(ranges []models.RangeWithMetadata, repos []string, metrics *models.ConversionMetrics, cache git.RepoTagsCache, httpClient *http.Client) ([]models.RangeWithMetadata, []models.RangeWithMetadata, []string) {
if len(ranges) == 0 {
return nil, nil, nil
}

r, un, sR := GitVersionsToCommits(ranges, repos, metrics, cache, httpClient)
if len(r) > 0 {
metrics.ResolvedRangesCount += len(r)
var resolvedRanges []models.RangeWithMetadata
var unresolvedRanges []models.RangeWithMetadata
var successfulRepos []string
var tagVersionRanges []models.RangeWithMetadata

for _, vr := range ranges {
if IsDirectGitRange(vr) {
repo := vr.Range.GetRepo()
if repo == "" && len(repos) > 0 {
repo = repos[0]
}
if repo != "" {
vr.Range.Repo = repo
vr.Range.Type = osvschema.Range_GIT
resolvedRanges = append(resolvedRanges, vr)
successfulRepos = append(successfulRepos, repo)
} else {
metrics.AddNotef("no repository available for git commit range")
unresolvedRanges = append(unresolvedRanges, vr)
}
} else {
tagVersionRanges = append(tagVersionRanges, vr)
}
}

if len(tagVersionRanges) > 0 {
r, un, sR := GitVersionsToCommits(tagVersionRanges, repos, metrics, cache, httpClient)
resolvedRanges = append(resolvedRanges, r...)
unresolvedRanges = append(unresolvedRanges, un...)
successfulRepos = append(successfulRepos, sR...)
}

if len(resolvedRanges) > 0 {
metrics.ResolvedRangesCount += len(resolvedRanges)
metrics.SetOutcome(models.Successful)
}

if len(un) > 0 {
metrics.UnresolvedRangesCount += len(un)
if len(r) == 0 {
if len(unresolvedRanges) > 0 {
metrics.UnresolvedRangesCount += len(unresolvedRanges)
if len(resolvedRanges) == 0 {
metrics.SetOutcome(models.NoCommitRanges)
}
}
Expand All @@ -758,7 +842,7 @@ func ProcessRanges(ranges []models.RangeWithMetadata, repos []string, metrics *m
}
}

return r, un, sR
return resolvedRanges, unresolvedRanges, successfulRepos
}

func LoadCPEDictionary(productToRepo *VPRepoCache, f string) error {
Expand Down
88 changes: 72 additions & 16 deletions vulnfeeds/conversion/cve5/__snapshots__/converter_test.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2010,14 +2010,6 @@
}
],
"source": "AFFECTED_FIELD"
},
{
"extracted_events": [
{
"fixed": "7.61.1"
}
],
"source": "DESCRIPTION"
}
]
},
Expand Down Expand Up @@ -2363,14 +2355,6 @@
}
],
"source": "AFFECTED_FIELD"
},
{
"extracted_events": [
{
"fixed": "7.61.1"
}
],
"source": "DESCRIPTION"
}
]
},
Expand Down Expand Up @@ -4162,3 +4146,75 @@
"summary": "Xz: malicious code in distributed source"
}
---

[TestCVE5Snapshot/CVE-2026-67185.json - 1]
{
"affected": [
{
"ranges": [
{
"events": [
{
"introduced": "0b3b5fdb5a058f50248cd8547824936b8dd10351"
},
{
"last_affected": "a381da252fe8e873c8aff22703040426cc9b2ae0"
}
],
"repo": "https://github.com/GeneralSandman/TinyWeb",
"type": "GIT"
}
]
}
],
"database_specific": {
"cna_assigner": "VulnCheck",
"cwe_ids": [
"CWE-22"
],
"osv_generated_from": "unknown",
"unresolved_ranges": [
{
"extracted_events": [
{
"introduced": "0.0.8"
},
{
"last_affected": "0.0.8"
}
],
"source": "AFFECTED_FIELD"
}
]
},
"details": "TinyWeb through 0.0.8 contains a path traversal vulnerability that allows unauthenticated attackers to read arbitrary files by submitting ../ sequences in the URL path, which are concatenated directly to the configured web root in HttpBuilder::buildResponse() without normalization, dot-segment removal, or boundary checks. Attackers can craft a single request with ../ sequences that pass through the URL parser unchanged and reach the filesystem call via HttpFile::setFile(), exposing sensitive files such as credential stores and private keys when the server process runs as root.",
"id": "CVE-2026-67185",
"modified": "2026-07-28T19:34:14.583Z",
"published": "2026-07-28T16:30:45.229Z",
"references": [
{
"type": "PACKAGE",
"url": "https://github.com/GeneralSandman/TinyWeb"
},
{
"type": "EVIDENCE",
"url": "https://github.com/theopaid/Unauthenticated-Path-Traversal-Allows-Arbitrary-File-Read-TinyWeb-"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-67185"
},
{
"type": "ADVISORY",
"url": "https://www.vulncheck.com/advisories/tinyweb-path-traversal-via-url-path-component"
}
],
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "TinyWeb 0.0.8 Path Traversal via URL Path Component"
}
---
80 changes: 7 additions & 73 deletions vulnfeeds/conversion/cve5/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,91 +2,25 @@ package cve5

import (
"cmp"
"errors"
"strconv"
"strings"

c "github.com/google/osv.dev/vulnfeeds/conversion"
"github.com/google/osv.dev/vulnfeeds/models"
"github.com/google/osv.dev/vulnfeeds/vulns"
"github.com/ossf/osv-schema/bindings/go/osvschema"
"github.com/google/osv.dev/vulnfeeds/conversion/cve5/strategies"
)

// VersionRangeType represents the type of versioning scheme for a range.
type VersionRangeType int

const acceptableQuality = vulns.Spaces
type VersionRangeType = strategies.VersionRangeType

const (
VersionRangeTypeUnknown VersionRangeType = iota
VersionRangeTypeGit
VersionRangeTypeSemver
VersionRangeTypeEcosystem
VersionRangeTypeUnknown = strategies.VersionRangeTypeUnknown
VersionRangeTypeGit = strategies.VersionRangeTypeGit
VersionRangeTypeSemver = strategies.VersionRangeTypeSemver
VersionRangeTypeEcosystem = strategies.VersionRangeTypeEcosystem
)

// String returns the string representation of a VersionRangeType.
func (vrt VersionRangeType) String() string {
switch vrt {
case VersionRangeTypeGit:
return "git"
case VersionRangeTypeEcosystem:
return "ecosystem"
case VersionRangeTypeSemver:
return "semver"
default:
return "unknown"
}
}

// toVersionRangeType converts a string to a VersionRangeType.
func toVersionRangeType(s string) VersionRangeType {
switch strings.ToLower(s) {
case "git":
return VersionRangeTypeGit
case "semver":
return VersionRangeTypeSemver
default:
// Other version types like "semver" are treated as ecosystem ranges.
return VersionRangeTypeEcosystem
}
}

// findCPEVersionRanges extracts version ranges and CPE strings from the CNA's
// CPE applicability statements in a CVE record.
func findCPEVersionRanges(cve models.CVE5) (versionRanges []models.RangeWithMetadata, cpes []string, err error) {
// TODO(jesslowe): Add logic to also extract CPEs from the 'affected' field (e.g., CVE-2025-1110).
for _, cpe := range cve.Containers.CNA.CPEApplicability {
for _, node := range cpe.Nodes {
if node.Operator != "OR" {
continue
}
for _, match := range node.CPEMatch {
if !match.Vulnerable {
continue
}
cpes = append(cpes, match.Criteria)

// If no start version is given, assume the vulnerability starts from version "0".
if match.VersionStartIncluding == "" {
match.VersionStartIncluding = "0"
}
var nr []*osvschema.Range
if match.VersionEndExcluding != "" {
nr = append(nr, c.BuildVersionRange(match.VersionStartIncluding, "", match.VersionEndExcluding))
} else if match.VersionEndIncluding != "" {
nr = append(nr, c.BuildVersionRange(match.VersionStartIncluding, match.VersionEndIncluding, ""))
}
if nr != nil {
versionRanges = append(versionRanges, c.ToRangeWithMetadata(nr, models.VersionSourceCPE)...)
}
}
}
}
if len(versionRanges) == 0 {
return nil, nil, errors.New("no versions extracted from CPEs")
}

return versionRanges, cpes, nil
return strategies.ToVersionRangeType(s)
}

// compareSemverLike provides a custom comparison function for version strings that may not
Expand Down
Loading
Loading