mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-08 21:14:47 +09:00
- 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.
97 lines
3.8 KiB
Python
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,
|
|
}
|