Files

117 lines
3.2 KiB
Go

package scanner
import (
"context"
"fmt"
"log/slog"
"time"
"atcr.io/pkg/hold"
"atcr.io/pkg/hold/pds"
"github.com/distribution/distribution/v3/registry/storage/driver"
)
// Worker performs vulnerability scanning on container images
type Worker struct {
config *hold.Config
driver driver.StorageDriver
pds *pds.HoldPDS
queue *Queue
}
// NewWorker creates a new scanner worker
func NewWorker(config *hold.Config, driver driver.StorageDriver, pds *pds.HoldPDS) *Worker {
return &Worker{
config: config,
driver: driver,
pds: pds,
}
}
// Start starts the worker pool and initializes vulnerability database
func (w *Worker) Start(queue *Queue) {
w.queue = queue
// Initialize vulnerability database on startup if scanning is enabled
if w.config.Scanner.VulnEnabled {
go func() {
ctx := context.Background()
if err := w.initializeVulnDatabase(ctx); err != nil {
slog.Error("Failed to initialize vulnerability database", "error", err)
slog.Warn("Vulnerability scanning will be disabled until database is available")
}
}()
}
queue.Start(w.processJob)
}
// processJob processes a single scan job
func (w *Worker) processJob(ctx context.Context, job *ScanJob) (*ScanResult, error) {
slog.Info("Processing scan job",
"repository", job.Repository,
"tag", job.Tag,
"digest", job.ManifestDigest,
"layers", len(job.Layers))
startTime := time.Now()
// Step 1: Extract image layers from storage
slog.Info("Extracting image layers", "repository", job.Repository)
imageDir, cleanup, err := w.extractLayers(ctx, job)
if err != nil {
return nil, fmt.Errorf("failed to extract layers: %w", err)
}
defer cleanup()
// Step 2: Generate SBOM with Syft
slog.Info("Generating SBOM", "repository", job.Repository)
sbomResult, _, sbomDigest, err := w.generateSBOM(ctx, imageDir)
if err != nil {
return nil, fmt.Errorf("failed to generate SBOM: %w", err)
}
// Step 3: Scan SBOM with Grype (if enabled)
var vulnJSON []byte
var vulnDigest string
var summary VulnerabilitySummary
if w.config.Scanner.VulnEnabled {
slog.Info("Scanning for vulnerabilities", "repository", job.Repository)
vulnJSON, vulnDigest, summary, err = w.scanVulnerabilities(ctx, sbomResult)
if err != nil {
return nil, fmt.Errorf("failed to scan vulnerabilities: %w", err)
}
}
// Step 4: Upload results to storage and create ORAS manifests
slog.Info("Storing scan results", "repository", job.Repository)
err = w.storeResults(ctx, job, sbomDigest, vulnDigest, vulnJSON, summary)
if err != nil {
return nil, fmt.Errorf("failed to store results: %w", err)
}
duration := time.Since(startTime)
slog.Info("Scan job completed",
"repository", job.Repository,
"tag", job.Tag,
"duration", duration,
"vulnerabilities", summary.Total)
return &ScanResult{
Job: job,
VulnerabilitiesJSON: vulnJSON,
Summary: summary,
SBOMDigest: sbomDigest,
VulnDigest: vulnDigest,
ScannedAt: time.Now(),
ScannerVersion: w.getScannerVersion(),
}, nil
}
// getScannerVersion returns the version string for the scanner
func (w *Worker) getScannerVersion() string {
// TODO: Get actual Syft and Grype versions dynamically
return "syft-v1.36.0/grype-v0.102.0"
}