Skip to content

fix: persist total_components count for EI analysis when job terminates [Backport release/0.6.z] - #2574

Merged
Strum355 merged 1 commit into
release/0.6.zfrom
backport-2570-to-release/0.6.z
Aug 12, 2026
Merged

fix: persist total_components count for EI analysis when job terminates [Backport release/0.6.z]#2574
Strum355 merged 1 commit into
release/0.6.zfrom
backport-2570-to-release/0.6.z

Conversation

@trustify-ci-bot

@trustify-ci-bot trustify-ci-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

Description

Backport of #2570 to release/0.6.z.

Summary by Sourcery

Persist and expose accurate total component counts for exploit intelligence analysis jobs, including when jobs complete or fail, and strengthen tests around job details and endpoint behaviour.

Bug Fixes:

  • Ensure total_components is stored when EI jobs complete or fail so counts remain available after job termination.
  • Fix product-result polling to always persist the total number of analyzed components for multi-component SPDX flows.

Enhancements:

  • Expand EI runner and endpoint tests to assert full job metadata, including product, report URL, component counts, and component statuses.
  • Simplify job summary construction by relying on count-aware factory functions instead of post-hoc total_components adjustments.

@sourcery-ai

sourcery-ai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This backport ensures exploit intelligence jobs persist and expose accurate total_components and related component counters when jobs complete or fail, and wires that through service APIs, runners, and tests.

Sequence diagram for EI product job completion/failure total_components persistence

sequenceDiagram
    actor Runner
    participant EiService
    participant Db

    Runner->>EiService: poll_for_product_result(job_id, product_id, db)
    EiService->>Db: fetch_components(job_id, db)
    Db-->>EiService: components
    EiService->>EiService: [compute total = components.len()]
    alt has_finding or all_excluded
        EiService->>Db: update_job_completed(job_id, total, db)
        Db-->>EiService: Result
    else no findings and not all_excluded
        EiService->>Db: update_job_failed(job_id, msg, total, db)
        Db-->>EiService: Result
    end
    EiService-->>Runner: Result
Loading

File-Level Changes

Change Details Files
Propagate and persist total_components for EI jobs on completion/failure, including single-component and multi-component product flows.
  • Extended ExploitIntelligenceService::update_job_completed to accept an optional total_components and persist it using ActiveValue::NotSet when absent.
  • Extended ExploitIntelligenceService::update_job_failed to accept an optional total_components and persist it similarly.
  • Updated runner polling logic for component and product result handling to compute total_components from fetched components and pass it into update_job_completed/update_job_failed as appropriate.
  • Updated worker error-handling paths (max retries exhausted, permanent failure) to call update_job_failed with the new total_components parameter (currently None).
  • Removed create_job_with_product helper and now rely on runtime flows to set product_id and total_components via updates rather than at job creation.
modules/exploit-intelligence/src/service/mod.rs
modules/exploit-intelligence/src/runner/polling.rs
modules/exploit-intelligence/src/runner/worker.rs
Strengthen tests to validate persisted total_components, component counters, and related job metadata across various EI scenarios.
  • Updated runner tests to pass the new total_components parameter into update_job_failed and assert detailed fields like product_id, report_url, total/completed/failed/excluded/vulnerable/not_vulnerable/uncertain_components and component list lengths/statuses.
  • Adjusted service tests to call the new update_job_completed/update_job_failed signatures and assert that total_components is correctly stored on jobs.
  • Extended endpoint tests’ insert_test_job helper to accept total_components and updated endpoint tests to set/verify total_components and other fields in responses.
  • Refined some assertions from generic component-iter checks to explicit index-based checks (e.g., components[0]) for clarity and stronger guarantees.
modules/exploit-intelligence/src/runner/test.rs
modules/exploit-intelligence/src/service/test.rs
modules/exploit-intelligence/src/endpoints/test.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

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.

Hey - I've found 1 issue, and left some high level feedback:

  • In poll_for_result, update_job_completed is called with a hard-coded Some(1) for total_components; consider deriving the count from fetch_components as in poll_for_product_result to avoid incorrect totals if the flow ever becomes multi-component.
  • The new update_job_completed/update_job_failed semantics leave total_components unchanged when called with None; verify whether callers ever expect the value to be cleared instead of preserved, and if so, consider setting it explicitly to Set(None) in those cases to avoid stale counts.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `poll_for_result`, `update_job_completed` is called with a hard-coded `Some(1)` for `total_components`; consider deriving the count from `fetch_components` as in `poll_for_product_result` to avoid incorrect totals if the flow ever becomes multi-component.
- The new `update_job_completed`/`update_job_failed` semantics leave `total_components` unchanged when called with `None`; verify whether callers ever expect the value to be cleared instead of preserved, and if so, consider setting it explicitly to `Set(None)` in those cases to avoid stale counts.

## Individual Comments

### Comment 1
<location path="modules/exploit-intelligence/src/endpoints/test.rs" line_range="475" />
<code_context>
     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?;

</code_context>
<issue_to_address>
**suggestion (testing):** Assert `total_components` on the timeout failure endpoint test

This timeout test only checks status and error message. Since `update_job_failed` is now called with `total_components = None`, please also assert that the GET response exposes `total_components == None`. Consider a variant where the job already has a `total_components` value to confirm it remains unchanged under timeout failures, so the endpoint is clearly tied to the new failure semantics for component counts.

