Cybersecurity-Projects/PROJECTS/intermediate/sbom-generator-vulnerabilit.../internal/cli/vuln.go

190 lines
4.2 KiB
Go

// ©AngelaMos | 2026
// vuln.go
package cli
import (
"context"
"os"
"time"
"github.com/spf13/cobra"
"github.com/CarterPerez-dev/bomber/internal/graph"
"github.com/CarterPerez-dev/bomber/internal/parser"
"github.com/CarterPerez-dev/bomber/internal/report"
"github.com/CarterPerez-dev/bomber/internal/scanner"
"github.com/CarterPerez-dev/bomber/internal/ui"
"github.com/CarterPerez-dev/bomber/internal/vuln"
"github.com/CarterPerez-dev/bomber/pkg/types"
)
var vulnCmd = &cobra.Command{
Use: "vuln [path]",
Short: "Scan for known vulnerabilities",
Args: cobra.MaximumNArgs(1),
RunE: runVuln,
}
func runVuln(cmd *cobra.Command, args []string) error {
path := "."
if len(args) > 0 {
path = args[0]
}
reg := parser.NewRegistry()
parser.RegisterAll(reg)
s := scanner.New(reg)
result, err := s.Scan(path)
if err != nil {
return err
}
if len(result.Graphs) == 0 {
if formatFlag == "json" {
return report.WriteJSON(os.Stdout, result, nil, nil)
}
ui.PrintBanner()
report.PrintScanSummary(os.Stdout, result)
return nil
}
sp := ui.NewSpinner("Querying vulnerability databases...")
if formatFlag != "json" {
sp.Start()
}
vulnReport, err := queryVulns(cmd.Context(), result)
if formatFlag != "json" {
sp.Stop()
}
if err != nil {
return err
}
if formatFlag == "json" {
return report.WriteJSON(os.Stdout, result, vulnReport, nil)
}
ui.PrintBanner()
report.PrintScanSummary(os.Stdout, result)
report.PrintVulnReport(os.Stdout, vulnReport)
return nil
}
func queryVulns(ctx context.Context, result *types.ScanResult) (*types.VulnReport, error) {
allPkgs := make([]types.Package, 0, len(result.Graphs))
for _, g := range result.Graphs {
allPkgs = append(allPkgs, graph.AllPackages(g)...)
}
var cache *vuln.Cache
if !noCache {
c, err := vuln.NewCache(vuln.DefaultCachePath(), 24*time.Hour)
if err == nil {
cache = c
defer func() { _ = cache.Close() }()
}
}
osvClient := vuln.NewOSVClient()
var clients []vuln.Client
clients = append(clients, osvClient)
nvdKey := os.Getenv("BOMBER_NVD_API_KEY")
if nvdKey != "" {
clients = append(clients, vuln.NewNVDClient(vuln.WithNVDAPIKey(nvdKey)))
}
vulnReport := &types.VulnReport{
TotalPkgs: result.TotalPkgs,
DirectPkgs: result.DirectPkgs,
BySeverity: make(map[types.Severity]int),
}
for _, client := range clients {
var uncached []types.Package
for _, pkg := range allPkgs {
if cache != nil {
cached, ok, err := cache.Get(pkg.PURL, client.Source())
if err == nil && ok {
vulnReport.Matches = append(vulnReport.Matches, cached...)
for _, m := range cached {
vulnReport.BySeverity[m.Vulnerability.Severity]++
}
continue
}
}
uncached = append(uncached, pkg)
}
if len(uncached) == 0 {
continue
}
matches, err := client.Query(ctx, uncached)
if err != nil {
continue
}
matchesByPURL := make(map[string][]types.VulnMatch)
for _, m := range matches {
vulnReport.Matches = append(vulnReport.Matches, m)
vulnReport.BySeverity[m.Vulnerability.Severity]++
matchesByPURL[m.Package.PURL] = append(matchesByPURL[m.Package.PURL], m)
}
if cache != nil {
for purl, pkgMatches := range matchesByPURL {
_ = cache.Put(purl, client.Source(), pkgMatches)
}
}
}
vulnReport.Matches = deduplicateMatches(vulnReport.Matches)
vulnReport.BySeverity = make(map[types.Severity]int)
for _, m := range vulnReport.Matches {
vulnReport.BySeverity[m.Vulnerability.Severity]++
}
return vulnReport, nil
}
func deduplicateMatches(matches []types.VulnMatch) []types.VulnMatch {
seen := make(map[string]int)
var deduped []types.VulnMatch
for _, m := range matches {
ids := make([]string, 0, 1+len(m.Vulnerability.Aliases))
ids = append(ids, m.Vulnerability.ID)
ids = append(ids, m.Vulnerability.Aliases...)
existingIdx := -1
for _, id := range ids {
if idx, ok := seen[id]; ok {
existingIdx = idx
break
}
}
if existingIdx >= 0 {
existing := deduped[existingIdx]
if m.Vulnerability.Score > existing.Vulnerability.Score ||
(m.Vulnerability.FixVersion != "" && existing.Vulnerability.FixVersion == "") {
deduped[existingIdx] = m
}
continue
}
idx := len(deduped)
for _, id := range ids {
seen[id] = idx
}
deduped = append(deduped, m)
}
return deduped
}