Implement Trivy adapter, contain job panics, fail interrupted runs on startup
The Trivy scanner still had unimplemented stubs, which panicked the scan task in the deployed test instance and left the run in 'running' forever. JobRunner now runs handlers in their own task and marks a panic as a failed run; on startup runs left 'running' by a previous process are marked failed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@ -393,6 +393,15 @@ impl domain::ports::JobRunRepository for SqliteJobRuns {
|
||||
.map(|r| r.as_ref().map(job_from_row))
|
||||
.map_err(storage)
|
||||
}
|
||||
async fn running(&self) -> Result<Vec<JobRun>, DomainError> {
|
||||
sqlx::query(&format!(
|
||||
"SELECT {JOB_COLS} FROM job_runs WHERE status = 'running'"
|
||||
))
|
||||
.fetch_all(&self.0)
|
||||
.await
|
||||
.map(|rows| rows.iter().map(job_from_row).collect())
|
||||
.map_err(storage)
|
||||
}
|
||||
async fn last_finished(&self, kind: JobKind) -> Result<Option<JobRun>, DomainError> {
|
||||
sqlx::query(&format!("SELECT {JOB_COLS} FROM job_runs WHERE kind = ? AND status != 'running' ORDER BY started_at DESC LIMIT 1"))
|
||||
.bind(kind.as_str())
|
||||
|
||||
@ -18,26 +18,133 @@ impl TrivyScanner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse `trivy ... --format json` output into findings (all results merged, deduplicated).
|
||||
pub fn parse_trivy_json(_json: &str) -> Result<Vec<RawFinding>, DomainError> {
|
||||
todo!()
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Report {
|
||||
#[serde(rename = "Results", default)]
|
||||
results: Option<Vec<ResultEntry>>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ResultEntry {
|
||||
#[serde(rename = "Vulnerabilities", default)]
|
||||
vulnerabilities: Option<Vec<Vuln>>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Vuln {
|
||||
#[serde(rename = "VulnerabilityID")]
|
||||
id: String,
|
||||
#[serde(rename = "PkgName", default)]
|
||||
pkg: String,
|
||||
#[serde(rename = "InstalledVersion", default)]
|
||||
installed: String,
|
||||
#[serde(rename = "FixedVersion", default)]
|
||||
fixed: Option<String>,
|
||||
#[serde(rename = "Severity", default)]
|
||||
severity: String,
|
||||
#[serde(rename = "Title", default)]
|
||||
title: Option<String>,
|
||||
#[serde(rename = "PrimaryURL", default)]
|
||||
url: Option<String>,
|
||||
}
|
||||
|
||||
/// Parse `trivy ... --format json` output into findings (all results merged, deduplicated).
|
||||
pub fn parse_trivy_json(json: &str) -> Result<Vec<RawFinding>, DomainError> {
|
||||
let report: Report = serde_json::from_str(json)
|
||||
.map_err(|e| DomainError::Unavailable(format!("trivy output: {e}")))?;
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut out = Vec::new();
|
||||
let vulns = report
|
||||
.results
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.flat_map(|r| r.vulnerabilities.unwrap_or_default());
|
||||
for v in vulns {
|
||||
let f = RawFinding {
|
||||
title: v
|
||||
.title
|
||||
.filter(|t| !t.is_empty())
|
||||
.unwrap_or_else(|| format!("{} in {}", v.id, v.pkg)),
|
||||
url: v
|
||||
.url
|
||||
.unwrap_or_else(|| format!("https://nvd.nist.gov/vuln/detail/{}", v.id)),
|
||||
cve_id: v.id,
|
||||
severity: Severity::parse(&v.severity),
|
||||
package: v.pkg,
|
||||
installed_version: v.installed,
|
||||
fixed_version: v.fixed.filter(|f| !f.is_empty()),
|
||||
};
|
||||
if seen.insert(f.key()) {
|
||||
out.push(f);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
impl TrivyScanner {
|
||||
async fn scan(
|
||||
&self,
|
||||
args: &[&str],
|
||||
out: &dyn LineSink,
|
||||
) -> Result<Vec<RawFinding>, DomainError> {
|
||||
out.line(&format!("$ trivy {}", args.join(" ")));
|
||||
let res = self.runner.run("trivy", args).await?;
|
||||
if !res.success {
|
||||
let tail: Vec<&str> = res.stderr.lines().rev().take(5).collect();
|
||||
for l in tail.into_iter().rev() {
|
||||
out.line(l);
|
||||
}
|
||||
return Err(DomainError::Unavailable(
|
||||
"trivy exited with a non-zero status".into(),
|
||||
));
|
||||
}
|
||||
let findings = parse_trivy_json(&res.stdout)?;
|
||||
out.line(&format!("{} finding(s)", findings.len()));
|
||||
Ok(findings)
|
||||
}
|
||||
}
|
||||
|
||||
const COMMON: [&str; 7] = [
|
||||
"--format",
|
||||
"json",
|
||||
"--quiet",
|
||||
"--scanners",
|
||||
"vuln",
|
||||
"--timeout",
|
||||
"15m",
|
||||
];
|
||||
|
||||
#[async_trait]
|
||||
impl VulnerabilityScanner for TrivyScanner {
|
||||
async fn version(&self) -> Result<String, DomainError> {
|
||||
let _ = &self.runner;
|
||||
todo!()
|
||||
let res = self.runner.run("trivy", &["--version"]).await?;
|
||||
if !res.success {
|
||||
return Err(DomainError::Unavailable("trivy is not installed".into()));
|
||||
}
|
||||
Ok(res
|
||||
.stdout
|
||||
.lines()
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.trim_start_matches("Version:")
|
||||
.trim()
|
||||
.to_string())
|
||||
}
|
||||
async fn scan_os(&self, _out: &dyn LineSink) -> Result<Vec<RawFinding>, DomainError> {
|
||||
todo!()
|
||||
async fn scan_os(&self, out: &dyn LineSink) -> Result<Vec<RawFinding>, DomainError> {
|
||||
let mut args = vec!["rootfs"];
|
||||
args.extend(COMMON);
|
||||
args.push("/");
|
||||
self.scan(&args, out).await
|
||||
}
|
||||
async fn scan_image(
|
||||
&self,
|
||||
_image: &str,
|
||||
_out: &dyn LineSink,
|
||||
image: &str,
|
||||
out: &dyn LineSink,
|
||||
) -> Result<Vec<RawFinding>, DomainError> {
|
||||
todo!()
|
||||
let mut args = vec!["image"];
|
||||
args.extend(COMMON);
|
||||
args.push(image);
|
||||
self.scan(&args, out).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user