Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions modules/exploit-intelligence/src/endpoints/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ async fn insert_test_job(
vulnerability_id: &str,
status: ExploitIntelligenceJobStatus,
error_message: Option<&str>,
total_components: Option<i32>,
) -> exploit_intelligence_job::Model {
let now = OffsetDateTime::now_utc();
let model = exploit_intelligence_job::ActiveModel {
Expand All @@ -59,7 +60,7 @@ async fn insert_test_job(
status: Set(status),
error_message: Set(error_message.map(|s| s.to_string())),
product_id: Set(None),
total_components: Set(None),
total_components: Set(total_components),
retry_count: Set(0),
created: Set(now),
updated: Set(now),
Expand Down Expand Up @@ -115,6 +116,7 @@ async fn get_job_completed(ctx: &TrustifyContext) -> anyhow::Result<()> {
"CVE-2024-1234",
ExploitIntelligenceJobStatus::Completed,
None,
Some(1),
)
.await;
insert_test_component(
Expand Down Expand Up @@ -155,6 +157,7 @@ async fn get_job_completed(ctx: &TrustifyContext) -> anyhow::Result<()> {
body.finding,
Some(ExploitIntelligenceFinding::NotVulnerable)
);
assert_eq!(body.total_components, Some(1));

Ok(())
}
Expand All @@ -174,6 +177,7 @@ async fn get_job_pending(ctx: &TrustifyContext) -> anyhow::Result<()> {
"CVE-2024-5678",
ExploitIntelligenceJobStatus::Pending,
None,
None,
)
.await;

Expand Down Expand Up @@ -221,6 +225,7 @@ async fn get_job_failed(ctx: &TrustifyContext) -> anyhow::Result<()> {
"CVE-2024-9999",
ExploitIntelligenceJobStatus::Failed,
Some("analysis timed out"),
None,
)
.await;

Expand Down Expand Up @@ -295,6 +300,7 @@ async fn list_jobs_paginated(ctx: &TrustifyContext) -> anyhow::Result<()> {
&format!("CVE-2024-{i:04}"),
ExploitIntelligenceJobStatus::Pending,
None,
None,
)
.await;
}
Expand Down Expand Up @@ -342,6 +348,7 @@ async fn list_jobs_filter_by_sbom_id(ctx: &TrustifyContext) -> anyhow::Result<()
"CVE-2024-0001",
ExploitIntelligenceJobStatus::Pending,
None,
None,
)
.await;

Expand All @@ -352,6 +359,7 @@ async fn list_jobs_filter_by_sbom_id(ctx: &TrustifyContext) -> anyhow::Result<()
"CVE-2024-0002",
ExploitIntelligenceJobStatus::Pending,
None,
None,
)
.await;

Expand Down Expand Up @@ -403,6 +411,7 @@ async fn analyze_deduplicates_active_jobs(ctx: &TrustifyContext) -> anyhow::Resu
"CVE-2024-DEDUP",
ExploitIntelligenceJobStatus::Pending,
None,
None,
)
.await;

Expand Down Expand Up @@ -455,14 +464,15 @@ async fn get_job_timeout_failure(ctx: &TrustifyContext) -> anyhow::Result<()> {
"CVE-2024-TIMEOUT",
ExploitIntelligenceJobStatus::Running,
None,
None,
)
.await;

// When the service marks it as failed with a timeout error
let ei_service = test_service();
let db_rw = db::ReadWrite::new(ctx.db.clone());
ei_service
.update_job_failed(job_id, "analysis timed out after 1800s", &db_rw)
.update_job_failed(job_id, "analysis timed out after 1800s", None, &db_rw)
.await?;

// Then fetching the job via GET shows the failed status and timeout message
Expand Down Expand Up @@ -507,6 +517,7 @@ async fn get_job_finding_without_advisory(ctx: &TrustifyContext) -> anyhow::Resu
"CVE-2024-VULN",
ExploitIntelligenceJobStatus::Completed,
None,
None,
)
.await;
insert_test_component(
Expand Down Expand Up @@ -605,6 +616,7 @@ async fn get_job_running(ctx: &TrustifyContext) -> anyhow::Result<()> {
"CVE-2024-RUN",
ExploitIntelligenceJobStatus::Running,
None,
None,
)
.await;

Expand Down Expand Up @@ -652,6 +664,7 @@ async fn list_jobs_filter_by_status(ctx: &TrustifyContext) -> anyhow::Result<()>
"CVE-2024-S001",
ExploitIntelligenceJobStatus::Completed,
None,
None,
)
.await;