Suggested implementation:

```rust
    let ei_service = test_service();
    let db_rw = db::ReadWrite::new(ctx.db.clone());

    // Assert that timeout failures expose `total_components == None`
    let timeout_status = ei_service
        .get_job_status(job_id, &db_rw)
        .await
        .expect("failed to fetch job status after timeout");
    assert_eq!(timeout_status.total_components, None);

    // Variant: job with pre-existing `total_components` keeps its value on timeout
    let pre_existing_total_components = Some(42u32);
    let pre_existing_job_id = ei_service
        .create_job(
            &ctx,
            "CVE-2024-9999",
            ExploitIntelligenceJobStatus::Running,
            pre_existing_total_components,
            None,
        )
        .await
        .expect("failed to create job with pre-existing total_components");

    ei_service
        .update_job_failed(
            pre_existing_job_id,
            "analysis timed out after 1800s",
            None,
            &db_rw,
        )
        .await
        .expect("failed to mark pre-existing job as timed out");

    let pre_existing_timeout_status = ei_service
        .get_job_status(pre_existing_job_id, &db_rw)
        .await
        .expect("failed to fetch pre-existing job status after timeout");
    assert_eq!(
        pre_existing_timeout_status.total_components,
        pre_existing_total_components
    );

    ei_service

```

I only see a small fragment of the test file, so you will likely need to adapt the above to your actual helpers and types:

1. Replace `get_job_status` with the actual GET endpoint helper used in this test (e.g. `get_exploit_intelligence_job`, `get_job`, or whatever returns the job DTO that includes `total_components`).
2. If the timeout test currently drives the endpoint via HTTP (e.g. using `ctx.get(...)`), move the `assert_eq!(..., None)` assertions to use the decoded HTTP response body instead of `ei_service.get_job_status(...)`.
3. Replace `create_job` with the existing job creation helper you already use for `"CVE-2024-5678"` and `"CVE-2024-9999"` in this file, matching its signature (status, `total_components`, `completed_components`).
4. Ensure the `job_id` used in the first assertion is the same one that was marked failed with `update_job_failed(job_id, "analysis timed out after 1800s", None, &db_rw)` in the timeout test.
5. If the job DTO nests `total_components` (e.g. `response.body.total_components` or `job.metrics.total_components`), adjust the field access in the assertions accordingly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

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)

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.

suggestion (testing): Assert total_components on the timeout failure endpoint test

This timeout test only checks status and error message. Since update_job_failed is now called with total_components = None, please also assert that the GET response exposes total_components == None. Consider a variant where the job already has a total_components value to confirm it remains unchanged under timeout failures, so the endpoint is clearly tied to the new failure semantics for component counts.

Suggested implementation:

    let ei_service = test_service();
    let db_rw = db::ReadWrite::new(ctx.db.clone());

    // Assert that timeout failures expose `total_components == None`
    let timeout_status = ei_service
        .get_job_status(job_id, &db_rw)
        .await
        .expect("failed to fetch job status after timeout");
    assert_eq!(timeout_status.total_components, None);

    // Variant: job with pre-existing `total_components` keeps its value on timeout
    let pre_existing_total_components = Some(42u32);
    let pre_existing_job_id = ei_service
        .create_job(
            &ctx,
            "CVE-2024-9999",
            ExploitIntelligenceJobStatus::Running,
            pre_existing_total_components,
            None,
        )
        .await
        .expect("failed to create job with pre-existing total_components");

    ei_service
        .update_job_failed(
            pre_existing_job_id,
            "analysis timed out after 1800s",
            None,
            &db_rw,
        )
        .await
        .expect("failed to mark pre-existing job as timed out");

    let pre_existing_timeout_status = ei_service
        .get_job_status(pre_existing_job_id, &db_rw)
        .await
        .expect("failed to fetch pre-existing job status after timeout");
    assert_eq!(
        pre_existing_timeout_status.total_components,
        pre_existing_total_components
    );

    ei_service

I only see a small fragment of the test file, so you will likely need to adapt the above to your actual helpers and types:

  1. Replace get_job_status with the actual GET endpoint helper used in this test (e.g. get_exploit_intelligence_job, get_job, or whatever returns the job DTO that includes total_components).
  2. If the timeout test currently drives the endpoint via HTTP (e.g. using ctx.get(...)), move the assert_eq!(..., None) assertions to use the decoded HTTP response body instead of ei_service.get_job_status(...).
  3. Replace create_job with the existing job creation helper you already use for "CVE-2024-5678" and "CVE-2024-9999" in this file, matching its signature (status, total_components, completed_components).
  4. Ensure the job_id used in the first assertion is the same one that was marked failed with update_job_failed(job_id, "analysis timed out after 1800s", None, &db_rw) in the timeout test.
  5. If the job DTO nests total_components (e.g. response.body.total_components or job.metrics.total_components), adjust the field access in the assertions accordingly.

@Strum355
Strum355 merged commit a269f96 into release/0.6.z Aug 12, 2026
9 checks passed
@Strum355
Strum355 deleted the backport-2570-to-release/0.6.z branch August 12, 2026 14:46
@github-project-automation github-project-automation Bot moved this to Done in Trustify Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant