added artifacts
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
package vulnscan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/forgeo/forgebucket/internal/events"
|
||||
"github.com/forgeo/forgebucket/internal/models"
|
||||
)
|
||||
|
||||
// Scanner watches for SBOM generation events and queries OSV for vulns.
|
||||
type Scanner struct {
|
||||
db *xorm.Engine
|
||||
bus events.EventBus
|
||||
client *Client
|
||||
}
|
||||
|
||||
func NewScanner(db *xorm.Engine, bus events.EventBus) *Scanner {
|
||||
return &Scanner{
|
||||
db: db,
|
||||
bus: bus,
|
||||
client: NewClient(),
|
||||
}
|
||||
}
|
||||
|
||||
// Start subscribes to SBOM-related events and scans for vulnerabilities.
|
||||
func (s *Scanner) Start(ctx context.Context) {
|
||||
// Listen for SBOM Report created events (sync trigger).
|
||||
// In practice this is called on-demand via the API, so Start is minimal.
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
// ScanByPURL queries OSV for a single package and stores findings.
|
||||
func (s *Scanner) ScanByPURL(repoID int64, purl, version string) ([]models.VulnerabilityFinding, error) {
|
||||
vulns, err := s.client.QueryByPURL(purl, version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.persistFindings(repoID, purl, version, vulns), nil
|
||||
}
|
||||
|
||||
// ScanSBOM reads the latest SBOM report for a repo, queries OSV for every
|
||||
// component, and stores the findings. Returns the new findings.
|
||||
func (s *Scanner) ScanSBOM(repoID int64) ([]models.VulnerabilityFinding, error) {
|
||||
var report models.SBOMReport
|
||||
found, err := s.db.Where("repo_id = ?", repoID).
|
||||
OrderBy("generated_at DESC").Get(&report)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("no SBOM found for repo %d", repoID)
|
||||
}
|
||||
|
||||
var doc struct {
|
||||
Components []struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
PURL string `json:"purl"`
|
||||
} `json:"components"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(report.BOMDocument), &doc); err != nil {
|
||||
return nil, fmt.Errorf("parse SBOM: %w", err)
|
||||
}
|
||||
|
||||
var allFindings []models.VulnerabilityFinding
|
||||
for _, comp := range doc.Components {
|
||||
if comp.PURL == "" || comp.Version == "" {
|
||||
continue
|
||||
}
|
||||
vulns, err := s.client.QueryByPURL(comp.PURL, comp.Version)
|
||||
if err != nil {
|
||||
log.Printf("vulnscan: query %s@%s: %v", comp.PURL, comp.Version, err)
|
||||
continue
|
||||
}
|
||||
findings := s.persistFindings(repoID, comp.PURL, comp.Version, vulns)
|
||||
allFindings = append(allFindings, findings...)
|
||||
}
|
||||
return allFindings, nil
|
||||
}
|
||||
|
||||
// ListFindings returns unfixed vulnerability findings for a repo.
|
||||
func (s *Scanner) ListFindings(repoID int64) ([]models.VulnerabilityFinding, error) {
|
||||
var findings []models.VulnerabilityFinding
|
||||
if err := s.db.Where("repo_id = ? AND dismissed = ?", repoID, false).
|
||||
OrderBy("cvss_score DESC, detected_at DESC").Find(&findings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if findings == nil {
|
||||
findings = []models.VulnerabilityFinding{}
|
||||
}
|
||||
return findings, nil
|
||||
}
|
||||
|
||||
// DismissFindings acknowledges a vulnerability finding.
|
||||
func (s *Scanner) DismissFindings(findingID int64, dismissedBy string) error {
|
||||
now := time.Now().UTC()
|
||||
affected, err := s.db.ID(findingID).Cols("dismissed", "dismissed_by", "dismissed_at").
|
||||
Update(&models.VulnerabilityFinding{
|
||||
Dismissed: true,
|
||||
DismissedBy: dismissedBy,
|
||||
DismissedAt: &now,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if affected == 0 {
|
||||
return fmt.Errorf("finding %d not found", findingID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scanner) persistFindings(repoID int64, purl, version string, vulns []OSVVuln) []models.VulnerabilityFinding {var findings []models.VulnerabilityFinding
|
||||
for _, v := range vulns {
|
||||
// Check for duplicate before inserting.
|
||||
existing := &models.VulnerabilityFinding{}
|
||||
if has, _ := s.db.Where("vuln_id = ? AND purl = ? AND repo_id = ?", v.ID, purl, repoID).Get(existing); has {
|
||||
continue
|
||||
}
|
||||
|
||||
cvssScore := parseCVSS(v)
|
||||
|
||||
finding := &models.VulnerabilityFinding{
|
||||
RepoID: repoID,
|
||||
VulnID: v.ID,
|
||||
PURL: purl,
|
||||
Version: version,
|
||||
Summary: truncateStr(v.Summary, 300),
|
||||
Details: v.Details,
|
||||
CVSSScore: cvssScore,
|
||||
FixedVersion: extractFixedVersion(v),
|
||||
DetectedAt: time.Now().UTC(),
|
||||
}
|
||||
if _, err := s.db.Insert(finding); err != nil {
|
||||
log.Printf("vulnscan: insert finding %s for %s: %v", v.ID, purl, err)
|
||||
continue
|
||||
}
|
||||
findings = append(findings, *finding)
|
||||
}
|
||||
return findings
|
||||
}
|
||||
|
||||
// parseCVSS extracts the CVSS score from OSV severity info.
|
||||
func parseCVSS(v OSVVuln) float64 {
|
||||
for _, sev := range v.Severity {
|
||||
if sev.Type == "CVSS_V3" || sev.Type == "CVSS_V2" {
|
||||
var score float64
|
||||
fmt.Sscanf(sev.Score, "%f", &score)
|
||||
return score
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// extractFixedVersion tries to extract the fixed version from affected ranges.
|
||||
func extractFixedVersion(v OSVVuln) string {
|
||||
for _, a := range v.Affected {
|
||||
for _, r := range a.Ranges {
|
||||
for _, e := range r.Events {
|
||||
if e.Fixed != "" {
|
||||
return e.Fixed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func truncateStr(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n] + "..."
|
||||
}
|
||||
Reference in New Issue
Block a user