Files
ServerKit/backend/app/models/image_scan.py
T
Juan Denis 57cbdfd5de feat(security): per-image CVE scanning (grype) and SBOM generation (syft)
- Add ImageVulnerabilityScan and SbomArtifact models.
- Add ImageScannerService to download/install grype and syft into
  /var/lib/serverkit/scanners, run CVE scans, generate SPDX SBOMs,
  and enforce a configurable deploy gate.
- Add security API endpoints for scanner install, image scans,
  scan history, deploy-gate checks, and SBOM create/download/list.
- Include lightweight image_scan badge in Application.to_dict() for UI.
- Add Alembic migration 018_image_scan_sbom.
2026-06-19 01:33:45 -04:00

97 lines
3.8 KiB
Python

from datetime import datetime
from app import db
import json
class ImageVulnerabilityScan(db.Model):
"""CVE scan result for a Docker image used by an application."""
__tablename__ = 'image_vulnerability_scans'
id = db.Column(db.Integer, primary_key=True)
application_id = db.Column(db.Integer, db.ForeignKey('applications.id'), nullable=False, index=True)
image_ref = db.Column(db.String(500), nullable=False)
scanner = db.Column(db.String(50), default='grype')
scanner_version = db.Column(db.String(50))
status = db.Column(db.String(20), default='pending') # pending, running, completed, failed
severity_counts = db.Column(db.Text) # JSON: {'critical': 0, 'high': 1, ...}
findings = db.Column(db.Text) # JSON list of grype matches
error_message = db.Column(db.Text)
started_at = db.Column(db.DateTime, default=datetime.utcnow)
completed_at = db.Column(db.DateTime, nullable=True)
application = db.relationship('Application', backref=db.backref('image_scans', lazy='dynamic', order_by='ImageVulnerabilityScan.started_at.desc()'))
def set_counts(self, counts):
self.severity_counts = json.dumps(counts)
def get_counts(self):
if not self.severity_counts:
return {}
try:
return json.loads(self.severity_counts)
except Exception:
return {}
def set_findings(self, findings):
self.findings = json.dumps(findings) if findings else None
def get_findings(self):
if not self.findings:
return []
try:
return json.loads(self.findings)
except Exception:
return []
@property
def highest_severity(self):
counts = self.get_counts()
for sev in ('critical', 'high', 'medium', 'low', 'negligible', 'unknown'):
if counts.get(sev, 0) > 0:
return sev
return 'none'
def to_dict(self, include_findings=False):
return {
'id': self.id,
'application_id': self.application_id,
'image_ref': self.image_ref,
'scanner': self.scanner,
'scanner_version': self.scanner_version,
'status': self.status,
'severity_counts': self.get_counts(),
'highest_severity': self.highest_severity,
'error_message': self.error_message,
'started_at': self.started_at.isoformat() if self.started_at else None,
'completed_at': self.completed_at.isoformat() if self.completed_at else None,
'findings': self.get_findings() if include_findings else None,
}
class SbomArtifact(db.Model):
"""Generated SPDX SBOM for a Docker image."""
__tablename__ = 'sbom_artifacts'
id = db.Column(db.Integer, primary_key=True)
application_id = db.Column(db.Integer, db.ForeignKey('applications.id'), nullable=False, index=True)
image_ref = db.Column(db.String(500), nullable=False)
generator = db.Column(db.String(50), default='syft')
generator_version = db.Column(db.String(50))
format = db.Column(db.String(20), default='spdx-json')
sbom_json = db.Column(db.Text) # SPDX 2.3 JSON
created_at = db.Column(db.DateTime, default=datetime.utcnow)
application = db.relationship('Application', backref=db.backref('sboms', lazy='dynamic', order_by='SbomArtifact.created_at.desc()'))
def to_dict(self, include_sbom=False):
return {
'id': self.id,
'application_id': self.application_id,
'image_ref': self.image_ref,
'generator': self.generator,
'generator_version': self.generator_version,
'format': self.format,
'created_at': self.created_at.isoformat() if self.created_at else None,
'sbom': json.loads(self.sbom_json) if include_sbom and self.sbom_json else None,
}