Expand All @@ -662,6 +675,7 @@ async fn list_jobs_filter_by_status(ctx: &TrustifyContext) -> anyhow::Result<()>
"CVE-2024-S002",
ExploitIntelligenceJobStatus::Pending,
None,
None,
)
.await;

Expand All @@ -672,6 +686,7 @@ async fn list_jobs_filter_by_status(ctx: &TrustifyContext) -> anyhow::Result<()>
"CVE-2024-S003",
ExploitIntelligenceJobStatus::Completed,
None,
None,
)
.await;

Expand Down
11 changes: 6 additions & 5 deletions modules/exploit-intelligence/src/runner/polling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ pub(crate) async fn poll_for_result(
.update_component_completed(component_id, finding, advisory_id, db)
.await?;

ei_service.update_job_completed(job_id, db).await?;
ei_service.update_job_completed(job_id, Some(1), db).await?;

tracing::info!(
job_id = %job_id,
Expand All @@ -154,7 +154,7 @@ pub(crate) async fn poll_for_result(
.update_component_failed(component_id, &error_msg, db)
.await?;

ei_service.update_job_completed(job_id, db).await?;
ei_service.update_job_completed(job_id, Some(1), db).await?;

Ok(())
}
Expand Down Expand Up @@ -288,16 +288,16 @@ pub(crate) async fn poll_for_product_result(
.await?;
}

// Check whether any component actually has a finding.
let components = ei_service.fetch_components(job_id, db).await?;
let total = Some(components.len() as i32);
Comment thread
Strum355 marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its unlikely any EI job would have 2b+ components ... though maybe add a source comment to inform future human/agents we ok with this


let has_finding = components.iter().any(|c| {
c.status == ExploitIntelligenceJobStatus::Completed && c.finding.is_some()
});
let all_excluded = !components.is_empty() && components.iter().all(|c| c.excluded);

if has_finding || all_excluded {
ei_service.update_job_completed(job_id, db).await?;
ei_service.update_job_completed(job_id, total, db).await?;

tracing::info!(
job_id = %job_id,
Expand All @@ -314,6 +314,7 @@ pub(crate) async fn poll_for_product_result(
"no components produced findings ({} total, {failed_not_excluded} failed)",
components.len()
),
total,
db,
)
.await?;
Expand All @@ -329,7 +330,7 @@ pub(crate) async fn poll_for_product_result(
}
ProductState::Failed => {
let msg = format!("EI product analysis failed for product {product_id}");
ei_service.update_job_failed(job_id, &msg, db).await?;
ei_service.update_job_failed(job_id, &msg, None, db).await?;
Ok(())
}
// Still in progress — no DB changes, job stays Running for re-poll next tick.
Expand Down
109 changes: 92 additions & 17 deletions modules/exploit-intelligence/src/runner/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ async fn run_pipeline(
.await
{
let _ = ei_service
.update_job_failed(claimed.id, &e.to_string(), &claim_tx)
.update_job_failed(claimed.id, &e.to_string(), None, &claim_tx)
.await;
claim_tx.commit().await.expect("commit tx");
break;
Expand Down Expand Up @@ -148,6 +148,16 @@ async fn cdx_completed_not_vulnerable(ctx: &TrustifyContext) -> anyhow::Result<(
details.finding,
Some(ExploitIntelligenceFinding::NotVulnerable)
);
assert_eq!(details.product_id, None);
assert!(details.report_url.is_some());
assert_eq!(details.total_components, Some(1));
assert_eq!(details.completed_components, Some(1));
assert_eq!(details.failed_components, Some(0));
assert_eq!(details.excluded_components, Some(0));
assert_eq!(details.vulnerable_components, Some(0));
assert_eq!(details.not_vulnerable_components, Some(1));
assert_eq!(details.uncertain_components, Some(0));
assert_eq!(details.components.len(), 1);
Ok(())
}

Expand Down Expand Up @@ -187,11 +197,19 @@ async fn cdx_ei_analysis_failed(ctx: &TrustifyContext) -> anyhow::Result<()> {
let details = ei_service.fetch_job(job_id, &*db_rw).await?;
let details = details.expect("job should exist");
assert_eq!(details.status, ExploitIntelligenceJobStatus::Completed);
assert!(
details
.components
.iter()
.any(|c| { c.status == ExploitIntelligenceJobStatus::Failed })
assert_eq!(details.finding, None);
assert_eq!(details.product_id, None);
assert_eq!(details.total_components, Some(1));
assert_eq!(details.completed_components, Some(0));
assert_eq!(details.failed_components, Some(1));
assert_eq!(details.excluded_components, Some(0));
assert_eq!(details.vulnerable_components, Some(0));
assert_eq!(details.not_vulnerable_components, Some(0));
assert_eq!(details.uncertain_components, Some(0));
assert_eq!(details.components.len(), 1);
assert_eq!(
details.components[0].status,
ExploitIntelligenceJobStatus::Failed
);
Ok(())
}
Expand All @@ -216,6 +234,10 @@ async fn cdx_upload_permanent_failure(ctx: &TrustifyContext) -> anyhow::Result<(
let details = ei_service.fetch_job(job_id, &*db_rw).await?;
let details = details.expect("job should exist");
assert_eq!(details.status, ExploitIntelligenceJobStatus::Failed);
assert_eq!(details.finding, None);
assert_eq!(details.product_id, None);
assert_eq!(details.total_components, None);
assert!(details.components.is_empty());
Ok(())
}

Expand All @@ -239,6 +261,10 @@ async fn cdx_upload_transient_failure(ctx: &TrustifyContext) -> anyhow::Result<(
let details = ei_service.fetch_job(job_id, &*db_rw).await?;
let details = details.expect("job should exist");
assert_eq!(details.status, ExploitIntelligenceJobStatus::Failed);
assert_eq!(details.finding, None);
assert_eq!(details.product_id, None);
assert_eq!(details.total_components, None);
assert!(details.components.is_empty());
Ok(())
}

Expand Down Expand Up @@ -322,6 +348,15 @@ async fn spdx_completed_with_finding(ctx: &TrustifyContext) -> anyhow::Result<()
details.finding,
Some(ExploitIntelligenceFinding::NotVulnerable)
);
assert_eq!(details.product_id.as_deref(), Some("prod-001"));
assert!(details.report_url.is_some());
assert_eq!(details.total_components, Some(1));
assert_eq!(details.completed_components, Some(1));
assert_eq!(details.failed_components, Some(0));
assert_eq!(details.excluded_components, Some(0));
assert_eq!(details.vulnerable_components, Some(0));
assert_eq!(details.not_vulnerable_components, Some(1));
assert_eq!(details.uncertain_components, Some(0));
assert_eq!(details.components.len(), 1);
Ok(())
}
Expand Down Expand Up @@ -382,7 +417,16 @@ async fn spdx_all_components_excluded(ctx: &TrustifyContext) -> anyhow::Result<(
.expect("job should exist");
assert_eq!(details.status, ExploitIntelligenceJobStatus::Completed);
assert_eq!(details.finding, None);
assert!(details.components.iter().any(|c| c.excluded));
assert_eq!(details.product_id.as_deref(), Some("prod-excl"));
assert_eq!(details.total_components, Some(1));
assert_eq!(details.completed_components, Some(0));
assert_eq!(details.failed_components, Some(0));
assert_eq!(details.excluded_components, Some(1));
assert_eq!(details.vulnerable_components, Some(0));
assert_eq!(details.not_vulnerable_components, Some(0));
assert_eq!(details.uncertain_components, Some(0));
assert_eq!(details.components.len(), 1);
assert!(details.components[0].excluded);
Ok(())
}

Expand Down Expand Up @@ -429,6 +473,10 @@ async fn spdx_product_failed(ctx: &TrustifyContext) -> anyhow::Result<()> {
.await?
.expect("job should exist");
assert_eq!(details.status, ExploitIntelligenceJobStatus::Failed);
assert_eq!(details.finding, None);
assert_eq!(details.product_id.as_deref(), Some("prod-fail"));
assert_eq!(details.total_components, None);
assert!(details.components.is_empty());
Ok(())
}

Expand Down Expand Up @@ -476,9 +524,17 @@ async fn cdx_completed_with_vex_ingestion(ctx: &TrustifyContext) -> anyhow::Resu
.await?
.expect("job should exist");
assert_eq!(details.status, ExploitIntelligenceJobStatus::Completed);
assert_eq!(
details.finding,
Some(ExploitIntelligenceFinding::NotVulnerable)
);
assert_eq!(details.total_components, Some(1));
assert_eq!(details.completed_components, Some(1));
assert_eq!(details.not_vulnerable_components, Some(1));
assert_eq!(details.components.len(), 1);
assert!(
details.components.iter().any(|c| c.advisory_id.is_some()),
"expected a component with an ingested advisory"
details.components[0].advisory_id.is_some(),
"expected component with an ingested advisory"
);
Ok(())
}
Expand Down Expand Up @@ -568,9 +624,16 @@ async fn spdx_mixed_completed_and_excluded(ctx: &TrustifyContext) -> anyhow::Res
details.finding,
Some(ExploitIntelligenceFinding::NotVulnerable)
);
assert_eq!(details.product_id.as_deref(), Some("prod-mixed"));
assert!(details.report_url.is_some());
assert_eq!(details.total_components, Some(2));
assert_eq!(details.completed_components, Some(1));
assert_eq!(details.failed_components, Some(0));
assert_eq!(details.excluded_components, Some(1));
assert_eq!(details.vulnerable_components, Some(0));
assert_eq!(details.not_vulnerable_components, Some(1));
assert_eq!(details.uncertain_components, Some(0));
assert_eq!(details.components.len(), 2);
assert_eq!(details.completed_components, Some(1),);
assert_eq!(details.excluded_components, Some(1),);
Ok(())
}

