package security import ( "bytes" "encoding/json" "fmt" "io" "net/http" "strings" "github.com/user/package-review/internal/inventory" ) // osvQuery represents a query to OSV.dev type osvQuery struct { Package struct { Name string `json:"name"` Ecosystem string `json:"ecosystem"` } `json:"package"` Version string `json:"version"` } // osvResponse represents a response from OSV.dev type osvResponse struct { Vulns []struct { ID string `json:"id"` Published string `json:"published"` Summary string `json:"summary"` Severity []struct { Type string `json:"type"` Score float64 `json:"score"` } `json:"severity"` References []struct { Type string `json:"type"` URL string `json:"url"` } `json:"references"` } `json:"vulns"` } // queryOSV queries OSV.dev for vulnerabilities in a package func queryOSV(pkg inventory.Package) ([]Finding, error) { var findings []Finding // Determine ecosystem ecosystem := "" switch pkg.Source { case "homebrew": ecosystem = "GIT" // Homebrew uses Git ecosystem // For Git ecosystem, we'd need the repo URL, which we don't have // Skip for now in MVP return findings, nil case "pip": ecosystem = "PyPI" case "npm": ecosystem = "npm" case "go": ecosystem = "Go" case "application": // Applications are harder to match return findings, nil default: return findings, nil } // Construct query query := osvQuery{} query.Package.Name = pkg.Name query.Package.Ecosystem = ecosystem query.Version = pkg.Version queryJSON, err := json.Marshal(query) if err != nil { return findings, fmt.Errorf("marshal query: %w", err) } // Query OSV.dev resp, err := http.Post( "https://api.osv.dev/v1/query", "application/json", bytes.NewReader(queryJSON), ) if err != nil { return findings, fmt.Errorf("osv request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return findings, fmt.Errorf("read response: %w", err) } if resp.StatusCode != 200 { return findings, fmt.Errorf("osv returned %d: %s", resp.StatusCode, string(body)) } var osvResp osvResponse if err := json.Unmarshal(body, &osvResp); err != nil { return findings, fmt.Errorf("parse response: %w", err) } // Convert to findings for _, vuln := range osvResp.Vulns { cvss := 0.0 if len(vuln.Severity) > 0 { // Use CVSS v3.1 score if available for _, sev := range vuln.Severity { if strings.HasPrefix(sev.Type, "CVSS_V3") { cvss = sev.Score break } } } severity := cvssToSeverity(cvss) link := "" for _, ref := range vuln.References { if ref.Type == "ADVISORY" || ref.Type == "WEB" { link = ref.URL break } } findings = append(findings, Finding{ PackageName: pkg.Name, PackageVersion: pkg.Version, CVE: vuln.ID, CVSS: cvss, Summary: vuln.Summary, Link: link, Severity: severity, }) } return findings, nil } // cvssToSeverity converts CVSS score to severity level func cvssToSeverity(cvss float64) string { switch { case cvss >= 9.0: return "CRITICAL" case cvss >= 7.0: return "HIGH" case cvss >= 4.0: return "MEDIUM" case cvss > 0: return "LOW" default: return "UNKNOWN" } }