190 lines
4.2 KiB
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
|
|
}
|