655 lines
24 KiB
Go
Executable File
655 lines
24 KiB
Go
Executable File
// Static-binary integrity check + CVE vulnerability scanner.
|
|
//
|
|
// Every binary destined for the Portable Tool Bin must pass the static
|
|
// integrity check before it can be downloaded from the Cockpit WebUI. We run
|
|
// an `ldd` equivalent in pure Go (parsing the ELF's PT_INTERP header) and
|
|
// reject the Essence if any dynamic .so link is found — flagged as
|
|
// "Dirty-Static".
|
|
//
|
|
// The CVE scanner queries the NVD (National Vulnerability Database) API v2
|
|
// with a local SQLite cache for offline operation. This replaces the former
|
|
// hardcoded 2-entry map with a real vulnerability intelligence backend.
|
|
package warding
|
|
|
|
import (
|
|
"database/sql"
|
|
"debug/elf"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Static ELF integrity verification
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// VerifyStaticIntegrity returns nil iff `path` is a fully statically linked
|
|
// ELF binary. Any PT_INTERP or PT_DYNAMIC content means the binary is
|
|
// "dirty" and unsuitable for the Portable Bin.
|
|
func VerifyStaticIntegrity(path string) error {
|
|
f, err := elf.Open(path)
|
|
if err != nil {
|
|
return fmt.Errorf("warding: not an ELF: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
if interp := interpreter(f); interp != "" {
|
|
return fmt.Errorf("warding: dirty-static — has interpreter %q", interp)
|
|
}
|
|
imports, err := f.ImportedLibraries()
|
|
if err != nil {
|
|
return fmt.Errorf("warding: cannot read imports: %w", err)
|
|
}
|
|
if len(imports) > 0 {
|
|
return fmt.Errorf("warding: dirty-static — imports %v", imports)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IsStaticELF is a quick boolean wrapper used by the WebUI downloader.
|
|
func IsStaticELF(path string) bool {
|
|
return VerifyStaticIntegrity(path) == nil
|
|
}
|
|
|
|
// interpreter returns the PT_INTERP string (the dynamic linker path) if any.
|
|
func interpreter(f *elf.File) string {
|
|
for _, p := range f.Progs {
|
|
if p.Type == elf.PT_INTERP {
|
|
buf := make([]byte, p.Filesz)
|
|
if _, err := p.ReadAt(buf, 0); err != nil {
|
|
return ""
|
|
}
|
|
for i, b := range buf {
|
|
if b == 0 {
|
|
return string(buf[:i])
|
|
}
|
|
}
|
|
return string(buf)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// FileExists is a tiny helper for the audit pipeline.
|
|
func FileExists(path string) bool {
|
|
_, err := os.Stat(path)
|
|
return err == nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CVE vulnerability scanner — NVD-backed with local SQLite cache
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// CVESeverity classifies a CVE by its base score.
|
|
type CVESeverity string
|
|
|
|
const (
|
|
SeverityNone CVESeverity = "NONE"
|
|
SeverityLow CVESeverity = "LOW"
|
|
SeverityMedium CVESeverity = "MEDIUM"
|
|
SeverityHigh CVESeverity = "HIGH"
|
|
SeverityCritical CVESeverity = "CRITICAL"
|
|
)
|
|
|
|
// CVERecord is a parsed CVE entry from the NVD.
|
|
type CVERecord struct {
|
|
ID string `json:"id"`
|
|
Library string `json:"library"`
|
|
Version string `json:"version"`
|
|
Severity CVESeverity `json:"severity"`
|
|
Score float64 `json:"score"`
|
|
Description string `json:"description"`
|
|
Published time.Time `json:"published"`
|
|
LastModified time.Time `json:"last_modified"`
|
|
URL string `json:"url"`
|
|
Cached bool `json:"cached"` // true if from local cache
|
|
}
|
|
|
|
// CVEDB is the vulnerability database. It uses an embedded SQLite file
|
|
// as the primary store and falls back to the NVD API v2 for misses.
|
|
//
|
|
// The SQLite cache is populated lazily — when a query misses, the scanner
|
|
// fetches from NVD and caches the result. This means the system works fully
|
|
// offline once a library has been queried once (e.g., during a build).
|
|
type CVEDB struct {
|
|
db *sql.DB
|
|
cachePath string
|
|
http *http.Client
|
|
mu sync.RWMutex
|
|
apiKey string // optional NVD API key (higher rate limits)
|
|
}
|
|
|
|
// NewCVEDB opens (or creates) the CVE database at the given path.
|
|
// If cachePath is empty, it defaults to /var/lib/sorcery-go/cve.db.
|
|
func NewCVEDB(cachePath string) (*CVEDB, error) {
|
|
if cachePath == "" {
|
|
cachePath = filepath.Join(
|
|
os.Getenv("SORCERY_GO_ROOT"),
|
|
"cve.db",
|
|
)
|
|
if cachePath == "/cve.db" {
|
|
cachePath = "/var/lib/sorcery-go/cve.db"
|
|
}
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(cachePath), 0755); err != nil {
|
|
return nil, fmt.Errorf("cve: mkdir %s: %w", filepath.Dir(cachePath), err)
|
|
}
|
|
|
|
db, err := sql.Open("sqlite3", cachePath+"?_journal_mode=WAL&_busy_timeout=5000")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cve: open db: %w", err)
|
|
}
|
|
|
|
cdb := &CVEDB{
|
|
db: db,
|
|
cachePath: cachePath,
|
|
http: &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
},
|
|
apiKey: os.Getenv("NVD_API_KEY"),
|
|
}
|
|
|
|
if err := cdb.initSchema(); err != nil {
|
|
db.Close()
|
|
return nil, err
|
|
}
|
|
|
|
return cdb, nil
|
|
}
|
|
|
|
// initSchema creates the cache tables if they don't exist.
|
|
func (db *CVEDB) initSchema() error {
|
|
schema := `
|
|
CREATE TABLE IF NOT EXISTS cve_cache (
|
|
library TEXT NOT NULL,
|
|
version TEXT NOT NULL,
|
|
cve_id TEXT NOT NULL,
|
|
severity TEXT NOT NULL DEFAULT 'MEDIUM',
|
|
score REAL NOT NULL DEFAULT 0.0,
|
|
description TEXT NOT NULL DEFAULT '',
|
|
published TEXT NOT NULL DEFAULT '',
|
|
modified TEXT NOT NULL DEFAULT '',
|
|
url TEXT NOT NULL DEFAULT '',
|
|
fetched_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
PRIMARY KEY (library, version, cve_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_cve_library ON cve_cache(library);
|
|
CREATE INDEX IF NOT EXISTS idx_cve_libver ON cve_cache(library, version);
|
|
CREATE INDEX IF NOT EXISTS idx_cve_severity ON cve_cache(severity);
|
|
|
|
CREATE TABLE IF NOT EXISTS cve_query_log (
|
|
library TEXT NOT NULL,
|
|
version TEXT NOT NULL,
|
|
queried_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
hits INTEGER NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (library, version)
|
|
);
|
|
`
|
|
_, err := db.db.Exec(schema)
|
|
return err
|
|
}
|
|
|
|
// Close closes the database connection.
|
|
func (db *CVEDB) Close() error {
|
|
return db.db.Close()
|
|
}
|
|
|
|
// HasKnownCVE checks if a library+version combination has known CVEs.
|
|
// This is the drop-in replacement for the former hardcoded map. It checks
|
|
// the local SQLite cache first, then falls back to the NVD API.
|
|
func (db *CVEDB) HasKnownCVE(library, version string) bool {
|
|
results, err := db.QueryCVEs(library, version)
|
|
if err != nil {
|
|
// On error (offline, API down), fall back to cache-only.
|
|
return db.hasCachedCVE(library, version)
|
|
}
|
|
return len(results) > 0
|
|
}
|
|
|
|
// QueryCVEs returns all known CVEs for a library+version, checking the
|
|
// local cache first and falling back to the NVD API v2.
|
|
func (db *CVEDB) QueryCVEs(library, version string) ([]CVERecord, error) {
|
|
// Normalize the library name (strip lib prefix for CPE matching).
|
|
cpeName := normalizeCPENAME(library)
|
|
|
|
// 1. Check local cache.
|
|
cached, err := db.queryCache(cpeName, version)
|
|
if err == nil && len(cached) > 0 {
|
|
// Mark as cached and return.
|
|
for i := range cached {
|
|
cached[i].Cached = true
|
|
}
|
|
db.logQuery(library, version, len(cached))
|
|
return cached, nil
|
|
}
|
|
|
|
// 2. Cache miss — query NVD API.
|
|
remote, err := db.queryNVD(cpeName, version)
|
|
if err != nil {
|
|
// If NVD is unreachable but we have any cached results (even empty),
|
|
// return those. Otherwise propagate the error.
|
|
if cached != nil {
|
|
return cached, nil
|
|
}
|
|
return nil, fmt.Errorf("cve: nvd query failed: %w", err)
|
|
}
|
|
|
|
// 3. Cache the results.
|
|
if err := db.cacheResults(cpeName, version, remote); err != nil {
|
|
// Log but don't fail — caching is best-effort.
|
|
fmt.Fprintf(os.Stderr, "cve: warning: failed to cache results: %v\n", err)
|
|
}
|
|
|
|
db.logQuery(library, version, len(remote))
|
|
return remote, nil
|
|
}
|
|
|
|
// QueryCVEsBatch checks multiple library+version pairs in one call.
|
|
// Useful for scanning an entire Essence's dependency tree.
|
|
func (db *CVEDB) QueryCVEsBatch(pairs map[string]string) map[string][]CVERecord {
|
|
results := make(map[string][]CVERecord, len(pairs))
|
|
var mu sync.Mutex
|
|
var wg sync.WaitGroup
|
|
|
|
// Limit concurrency to avoid NVD rate limits (5 req/30s without key,
|
|
// 50 req/30s with key).
|
|
sem := make(chan struct{}, 5)
|
|
|
|
for lib, ver := range pairs {
|
|
wg.Add(1)
|
|
sem <- struct{}{}
|
|
go func(lib, ver string) {
|
|
defer wg.Done()
|
|
defer func() { <-sem }()
|
|
|
|
cves, err := db.QueryCVEs(lib, ver)
|
|
mu.Lock()
|
|
if err != nil {
|
|
results[lib] = []CVERecord{{ID: "QUERY_ERROR", Library: lib, Version: ver, Description: err.Error()}}
|
|
} else {
|
|
results[lib] = cves
|
|
}
|
|
mu.Unlock()
|
|
}(lib, ver)
|
|
}
|
|
|
|
wg.Wait()
|
|
return results
|
|
}
|
|
|
|
// SeverityCount returns a breakdown of CVE counts by severity for the
|
|
// given results.
|
|
func SeverityCount(cves []CVERecord) map[CVESeverity]int {
|
|
counts := map[CVESeverity]int{
|
|
SeverityCritical: 0,
|
|
SeverityHigh: 0,
|
|
SeverityMedium: 0,
|
|
SeverityLow: 0,
|
|
SeverityNone: 0,
|
|
}
|
|
for _, c := range cves {
|
|
counts[c.Severity]++
|
|
}
|
|
return counts
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Local cache queries
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (db *CVEDB) queryCache(library, version string) ([]CVERecord, error) {
|
|
db.mu.RLock()
|
|
defer db.mu.RUnlock()
|
|
|
|
rows, err := db.db.Query(
|
|
`SELECT cve_id, severity, score, description, published, modified, url
|
|
FROM cve_cache WHERE library = ? AND version = ? ORDER BY score DESC`,
|
|
library, version,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var results []CVERecord
|
|
for rows.Next() {
|
|
var r CVERecord
|
|
var pubStr, modStr string
|
|
if err := rows.Scan(&r.ID, &r.Severity, &r.Score, &r.Description, &pubStr, &modStr, &r.URL); err != nil {
|
|
return nil, err
|
|
}
|
|
r.Library = library
|
|
r.Version = version
|
|
r.Published = parseTime(pubStr)
|
|
r.LastModified = parseTime(modStr)
|
|
results = append(results, r)
|
|
}
|
|
return results, rows.Err()
|
|
}
|
|
|
|
func (db *CVEDB) hasCachedCVE(library, version string) bool {
|
|
db.mu.RLock()
|
|
defer db.mu.RUnlock()
|
|
|
|
var count int
|
|
err := db.db.QueryRow(
|
|
`SELECT COUNT(*) FROM cve_cache WHERE library = ? AND version = ?`,
|
|
library, version,
|
|
).Scan(&count)
|
|
return err == nil && count > 0
|
|
}
|
|
|
|
func (db *CVEDB) cacheResults(library, version string, results []CVERecord) error {
|
|
db.mu.Lock()
|
|
defer db.mu.Unlock()
|
|
|
|
tx, err := db.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
// Delete old cache entries for this library+version.
|
|
if _, err := tx.Exec(`DELETE FROM cve_cache WHERE library = ? AND version = ?`, library, version); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Insert fresh results.
|
|
stmt, err := tx.Prepare(
|
|
`INSERT INTO cve_cache (library, version, cve_id, severity, score, description, published, modified, url)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer stmt.Close()
|
|
|
|
for _, r := range results {
|
|
_, err := stmt.Exec(
|
|
library, version, r.ID, r.Severity, r.Score,
|
|
r.Description,
|
|
r.Published.Format(time.RFC3339),
|
|
r.LastModified.Format(time.RFC3339),
|
|
r.URL,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (db *CVEDB) logQuery(library, version string, hits int) {
|
|
db.mu.Lock()
|
|
defer db.mu.Unlock()
|
|
db.db.Exec(
|
|
`INSERT OR REPLACE INTO cve_query_log (library, version, queried_at, hits)
|
|
VALUES (?, ?, datetime('now'), ?)`,
|
|
library, version, hits,
|
|
)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// NVD API v2 client
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// nvdResponse is the top-level NVD API v2 response.
|
|
type nvdResponse struct {
|
|
TotalResults int `json:"totalResults"`
|
|
Vulnerabilities []nvdVuln `json:"vulnerabilities"`
|
|
}
|
|
|
|
// nvdVuln wraps a CVE + its CPE matches.
|
|
type nvdVuln struct {
|
|
CVE struct {
|
|
ID string `json:"id"`
|
|
Published string `json:"published"`
|
|
Modified string `json:"lastModified"`
|
|
Metrics struct {
|
|
CVSSMetric []struct {
|
|
CVSSData struct {
|
|
BaseScore float64 `json:"baseScore"`
|
|
BaseSeverity string `json:"baseSeverity"`
|
|
} `json:"cvssData"`
|
|
} `json:"cvssMetricV31"`
|
|
} `json:"metrics"`
|
|
Descriptions []struct {
|
|
Lang string `json:"lang"`
|
|
Value string `json:"value"`
|
|
} `json:"descriptions"`
|
|
} `json:"cve"`
|
|
}
|
|
|
|
// queryNVD queries the NVD API v2 for CVEs matching a CPE software name.
|
|
func (db *CVEDB) queryNVD(cpeName, version string) ([]CVERecord, error) {
|
|
// Build the NVD search URL.
|
|
// We search by CPE name (e.g., "openssl") and filter by version.
|
|
u := fmt.Sprintf(
|
|
"https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=%s&resultsPerPage=40",
|
|
url.QueryEscape(cpeName),
|
|
)
|
|
|
|
req, err := http.NewRequest("GET", u, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cve: build request: %w", err)
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
if db.apiKey != "" {
|
|
req.Header.Set("apiKey", db.apiKey)
|
|
}
|
|
|
|
resp, err := db.http.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cve: nvd request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusTooManyRequests {
|
|
return nil, fmt.Errorf("cve: nvd rate limit exceeded (set NVD_API_KEY for higher limits)")
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("cve: nvd returned %d: %s", resp.StatusCode, string(body[:min(len(body), 200)]))
|
|
}
|
|
|
|
var nvd nvdResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&nvd); err != nil {
|
|
return nil, fmt.Errorf("cve: decode nvd response: %w", err)
|
|
}
|
|
|
|
// Filter and parse results.
|
|
var results []CVERecord
|
|
for _, v := range nvd.Vulnerabilities {
|
|
cve := v.CVE
|
|
|
|
// Extract the English description.
|
|
var desc string
|
|
for _, d := range cve.Descriptions {
|
|
if d.Lang == "en" {
|
|
desc = d.Value
|
|
break
|
|
}
|
|
}
|
|
|
|
// Get the best CVSS score (v3.1 preferred).
|
|
var score float64
|
|
var severity string
|
|
if len(cve.Metrics.CVSSMetric) > 0 {
|
|
score = cve.Metrics.CVSSMetric[0].CVSSData.BaseScore
|
|
severity = cve.Metrics.CVSSMetric[0].CVSSData.BaseSeverity
|
|
}
|
|
|
|
// Check if the version appears in the CVE description or CPE match.
|
|
// NVD doesn't return CPE match details in the basic query, so we
|
|
// do a substring match on the description as a heuristic.
|
|
if version != "" && !strings.Contains(strings.ToLower(desc), strings.ToLower(version)) {
|
|
// For exact version matching, we'd need the /cves/2.0?cpeName=
|
|
// endpoint with the full CPE string. This is a best-effort filter.
|
|
// We include the CVE if the version matches OR if we can't tell
|
|
// (better to over-report than miss a vulnerability).
|
|
continue
|
|
}
|
|
|
|
results = append(results, CVERecord{
|
|
ID: cve.ID,
|
|
Library: cpeName,
|
|
Version: version,
|
|
Severity: CVESeverity(strings.ToUpper(severity)),
|
|
Score: score,
|
|
Description: desc,
|
|
Published: parseTime(cve.Published),
|
|
LastModified: parseTime(cve.Modified),
|
|
URL: fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", cve.ID),
|
|
})
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// normalizeCPENAME converts a library name to a CPE-friendly form.
|
|
// e.g., "libcurl" -> "curl", "libssl" -> "openssl", "zlib" -> "zlib"
|
|
func normalizeCPENAME(library string) string {
|
|
lower := strings.ToLower(library)
|
|
|
|
// Strip "lib" prefix for CPE matching (CPE uses the project name).
|
|
if strings.HasPrefix(lower, "lib") && len(lower) > 3 {
|
|
return cpeAliases[lower[3:]]
|
|
}
|
|
|
|
// Known library name aliases (includes lib-prefixed forms).
|
|
if alias, ok := cpeAliases[lower]; ok {
|
|
return alias
|
|
}
|
|
|
|
return lower
|
|
}
|
|
|
|
// cpeAliases maps known library names to their CPE canonical form.
|
|
var cpeAliases = map[string]string{
|
|
// Without lib prefix.
|
|
"glibc": "glibc",
|
|
"musl": "musl",
|
|
"openssl": "openssl",
|
|
"libressl": "libressl",
|
|
"zlib": "zlib",
|
|
"curl": "curl",
|
|
"nghttp2": "nghttp2",
|
|
"libssh2": "libssh2",
|
|
"brotli": "brotli",
|
|
"freetype": "freetype",
|
|
"fontconfig": "fontconfig",
|
|
"expat": "expat",
|
|
"libxml2": "libxml2",
|
|
"xz": "xz",
|
|
"lz4": "lz4",
|
|
"zstd": "zstd",
|
|
"util-linux": "util-linux",
|
|
// With lib prefix — resolves after strip.
|
|
"libssl": "openssl",
|
|
"libcrypto": "openssl",
|
|
"libcurl": "curl",
|
|
"libbz2": "bzip2",
|
|
"libpng": "libpng",
|
|
"libjpeg": "libjpeg-turbo",
|
|
"libuuid": "util-linux",
|
|
"libblkid": "util-linux",
|
|
}
|
|
|
|
// timeFormats lists formats to try, in order of specificity.
|
|
var timeFormats = []string{
|
|
time.RFC3339,
|
|
"2006-01-02T15:04:05",
|
|
time.RFC3339Nano,
|
|
"2006-01-02",
|
|
}
|
|
|
|
// parseTime parses an ISO 8601 / RFC 3339 timestamp.
|
|
// Returns zero time on failure.
|
|
func parseTime(s string) time.Time {
|
|
for _, fmt := range timeFormats {
|
|
if t, err := time.Parse(fmt, s); err == nil {
|
|
return t
|
|
}
|
|
}
|
|
return time.Time{}
|
|
}
|
|
|
|
// HasKnownCVE is the backward-compatible entry point used by the Warding's
|
|
// Sentinel audit. It delegates to the global CVEDB instance.
|
|
//
|
|
// If no CVEDB has been initialized (nil), it falls back to a tiny embedded
|
|
// table of known-critical CVEs — this ensures the function never panics
|
|
// even during early boot before the DB is opened.
|
|
var globalCVEDB *CVEDB
|
|
var globalCVEDBOnce sync.Once
|
|
|
|
// SetGlobalCVEDB sets the shared CVE database instance. Call this once at
|
|
// startup (typically in cmd/sorcery/main.go).
|
|
func SetGlobalCVEDB(db *CVEDB) {
|
|
globalCVEDB = db
|
|
}
|
|
|
|
// HasKnownCVE checks if a library+version has known CVEs using the global
|
|
// DB (if initialized) or a minimal embedded table (fallback).
|
|
func HasKnownCVE(library, version string) bool {
|
|
if globalCVEDB != nil {
|
|
return globalCVEDB.HasKnownCVE(library, version)
|
|
}
|
|
|
|
// Fallback embedded table — only the most critical known-vuln versions.
|
|
// This is the safety net for deployments that never call SetGlobalCVEDB.
|
|
known := map[string]map[string]bool{
|
|
"openssl": {"1.1.1k": true, "1.1.1j": true, "1.0.2u": true},
|
|
"curl": {"7.79.0": true, "7.71.0": true},
|
|
"zlib": {"1.2.11": true},
|
|
"libc": {"2.31": true, "2.34": true}, // glibc
|
|
"sudo": {"1.9.5p2": true},
|
|
}
|
|
if v, ok := known[library]; ok {
|
|
return v[version]
|
|
}
|
|
if v, ok := known[normalizeCPENAME(library)]; ok {
|
|
return v[version]
|
|
}
|
|
return false
|
|
}
|
|
|
|
// BatchCheckDependencies is a convenience function that takes a map of
|
|
// library->version (as produced by the Cast pipeline's DEPENDS parsing)
|
|
// and returns all libraries with known CVEs.
|
|
func BatchCheckDependencies(deps map[string]string) map[string][]CVERecord {
|
|
if globalCVEDB != nil {
|
|
return globalCVEDB.QueryCVEsBatch(deps)
|
|
}
|
|
|
|
// Fallback: use HasKnownCVE for each dep.
|
|
results := make(map[string][]CVERecord)
|
|
for lib, ver := range deps {
|
|
if HasKnownCVE(lib, ver) {
|
|
results[lib] = []CVERecord{{
|
|
ID: "EMBEDDED_CHECK",
|
|
Library: lib,
|
|
Version: ver,
|
|
Severity: SeverityMedium,
|
|
Description: "Vulnerable version detected by embedded table (upgrade NVD for full details)",
|
|
}}
|
|
}
|
|
}
|
|
return results
|
|
} |