Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d3c5e1e
feat(exploit-intelligence): add orchestration module with API endpoints
Strum355 Jun 11, 2026
67197b2
fix(exploit-intelligence): add polling timeout, job deduplication, an…
Strum355 Jun 12, 2026
409e0ed
fix(exploit-intelligence): expose finding as typed enum in API response
Strum355 Jun 22, 2026
256cd62
fix(exploit-intelligence): replace fragile JSON pointer parsing with …
Strum355 Jun 22, 2026
295ed3c
fix(exploit-intelligence): configure HTTP client with timeouts and co…
Strum355 Jun 22, 2026
16c38e1
fix(exploit-intelligence): add OIDC client credentials auth with toke…
Strum355 Jun 22, 2026
1a070e8
fix(exploit-intelligence): add retry logic with exponential backoff
Strum355 Jun 22, 2026
2cc075b
fix(exploit-intelligence): recover orphaned jobs after server restart
Strum355 Jun 22, 2026
280ea93
feat(exploit-intelligence): add multi-component SPDX product flow
Strum355 Jun 25, 2026
4cbbe0d
feat(exploit-intelligence): add separate UI URL config for report dee…
Strum355 Jun 25, 2026
22ee0bf
fix(exploit-intelligence): address review findings across module
Strum355 Jun 26, 2026
2981c8c
fix(exploit-intelligence): rename component_purl to component_ref and…
Strum355 Jun 30, 2026
63fbdea
fix(exploit-intelligence): case-insensitive finding status comparison
Strum355 Jul 7, 2026
0004082
feat(exploit-intelligence): adapt orchestration to unified entity schema
Strum355 Jul 7, 2026
d45f56c
refactor(exploit-intelligence): use backon crate for retry and polling
Strum355 Jul 9, 2026
880435a
refactor(exploit-intelligence): move business logic from endpoints to…
Strum355 Jul 13, 2026
e308729
refactor(exploit-intelligence): make retry/poll values configurable w…
Strum355 Jul 13, 2026
49065f5
feat(exploit-intelligence): add heartbeat locking for multi-instance …
Strum355 Jul 14, 2026
8690e40
fix(exploit-intelligence): replace expect() with error propagation in…
Strum355 Jul 14, 2026
aca7b8e
refactor(exploit-intelligence): replace custom OIDC with existing tru…
Strum355 Jul 15, 2026
f272d7d
refactor(exploit-intelligence): replace heartbeat with PostgreSQL job…
Strum355 Jul 15, 2026
fc2f7b4
refactor(exploit-intelligence): address code style review comments
Strum355 Jul 15, 2026
4f76621
chore: bump migration num and address more comments
Strum355 Jul 16, 2026
b054110
fix(exploit-intelligence): use ActiveModel for single-row updates and…
Strum355 Jul 16, 2026
e51e839
feat(exploit-intelligence): add excluded component tracking, improve …
Strum355 Jul 17, 2026
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
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ aws-config = { version = "1.8.14", features = ["behavior-version-latest"] }
aws-sdk-s3 = { version = "1.124.0", default-features = false, features = ["behavior-version-latest", "default-https-client", "http-1x", "rt-tokio", "sigv4a"] }
aws-smithy-http-client = { version = "1.2.0", features = ["rustls-aws-lc"] }
aws-smithy-types = { version = "1.4.5" }
backon = "1.6"
base16ct = "1"
base64 = "0.22"
build-info = "0.0.44"
Expand Down
5 changes: 5 additions & 0 deletions common/auth/src/permission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ permission! {

#[strum(serialize = "ai")]
Ai,

#[strum(serialize = "create.exploitIntelligence")]
CreateExploitIntelligence,
#[strum(serialize = "read.exploitIntelligence")]
ReadExploitIntelligence,
}
}

Expand Down
13 changes: 13 additions & 0 deletions docs/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@
| `UI_ISSUER_URL` | Issuer URL used by the UI | `http://localhost:8090/realms/trustify` |
| `UI_LOAD_USER` | Whether to load user info | `true` |
| `UI_SCOPE` | Scopes to request | `openid` |
| `EXPLOIT_INTELLIGENCE_URL` | Base URL of the Exploit Intelligence client service | |
| `EXPLOIT_INTELLIGENCE_UI_URL` | Base URL of the EI web UI for deep-linking to reports | |
| `EXPLOIT_INTELLIGENCE_POLL_INTERVAL`| Polling interval for EI analysis completion (humantime) | `30s` |
| `EXPLOIT_INTELLIGENCE_MAX_POLL_DURATION`| Maximum duration before EI polling is considered timed out (humantime) | `30m` |
| `EXPLOIT_INTELLIGENCE_UPLOAD_MAX_RETRIES`| Maximum number of upload retry attempts | `3` |
| `EXPLOIT_INTELLIGENCE_UPLOAD_RETRY_DELAY`| Initial delay between upload retries (humantime, exponential backoff) | `1s` |
| `EXPLOIT_INTELLIGENCE_MAX_CONSECUTIVE_POLL_FAILURES`| Maximum consecutive poll failures before giving up | `5` |
| `EXPLOIT_INTELLIGENCE_AUTH_TOKEN` | Static authentication token for the Exploit Intelligence service (used when OIDC is not configured) | |
| `EXPLOIT_INTELLIGENCE_OIDC_ISSUER_URL` | OIDC issuer URL for EI service authentication (client credentials flow with discovery) | |
| `EXPLOIT_INTELLIGENCE_OIDC_CLIENT_ID` | OIDC client ID for EI service authentication | |
| `EXPLOIT_INTELLIGENCE_OIDC_CLIENT_SECRET`| OIDC client secret for EI service authentication | |
| `EXPLOIT_INTELLIGENCE_OIDC_REFRESH_BEFORE`| Duration an EI access token must still be valid before requesting a new one | `30s` |
| `EXPLOIT_INTELLIGENCE_OIDC_TLS_INSECURE`| Allow insecure TLS connections with the EI OIDC issuer | `false` |

