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
149 changes: 120 additions & 29 deletions vulnfeeds/conversion/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func AddAffected(v *vulns.Vulnerability, aff *osvschema.Affected, metrics *model
DatabaseSpecific: aff.GetDatabaseSpecific(),
}
v.Affected = append(v.Affected, newAff)
metrics.ResolvedRangesCount += len(uniqueRanges)
}
}

Expand Down Expand Up @@ -187,7 +188,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 +210,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,23 +219,43 @@ 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
}

resolvedVersions := make(map[string]string)
resolveVersion := func(ver string) string {
if ver == "" {
return ""
}
if ver == "0" {
return "0"
}
if commit, seen := resolvedVersions[ver]; seen {
return commit
}
commit, err := git.VersionToCommit(ver, normalizedTags)
if err != nil {
metrics.AddNotef("error resolving version to commit - %s - %s", ver, err)
}
resolvedVersions[ver] = commit

return commit
}

var stillUnresolvedRanges []models.RangeWithMetadata
for _, vr := range unresolvedRanges {
vRepo := vr.Range.GetRepo()
if vRepo != "" {
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 All @@ -258,23 +279,9 @@ func GitVersionsToCommits(versionRanges []models.RangeWithMetadata, repos []stri
}
}

var introducedCommit string
if introduced == "0" {
introducedCommit = "0"
} else {
introducedCommit, err = git.VersionToCommit(introduced, normalizedTags)
if err != nil {
metrics.AddNotef("error resolving version to commit - %s - %s", introduced, err)
}
}
fixedCommit, err := git.VersionToCommit(fixed, normalizedTags)
if err != nil {
metrics.AddNotef("error resolving version to commit - %s - %s", fixed, err)
}
lastAffectedCommit, err := git.VersionToCommit(lastAffected, normalizedTags)
if err != nil {
metrics.AddNotef("error resolving version to commit - %s - %s", lastAffected, err)
}
introducedCommit := resolveVersion(introduced)
fixedCommit := resolveVersion(fixed)
lastAffectedCommit := resolveVersion(lastAffected)

if fixedCommit != "" || lastAffectedCommit != "" {
var newVR *osvschema.Range
Expand Down Expand Up @@ -727,21 +734,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 +849,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"
}
---
Loading
Loading