Expand Down Expand Up @@ -661,6 +724,10 @@ async fn spdx_reports_endpoint_failure(ctx: &TrustifyContext) -> anyhow::Result<
.await?
.expect("job should exist");
assert_eq!(details.status, ExploitIntelligenceJobStatus::Failed);
assert_eq!(details.finding, None);
assert_eq!(details.product_id.as_deref(), Some("prod-reportfail"));
assert_eq!(details.total_components, None);
assert!(details.components.is_empty());
Ok(())
}

Expand Down Expand Up @@ -711,12 +778,16 @@ async fn spdx_component_expired_report(ctx: &TrustifyContext) -> anyhow::Result<
.await?
.expect("job should exist");
assert_eq!(details.status, ExploitIntelligenceJobStatus::Failed);
assert!(
details
.components
.iter()
.any(|c| { c.status == ExploitIntelligenceJobStatus::Failed }),
"expected a failed component"
assert_eq!(details.finding, None);
assert_eq!(details.product_id.as_deref(), Some("prod-compexp"));
assert_eq!(details.total_components, Some(1));
assert_eq!(details.completed_components, Some(0));
assert_eq!(details.failed_components, Some(1));
assert_eq!(details.excluded_components, Some(0));
assert_eq!(details.components.len(), 1);
assert_eq!(
details.components[0].status,
ExploitIntelligenceJobStatus::Failed
);
Ok(())
}
Expand Down Expand Up @@ -761,5 +832,9 @@ async fn spdx_no_findings_marks_job_failed(ctx: &TrustifyContext) -> anyhow::Res
.await?
.expect("job should exist");
assert_eq!(details.status, ExploitIntelligenceJobStatus::Failed);
assert_eq!(details.finding, None);
assert_eq!(details.product_id.as_deref(), Some("prod-nofind"));
assert_eq!(details.total_components, Some(0));
assert!(details.components.is_empty());
Ok(())
}
Loading
Loading