Skip to content

fix: reject empty/whitespace-only vulnerability_id in /v3/exploit-intelligence/analyze endpoint - #2567

Merged
Strum355 merged 1 commit into
guacsec:mainfrom
Strum355:TC-5535
Aug 12, 2026
Merged

fix: reject empty/whitespace-only vulnerability_id in /v3/exploit-intelligence/analyze endpoint#2567
Strum355 merged 1 commit into
guacsec:mainfrom
Strum355:TC-5535

Conversation

@Strum355

@Strum355 Strum355 commented Aug 11, 2026

Copy link
Copy Markdown
Member

Fixes TC-5535

Summary by Sourcery

Validate and reject exploit intelligence analyze requests with blank vulnerability IDs and surface them as client errors.

Bug Fixes:

  • Return HTTP 400 Bad Request when /v3/exploit-intelligence/analyze is called with an empty or whitespace-only vulnerability_id.
  • Treat bad-request errors from exploit intelligence as permanent analysis errors rather than retryable ones.

Enhancements:

  • Trim vulnerability_id input before processing to avoid jobs keyed by whitespace-only IDs.
  • Extend exploit intelligence error handling with a dedicated BadRequest variant and corresponding HTTP response payload.

@sourcery-ai

sourcery-ai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR adds validation to the /v3/exploit-intelligence/analyze endpoint to reject empty or whitespace-only vulnerability_id values with a 400 Bad Request, wires a new BadRequest error type through the exploit-intelligence module, and ensures the runner treats such errors as permanent while covering the behavior with an endpoint test.

Sequence diagram for updated exploit_intelligence_analyze request handling

sequenceDiagram
    actor Client
    participant AnalyzeEndpoint as analyze
    participant EIService as ei_service
    participant DatabaseRW as db_rw
    participant DatabaseRO as db_ro

    Client->>AnalyzeEndpoint: POST /v3/exploit-intelligence/analyze
    AnalyzeEndpoint->>ei_service: runtime()
    ei_service-->>AnalyzeEndpoint: Option<Runtime>
    alt [runtime is None]
        AnalyzeEndpoint-->>Client: Error::Unavailable (503)
    else [runtime is Some]
        AnalyzeEndpoint->>AnalyzeEndpoint: vulnerability_id.trim()
        alt [vulnerability_id.is_empty()]
            AnalyzeEndpoint-->>Client: Error::BadRequest (400)
        else [vulnerability_id not empty]
            AnalyzeEndpoint->>DatabaseRW: begin()
            DatabaseRW-->>AnalyzeEndpoint: tx
            AnalyzeEndpoint->>EIService: create_job(sbom_id, vulnerability_id, tx)
            EIService-->>AnalyzeEndpoint: Result<Job, Error>
            alt [Ok(job)]
                AnalyzeEndpoint-->>Client: 201 AnalyzeResponse
            else [Err(e)]
                AnalyzeEndpoint->>DatabaseRW: rollback(tx)
                AnalyzeEndpoint->>DatabaseRO: begin()
                DatabaseRO-->>AnalyzeEndpoint: ro_tx
                AnalyzeEndpoint->>EIService: find_active_job(sbom_id, vulnerability_id, ro_tx)
                EIService-->>AnalyzeEndpoint: Option<Job>
                alt [Some(job)]
                    AnalyzeEndpoint-->>Client: 200 AnalyzeResponse
                else [None]
                    AnalyzeEndpoint-->>Client: e (mapped to HTTP)
                end
            end
        end
    end
Loading

Sequence diagram for Error to AnalysisError conversion with BadRequest

sequenceDiagram
    participant Runner
    participant AnalysisError

    Runner->>AnalysisError: From<crate::Error>::from(Error::BadRequest)
    AnalysisError-->>Runner: AnalysisError::Permanent

    Runner->>AnalysisError: From<crate::Error>::from(Error::Database)
    AnalysisError-->>Runner: AnalysisError::Retryable

    Runner->>AnalysisError: From<crate::Error>::from(Error::Unavailable)
    AnalysisError-->>Runner: AnalysisError::Permanent
Loading

File-Level Changes

Change Details Files
Add endpoint-level validation to reject empty or whitespace-only vulnerability_id and return HTTP 400.
  • Trim vulnerability_id from the request body before use.
  • Return a BadRequest error when the trimmed vulnerability_id is empty.
  • Pass the trimmed vulnerability_id into create_job and find_active_job instead of the raw body field.
modules/exploit-intelligence/src/endpoints/mod.rs
Add a test to verify that blank vulnerability_id values are rejected with 400 Bad Request on the analyze endpoint.
  • Initialize a test app wired with exploit-intelligence analyze endpoint and test services.
  • Post analyze requests with empty and whitespace-only vulnerability_id values.
  • Assert that the responses for these requests have HTTP status 400.
