Files

124 lines
4.2 KiB
Go

package scanner
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"log/slog"
"time"
"atcr.io/pkg/atproto"
)
// storeResults uploads scan results and creates ORAS manifest records in the hold's PDS
func (w *Worker) storeResults(ctx context.Context, job *ScanJob, sbomDigest, vulnDigest string, vulnJSON []byte, summary VulnerabilitySummary) error {
if !w.config.Scanner.VulnEnabled {
slog.Info("Vulnerability scanning disabled, skipping result storage")
return nil
}
slog.Info("Storing scan results as ORAS artifact",
"repository", job.Repository,
"subjectDigest", job.ManifestDigest,
"vulnDigest", vulnDigest)
// Create ORAS manifest for vulnerability report
orasManifest := map[string]interface{}{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"artifactType": "application/vnd.atcr.vulnerabilities+json",
"config": map[string]interface{}{
"mediaType": "application/vnd.oci.empty.v1+json",
"digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", // Empty JSON object
"size": 2,
},
"subject": map[string]interface{}{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": job.ManifestDigest,
"size": 0, // We don't have the size, but it's optional
},
"layers": []map[string]interface{}{
{
"mediaType": "application/json",
"digest": vulnDigest,
"size": len(vulnJSON),
"annotations": map[string]string{
"org.opencontainers.image.title": "vulnerability-report.json",
},
},
},
"annotations": map[string]string{
"io.atcr.vuln.critical": fmt.Sprintf("%d", summary.Critical),
"io.atcr.vuln.high": fmt.Sprintf("%d", summary.High),
"io.atcr.vuln.medium": fmt.Sprintf("%d", summary.Medium),
"io.atcr.vuln.low": fmt.Sprintf("%d", summary.Low),
"io.atcr.vuln.total": fmt.Sprintf("%d", summary.Total),
"io.atcr.vuln.scannedAt": time.Now().Format(time.RFC3339),
"io.atcr.vuln.scannerVersion": w.getScannerVersion(),
},
}
// Encode ORAS manifest to JSON
orasManifestJSON, err := json.Marshal(orasManifest)
if err != nil {
return fmt.Errorf("failed to encode ORAS manifest: %w", err)
}
// Calculate ORAS manifest digest
orasDigest := fmt.Sprintf("sha256:%x", sha256Bytes(orasManifestJSON))
// Upload ORAS manifest blob to storage
if err := w.uploadBlob(ctx, orasDigest, orasManifestJSON); err != nil {
return fmt.Errorf("failed to upload ORAS manifest blob: %w", err)
}
// Create manifest record in hold's PDS
if err := w.createManifestRecord(ctx, job, orasDigest, orasManifestJSON, summary); err != nil {
return fmt.Errorf("failed to create manifest record: %w", err)
}
slog.Info("Successfully stored scan results", "orasDigest", orasDigest)
return nil
}
// createManifestRecord creates an ORAS manifest record in the hold's PDS
func (w *Worker) createManifestRecord(ctx context.Context, job *ScanJob, orasDigest string, orasManifestJSON []byte, summary VulnerabilitySummary) error {
// Create ManifestRecord from ORAS manifest
record, err := atproto.NewManifestRecord(job.Repository, orasDigest, orasManifestJSON)
if err != nil {
return fmt.Errorf("failed to create manifest record: %w", err)
}
// Set SBOM/vulnerability specific fields
record.OwnerDID = job.UserDID
record.ScannedAt = time.Now().Format(time.RFC3339)
record.ScannerVersion = w.getScannerVersion()
// Add hold DID (this ORAS artifact is stored in the hold's PDS)
record.HoldDID = w.pds.DID()
// Convert digest to record key (remove "sha256:" prefix)
rkey := orasDigest[len("sha256:"):]
// Store record in hold's PDS
slog.Info("Creating manifest record in hold's PDS",
"collection", atproto.ManifestCollection,
"rkey", rkey,
"ownerDid", job.UserDID)
_, _, err = w.pds.CreateManifestRecord(ctx, record, rkey)
if err != nil {
return fmt.Errorf("failed to put record in PDS: %w", err)
}
slog.Info("Manifest record created successfully", "uri", fmt.Sprintf("at://%s/%s/%s", w.pds.DID(), atproto.ManifestCollection, rkey))
return nil
}
// sha256Bytes calculates SHA256 hash of byte slice
func sha256Bytes(data []byte) []byte {
hash := sha256.Sum256(data)
return hash[:]
}