## Data Migration

Expand Down
7 changes: 7 additions & 0 deletions entity/src/exploit_intelligence_job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ pub struct Model {
/// Expected number of components in the product.
pub total_components: Option<i32>,

/// Visibility timeout for the job queue pattern. When set, the job is
/// invisible to other workers until this timestamp is reached.
pub visible_at: Option<OffsetDateTime>,

/// Number of times this job has been dequeued for processing.
pub retry_count: i32,

/// Timestamp when the job was created.
pub created: OffsetDateTime,

Expand Down
4 changes: 4 additions & 0 deletions entity/src/exploit_intelligence_job_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ pub struct Model {
/// FK to advisory — set when a VEX document is ingested from the result.
pub advisory_id: Option<Uuid>,

/// Whether this component was excluded by the EI service (e.g.,
/// unsupported ecosystem) rather than genuinely failing.
pub excluded: bool,

/// Error details for failed analyses.
pub error_message: Option<String>,

Expand Down
4 changes: 2 additions & 2 deletions migration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ mod m0002210_sbom_node_name_index;
mod m0002220_drop_qualified_purl_gist_indexes;
mod m0002230_sle_license_id_index;
mod m0002240_product_version_sbom_index;
mod m0002230_create_exploit_intelligence_job;
mod m0002250_create_exploit_intelligence_job;

pub trait MigratorExt: Send {
fn build_migrations() -> Migrations;
Expand Down Expand Up @@ -148,7 +148,7 @@ impl MigratorExt for Migrator {
.normal(m0002220_drop_qualified_purl_gist_indexes::Migration)
.normal(m0002230_sle_license_id_index::Migration)
.normal(m0002240_product_version_sbom_index::Migration)
.normal(m0002230_create_exploit_intelligence_job::Migration)
.normal(m0002250_create_exploit_intelligence_job::Migration)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ impl MigrationTrait for Migration {
.col(ColumnDef::new(ExploitIntelligenceJob::ErrorMessage).string())
.col(ColumnDef::new(ExploitIntelligenceJob::ProductId).string())
.col(ColumnDef::new(ExploitIntelligenceJob::TotalComponents).integer())
.col(
ColumnDef::new(ExploitIntelligenceJob::VisibleAt)
.timestamp_with_time_zone()
.null(),
)
.col(
ColumnDef::new(ExploitIntelligenceJob::RetryCount)
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(ExploitIntelligenceJob::Created)
.timestamp_with_time_zone()
Expand Down Expand Up @@ -102,6 +113,18 @@ impl MigrationTrait for Migration {
)
.await?;

// Partial unique index to prevent duplicate active jobs for the same
// (sbom_id, vulnerability_id) pair. SeaORM's Index builder does not
// support partial indexes, so we use raw SQL.
manager
.get_connection()
.execute_unprepared(
"CREATE UNIQUE INDEX IF NOT EXISTS idx_ei_job_active_dedup \
ON exploit_intelligence_job (sbom_id, vulnerability_id) \
WHERE status IN ('pending', 'running')",
)
.await?;

manager
.create_table(
Table::create()
Expand Down Expand Up @@ -138,6 +161,12 @@ impl MigrationTrait for Migration {
.custom(EiFinding::Table),
)
.col(ColumnDef::new(ExploitIntelligenceJobComponent::AdvisoryId).uuid())
.col(
ColumnDef::new(ExploitIntelligenceJobComponent::Excluded)
.boolean()
.not_null()
.default(false),
)
.col(ColumnDef::new(ExploitIntelligenceJobComponent::ErrorMessage).string())
.col(
ColumnDef::new(ExploitIntelligenceJobComponent::Created)
Expand Down Expand Up @@ -222,6 +251,11 @@ impl MigrationTrait for Migration {
)
.await?;

manager
.get_connection()
.execute_unprepared("DROP INDEX IF EXISTS idx_ei_job_active_dedup")
.await?;

manager
.drop_index(
Index::drop()
Expand Down Expand Up @@ -287,6 +321,8 @@ enum ExploitIntelligenceJob {
ErrorMessage,
ProductId,
TotalComponents,
VisibleAt,
RetryCount,
Created,
Updated,
}
Expand All @@ -301,6 +337,7 @@ enum ExploitIntelligenceJobComponent {
Status,
Finding,
AdvisoryId,
Excluded,
ErrorMessage,
Created,
Updated,
Expand Down
6 changes: 5 additions & 1 deletion modules/fundamental/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ trustify-query-derive = { workspace = true }

actix-http = { workspace = true }
actix-web = { workspace = true }
backon = { workspace = true }
bytes = { workspace = true }
anyhow = { workspace = true }
async-trait = { workspace = true }
cpe = { workspace = true }
Expand All @@ -30,6 +32,7 @@ futures-util = { workspace = true }
isx = { workspace = true }
itertools = { workspace = true }
log = { workspace = true }
reqwest = { workspace = true, features = ["form", "json", "multipart"] }
sanitize-filename = { workspace = true }
sea-orm = { workspace = true }
sea-query = { workspace = true }
Expand All @@ -43,8 +46,10 @@ tar = { workspace = true }
thiserror = { workspace = true }
time = { workspace = true }
tokio = { workspace = true, features = ["full"] }
tokio-util = { workspace = true, features = ["full"] }
tracing = { workspace = true }
tracing-futures = { workspace = true, features = ["futures-03"] }
urlencoding = { workspace = true }
utoipa = { workspace = true, features = ["actix_extras", "uuid", "time"] }
utoipa-actix-web = { workspace = true }
uuid = { workspace = true }
Expand Down Expand Up @@ -86,7 +91,6 @@ strum = { workspace = true }
tar = { workspace = true }
test-context = { workspace = true }
test-log = { workspace = true, features = ["log", "trace"] }
tokio-util = { workspace = true }
tracing = { workspace = true, features = ["std"]}
tracing-core = { workspace = true }
tracing-opentelemetry = { workspace = true }
Expand Down
18 changes: 16 additions & 2 deletions modules/fundamental/src/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ use trustify_module_ingestor::service::IngestorService;
use trustify_module_storage::service::dispatch::DispatchBackend;
use utoipa::{IntoParams, ToSchema};

use crate::exploit_intelligence::service::{ExploitIntelligenceConfig, ExploitIntelligenceService};

#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub struct Config {
pub sbom_upload_limit: usize,
pub advisory_upload_limit: usize,
pub max_group_name_length: usize,
}

#[allow(clippy::too_many_arguments)]
pub fn configure(
svc: &mut utoipa_actix_web::service_config::ServiceConfig,
config: Config,
Expand All @@ -21,9 +24,10 @@ pub fn configure(
storage: impl Into<DispatchBackend>,
analysis: AnalysisService,
cache: PaginationCache,
ei_config: Option<ExploitIntelligenceConfig>,
) {
let ingestor_service = IngestorService::new(Graph::new(), storage, Some(analysis));
svc.app_data(web::Data::new(ingestor_service));
svc.app_data(web::Data::new(ingestor_service.clone()));

crate::advisory::endpoints::configure(
svc,
Expand All @@ -45,7 +49,17 @@ pub fn configure(
);
crate::vulnerability::endpoints::configure(svc, db_ro.clone(), cache.clone());
crate::weakness::endpoints::configure(svc, db_ro.clone(), cache.clone());
crate::sbom_group::endpoints::configure(svc, db_rw, db_ro, config.max_group_name_length, cache);
crate::sbom_group::endpoints::configure(
svc,
db_rw.clone(),
db_ro.clone(),
config.max_group_name_length,
cache,
);

let ei_service = ExploitIntelligenceService::new(ei_config)
.unwrap_or_else(|e| panic!("failed to create ExploitIntelligenceService: {e}"));
crate::exploit_intelligence::endpoints::configure(svc, db_rw, db_ro, ei_service);
}

#[derive(Clone, Debug, PartialEq, Eq, Default, ToSchema, serde::Deserialize, IntoParams)]
Expand Down
19 changes: 19 additions & 0 deletions modules/fundamental/src/exploit_intelligence/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use std::sync::Arc;
use trustify_auth::client::{BearerTokenProvider, OpenIdTokenProviderConfig, TokenProvider};

/// Build an EI [`TokenProvider`] from OIDC configuration, falling back to a
/// static token if OIDC is not configured.
///
/// Returns `None` when neither OIDC nor a static token is available.
pub async fn build_provider(
oidc_config: Option<OpenIdTokenProviderConfig>,
static_token: Option<String>,
) -> anyhow::Result<Option<Arc<dyn TokenProvider>>> {
match oidc_config {
Some(config) => Ok(Some(
OpenIdTokenProviderConfig::new_provider(Some(config)).await?,
)),
None => Ok(static_token
.map(|t| -> Arc<dyn TokenProvider> { Arc::new(BearerTokenProvider { token: t }) })),
}
}
Loading
Loading