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
110 changes: 53 additions & 57 deletions affected.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,90 +6,51 @@ import (

// isAffectedVersion checks if a version is affected according to the Affected entry.
func isAffectedVersion(affected Affected, version string) bool {
// Check explicit versions list first
for _, v := range affected.Versions {
if v == version {
return true
}
}

// Check version ranges
for _, r := range affected.Ranges {
if r.Type != "SEMVER" && r.Type != "ECOSYSTEM" {
continue
}

inRange := false
for _, e := range r.Events {
if e.Introduced != "" {
// "0" means all versions from the beginning
if e.Introduced == "0" {
inRange = true
} else if vers.Compare(version, e.Introduced) >= 0 {
inRange = true
}
}
if e.Fixed != "" && inRange {
if vers.Compare(version, e.Fixed) >= 0 {
inRange = false
}
}
if e.LastAffected != "" && inRange {
if vers.Compare(version, e.LastAffected) > 0 {
inRange = false
}
}
}
if inRange {
if versionInRange(r.Events, version) {
return true
}
}

return false
}

func versionInRange(events []Event, version string) bool {
inRange := false
for _, e := range events {
if e.Introduced != "" {
inRange = e.Introduced == "0" || vers.Compare(version, e.Introduced) >= 0
}
if e.Fixed != "" && inRange && vers.Compare(version, e.Fixed) >= 0 {
inRange = false
}
if e.LastAffected != "" && inRange && vers.Compare(version, e.LastAffected) > 0 {
inRange = false
}
}
return inRange
}

// AffectedVersionRange returns a vers range string representing the affected versions.
// Events are processed sequentially, emitting a constraint for each
// introduced/fixed or introduced/lastAffected pair.
func AffectedVersionRange(affected Affected) string {
// If explicit versions are listed, return them
if len(affected.Versions) > 0 {
return versionsToRange(affected.Versions)
}

// Build range from events
var parts []string
for _, r := range affected.Ranges {
var introduced string
for _, e := range r.Events {
if e.Introduced != "" {
introduced = e.Introduced
}
if e.Fixed != "" && introduced != "" {
if introduced == "0" {
parts = append(parts, "<"+e.Fixed)
} else {
parts = append(parts, ">="+introduced+"|<"+e.Fixed)
}
introduced = ""
}
if e.LastAffected != "" && introduced != "" {
if introduced == "0" {
parts = append(parts, "<="+e.LastAffected)
} else {
parts = append(parts, ">="+introduced+"|<="+e.LastAffected)
}
introduced = ""
}
}
// Handle trailing introduced with no fix
if introduced != "" {
if introduced == "0" {
parts = append(parts, "*")
} else {
parts = append(parts, ">="+introduced)
}
}
parts = append(parts, rangeEventParts(r.Events)...)
}

if len(parts) == 0 {
Expand All @@ -103,6 +64,41 @@ func AffectedVersionRange(affected Affected) string {
return result
}

func rangeEventParts(events []Event) []string {
var parts []string
var introduced string
for _, e := range events {
if e.Introduced != "" {
introduced = e.Introduced
}
if e.Fixed != "" && introduced != "" {
parts = append(parts, formatRange(introduced, "<"+e.Fixed))
introduced = ""
}
if e.LastAffected != "" && introduced != "" {
parts = append(parts, formatRange(introduced, "<="+e.LastAffected))
introduced = ""
}
}
if introduced != "" {
parts = append(parts, formatRange(introduced, ""))
}
return parts
}

func formatRange(introduced, bound string) string {
if introduced == "0" {
if bound == "" {
return "*"
}
return bound
}
if bound == "" {
return ">=" + introduced
}
return ">=" + introduced + "|" + bound
}

func versionsToRange(versions []string) string {
if len(versions) == 0 {
return ""
Expand Down
47 changes: 30 additions & 17 deletions cvss.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ import (
gocvss40 "github.com/pandatix/go-cvss/40"
)

const (
LevelCritical = "critical"
LevelHigh = "high"
LevelMedium = "medium"
LevelLow = "low"
LevelNone = "none"

thresholdCritical = 9.0
thresholdHigh = 7.0
thresholdMedium = 4.0
thresholdLow = 0.1
)

// CVSS holds parsed CVSS information.
type CVSS struct {
Version string
Expand Down Expand Up @@ -114,33 +127,33 @@ func parseCVSS40(vector string) (*CVSS, error) {
// scoreToLevel converts a CVSS v2/v3 score to a severity level.
func scoreToLevel(score float64) string {
switch {
case score >= 9.0:
return "critical"
case score >= 7.0:
return "high"
case score >= 4.0:
return "medium"
case score >= thresholdCritical:
return LevelCritical
case score >= thresholdHigh:
return LevelHigh
case score >= thresholdMedium:
return LevelMedium
case score > 0:
return "low"
return LevelLow
default:
return "none"
return LevelNone
}
}

// scoreToLevel40 converts a CVSS v4.0 score to a severity level.
// CVSS 4.0 uses different thresholds.
func scoreToLevel40(score float64) string {
switch {
case score >= 9.0:
return "critical"
case score >= 7.0:
return "high"
case score >= 4.0:
return "medium"
case score >= 0.1:
return "low"
case score >= thresholdCritical:
return LevelCritical
case score >= thresholdHigh:
return LevelHigh
case score >= thresholdMedium:
return LevelMedium
case score >= thresholdLow:
return LevelLow
default:
return "none"
return LevelNone
}
}

Expand Down
102 changes: 53 additions & 49 deletions depsdev/depsdev.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ func (s *Source) QueryBatch(ctx context.Context, purls []*purl.PURL) ([][]vulns.
return nil, nil
}

// deps.dev batch endpoint supports up to 5000 requests
const batchSize = 5000
results := make([][]vulns.Vulnerability, len(purls))

Expand All @@ -126,65 +125,70 @@ func (s *Source) QueryBatch(ctx context.Context, purls []*purl.PURL) ([][]vulns.
if end > len(purls) {
end = len(purls)
}
batch := purls[i:end]

// Build batch request
var requests []batchRequest
for _, p := range batch {
requests = append(requests, batchRequest{
Purl: p.String(),
})
}

reqBody, err := json.Marshal(batchQueryRequest{Requests: requests})
batchResp, err := s.fetchBatch(ctx, purls[i:end])
if err != nil {
return nil, fmt.Errorf("marshaling batch request: %w", err)
return nil, err
}

httpReq, err := http.NewRequestWithContext(ctx, "POST", s.baseURL+"/purlbatch", strings.NewReader(string(reqBody)))
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
for j, r := range batchResp.Responses {
results[i+j] = s.resolveAdvisories(ctx, r)
}
httpReq.Header.Set("Content-Type", "application/json")
}

resp, err := s.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("executing request: %w", err)
}
return results, nil
}

if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
_ = resp.Body.Close()
return nil, fmt.Errorf("batch query failed with status %d: %s", resp.StatusCode, string(respBody))
}
func (s *Source) fetchBatch(ctx context.Context, purls []*purl.PURL) (*batchQueryResponse, error) {
var requests []batchRequest
for _, p := range purls {
requests = append(requests, batchRequest{Purl: p.String()})
}

var batchResp batchQueryResponse
if err := json.NewDecoder(resp.Body).Decode(&batchResp); err != nil {
_ = resp.Body.Close()
return nil, fmt.Errorf("decoding response: %w", err)
}
_ = resp.Body.Close()
reqBody, err := json.Marshal(batchQueryRequest{Requests: requests})
if err != nil {
return nil, fmt.Errorf("marshaling batch request: %w", err)
}

// Process batch results
for j, r := range batchResp.Responses {
if r.Version == nil {
continue
}
var vulnList []vulns.Vulnerability
for _, key := range r.Version.AdvisoryKeys {
adv, err := s.getAdvisory(ctx, key.ID)
if err != nil {
continue
}
if adv != nil {
vulnList = append(vulnList, *adv)
}
}
results[i+j] = vulnList
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", s.baseURL+"/purlbatch", strings.NewReader(string(reqBody)))
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")

return results, nil
resp, err := s.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("executing request: %w", err)
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("batch query failed with status %d: %s", resp.StatusCode, string(respBody))
}

var batchResp batchQueryResponse
if err := json.NewDecoder(resp.Body).Decode(&batchResp); err != nil {
return nil, fmt.Errorf("decoding response: %w", err)
}
return &batchResp, nil
}

func (s *Source) resolveAdvisories(ctx context.Context, r batchResponse) []vulns.Vulnerability {
if r.Version == nil {
return nil
}
var result []vulns.Vulnerability
for _, key := range r.Version.AdvisoryKeys {
adv, err := s.getAdvisory(ctx, key.ID)
if err != nil {
continue
}
if adv != nil {
result = append(result, *adv)
}
}
return result
}

// Get fetches a specific vulnerability by ID (OSV ID).
Expand Down
Loading