modules/exploit-intelligence/src/endpoints/test.rs
Introduce a BadRequest error variant in exploit-intelligence errors and ensure it is surfaced as HTTP 400 responses.
  • Add BadRequest(String) variant to the exploit-intelligence Error enum.
  • Map BadRequest to an HTTP 400 response containing ErrorInformation with type "BadRequest" and the provided message.
modules/exploit-intelligence/src/error.rs
Update the exploit-intelligence runner to treat BadRequest errors as permanent rather than retryable.
  • Extend AnalysisError conversion to classify BadRequest errors as Permanent.
  • Keep retryable classification only for database and generic Any errors.
modules/exploit-intelligence/src/runner/mod.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 2 issues, and left some high level feedback:

  • In Error::BadRequest’s ResponseError implementation you pass the raw message string to ErrorInformation::new rather than self, which is inconsistent with the other variants and may drop useful context; consider aligning this with the other error responses.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `Error::BadRequest`’s `ResponseError` implementation you pass the raw message string to `ErrorInformation::new` rather than `self`, which is inconsistent with the other variants and may drop useful context; consider aligning this with the other error responses.

## Individual Comments

### Comment 1
<location path="modules/exploit-intelligence/src/endpoints/test.rs" line_range="774-775" />
<code_context>
+            })
+            .to_request();
+
+        let resp = test::call_service(&app, req).await;
+        assert_eq!(resp.status(), 400, "expected 400 for vulnerability_id={blank:?}");
+    }
+
</code_context>
<issue_to_address>
**suggestion (testing):** Assert the error response payload, not just the HTTP status, to lock in the BadRequest contract.

Given that `Error::BadRequest` now returns a structured `ErrorInformation`, consider deserializing the response body and asserting its `kind` (e.g. `"BadRequest"`) and message (e.g. `"vulnerability_id must not be empty"`). This will ensure the test validates the full error contract, not just the status code.

Suggested implementation:

```rust
    for blank in ["", "   "] {
        let req = TestRequest::post()
            .uri("/v3/exploit-intelligence/analyze")
            .set_json(AnalyzeRequest {
                sbom_id: Uuid::now_v7(),
                vulnerability_id: blank.to_string(),
            })
            .to_request();

        let resp = test::call_service(&app, req).await;
        assert_eq!(
            resp.status(),
            StatusCode::BAD_REQUEST,
            "expected 400 for vulnerability_id={blank:?}"
        );

        let body = actix_web::body::to_bytes(resp.into_body()).await?;
        let error: ErrorInformation = serde_json::from_slice(&body)?;

        assert_eq!(
            error.kind,
            "BadRequest",
            "unexpected error kind for vulnerability_id={blank:?}"
        );
        assert_eq!(
            error.message,
            "vulnerability_id must not be empty",
            "unexpected error message for vulnerability_id={blank:?}"
        );
    }


```

1. Ensure the test module imports the `StatusCode` and `ErrorInformation` types, and `serde_json`:
   - Add `use actix_web::http::StatusCode;`
   - Add the appropriate import for `ErrorInformation` (e.g. `use trustify_error::ErrorInformation;` or wherever it is defined).
   - Add `use serde_json;` if not already present.
2. Confirm that `ErrorInformation` has `kind` and `message` fields with the expected types; adjust the field names in the assertions if the actual struct uses different names (e.g. `error_type`, `detail`, etc.).
3. If the error payload nests these fields (e.g. `{ "error": { "kind": "...", "message": "..." } }`), update the deserialization and assertions accordingly to match the actual JSON structure.
</issue_to_address>

### Comment 2
<location path="modules/exploit-intelligence/src/endpoints/test.rs" line_range="746-747" />
<code_context>
     Ok(())
 }

+/// Verifies that POSTing an analyze request with an empty or whitespace-only
+/// vulnerability_id returns 400 Bad Request.
+#[test_context(TrustifyContext)]
+#[test(actix_web::test)]
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests that verify blank vulnerability_id errors are treated as permanent (non-retryable) analysis errors.

This test only checks that the endpoint returns HTTP 400, but doesn’t verify that `Error::BadRequest` is treated as a permanent analysis error. Please add or update tests in the analysis/runner suite to cover the `BadRequest` variant and assert it maps to `AnalysisError::Permanent`, so the background analysis behavior matches the bugfix intent.
</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.

Comment thread modules/exploit-intelligence/src/endpoints/test.rs Outdated
Comment thread modules/exploit-intelligence/src/endpoints/test.rs
@i386x

i386x commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Looks good.

@Strum355
Strum355 added this pull request to the merge queue Aug 12, 2026

@rh-jfuller rh-jfuller 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.

LGTM

Merged via the queue into guacsec:main with commit 2b2a763 Aug 12, 2026
13 of 15 checks passed
@Strum355
Strum355 deleted the TC-5535 branch August 12, 2026 09:38
@github-project-automation github-project-automation Bot moved this to Done in Trustify Aug 12, 2026
@trustify-ci-bot

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants