From 2d20ad7a6ee20715f073d577654f365d059526d7 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Mon, 20 Jul 2026 19:04:03 +0200 Subject: [PATCH] feat(agent): live OSCAL assessment endpoint POST /api/v1/oscal/assess Wires ingest (OscalControlsProvider) + assessment (assess) behind an agent endpoint: pulls the catalog for the target's frameworks (default CRA), loads the target's findings from the tenant DB, and returns an OSCAL assessment-results document linking findings to controls. - BreakpilotConfig (base_url/token/snapshot_dir) from BREAKPILOT_* env; endpoint returns 503 when base_url is unset - handlers::oscal::assess_target on the authenticated (tenant-scoped) router - test helpers updated for the new config field clippy -D warnings + fmt + CI test cmd (287+47+27) clean. Co-Authored-By: Claude Opus 4.8 --- compliance-agent/src/api/handlers/mod.rs | 1 + compliance-agent/src/api/handlers/oscal.rs | 89 ++++++++++++++++++++++ compliance-agent/src/api/routes.rs | 1 + compliance-agent/src/config.rs | 14 +++- compliance-agent/src/pentest/cleanup.rs | 1 + compliance-agent/tests/common/mod.rs | 1 + compliance-core/src/config.rs | 27 +++++++ 7 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 compliance-agent/src/api/handlers/oscal.rs diff --git a/compliance-agent/src/api/handlers/mod.rs b/compliance-agent/src/api/handlers/mod.rs index c69fd18..5406c78 100644 --- a/compliance-agent/src/api/handlers/mod.rs +++ b/compliance-agent/src/api/handlers/mod.rs @@ -10,6 +10,7 @@ pub mod issues; pub mod mcp_tokens; pub mod notifications; pub mod onboarding; +pub mod oscal; pub mod pentest_handlers; pub use pentest_handlers as pentest; pub mod sbom; diff --git a/compliance-agent/src/api/handlers/oscal.rs b/compliance-agent/src/api/handlers/oscal.rs new file mode 100644 index 0000000..4030431 --- /dev/null +++ b/compliance-agent/src/api/handlers/oscal.rs @@ -0,0 +1,89 @@ +//! OSCAL assessment endpoint. +//! +//! Assesses a target's findings against the breakpilot-compliance control +//! catalog and returns a standard OSCAL assessment-results document. Ties +//! together the ingest provider ([`OscalControlsProvider`]) and the assessment +//! emitter (`compliance_core::models::oscal_assessment`). + +use axum::extract::Extension; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::Json; +use mongodb::bson::doc; +use serde::Deserialize; + +use compliance_core::models::onboarding::ComplianceFramework; +use compliance_core::models::oscal_assessment::{assess, ControlLinker}; +use compliance_core::models::Finding; +use compliance_core::tenant_ctx::TenantCtx; + +use super::dto::{collect_cursor_async, tenant_db, AgentExt}; +use crate::controls::OscalControlsProvider; + +#[derive(Debug, Deserialize)] +pub struct AssessRequest { + /// The target / repo id whose findings are assessed. + pub target_id: String, + /// Frameworks to assess against; defaults to `[Cra]` when empty. + #[serde(default)] + pub frameworks: Vec, +} + +/// `POST /api/v1/oscal/assess` — pull the catalog(s), load the target's findings, +/// and emit an OSCAL assessment-results document linking findings to controls. +pub async fn assess_target( + Extension(agent): AgentExt, + tenant: TenantCtx, + Json(req): Json, +) -> Response { + let cfg = &agent.config.breakpilot; + let Some(base_url) = cfg.base_url.clone() else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + "breakpilot base URL not configured (set BREAKPILOT_BASE_URL)", + ) + .into_response(); + }; + + let frameworks = if req.frameworks.is_empty() { + vec![ComplianceFramework::Cra] + } else { + req.frameworks.clone() + }; + + let db = match tenant_db(&agent, &tenant).await { + Ok(db) => db, + Err(code) => return code.into_response(), + }; + + let findings: Vec = match db.findings().find(doc! { "repo_id": &req.target_id }).await + { + Ok(cursor) => collect_cursor_async(cursor).await, + Err(e) => { + tracing::warn!(error = %e, "failed to load findings for OSCAL assessment"); + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + }; + + let provider = OscalControlsProvider::new( + agent.http.clone(), + base_url, + cfg.token.clone(), + &cfg.snapshot_dir, + ); + let mut controls = Vec::new(); + for framework in &frameworks { + match provider.load(*framework).await { + Ok(document) => controls.extend(document.to_controls()), + Err(e) => tracing::warn!(?framework, error = %e, "OSCAL catalog load failed"), + } + } + + let assessment = assess( + &controls, + &findings, + &ControlLinker::cra_seed(), + chrono::Utc::now(), + ); + Json(assessment).into_response() +} diff --git a/compliance-agent/src/api/routes.rs b/compliance-agent/src/api/routes.rs index 219760d..e9413dc 100644 --- a/compliance-agent/src/api/routes.rs +++ b/compliance-agent/src/api/routes.rs @@ -6,6 +6,7 @@ use crate::api::handlers; pub fn build_router() -> Router { Router::new() .route("/api/v1/health", get(handlers::health)) + .route("/api/v1/oscal/assess", post(handlers::oscal::assess_target)) .route("/api/v1/stats/overview", get(handlers::stats_overview)) .route( "/api/v1/settings/ssh-public-key", diff --git a/compliance-agent/src/config.rs b/compliance-agent/src/config.rs index 2878f3d..257f554 100644 --- a/compliance-agent/src/config.rs +++ b/compliance-agent/src/config.rs @@ -1,4 +1,4 @@ -use compliance_core::config::PlcRuntimeConfig; +use compliance_core::config::{BreakpilotConfig, PlcRuntimeConfig}; use compliance_core::AgentConfig; use secrecy::SecretString; @@ -66,6 +66,7 @@ pub fn load_config() -> Result { tenant_registry_url: env_var_opt("TENANT_REGISTRY_URL"), plc_runtime: load_plc_runtime_config(), werkbank_runner_token: env_secret_opt("WERKBANK_RUNNER_TOKEN"), + breakpilot: load_breakpilot_config(), }) } @@ -90,3 +91,14 @@ fn load_plc_runtime_config() -> PlcRuntimeConfig { .unwrap_or(d.openplc_password), } } + +/// Assemble the breakpilot OSCAL-catalog source from env, defaulting the snapshot +/// directory. A missing `BREAKPILOT_BASE_URL` leaves the controls provider off. +fn load_breakpilot_config() -> BreakpilotConfig { + let d = BreakpilotConfig::default(); + BreakpilotConfig { + base_url: env_var_opt("BREAKPILOT_BASE_URL"), + token: env_secret_opt("BREAKPILOT_TOKEN"), + snapshot_dir: env_var_opt("BREAKPILOT_SNAPSHOT_DIR").unwrap_or(d.snapshot_dir), + } +} diff --git a/compliance-agent/src/pentest/cleanup.rs b/compliance-agent/src/pentest/cleanup.rs index 0c3fe8b..1869581 100644 --- a/compliance-agent/src/pentest/cleanup.rs +++ b/compliance-agent/src/pentest/cleanup.rs @@ -344,6 +344,7 @@ mod tests { tenant_registry_url: None, plc_runtime: compliance_core::PlcRuntimeConfig::default(), werkbank_runner_token: None, + breakpilot: compliance_core::config::BreakpilotConfig::default(), } } diff --git a/compliance-agent/tests/common/mod.rs b/compliance-agent/tests/common/mod.rs index 57ccd5b..9b45a56 100644 --- a/compliance-agent/tests/common/mod.rs +++ b/compliance-agent/tests/common/mod.rs @@ -60,6 +60,7 @@ pub fn dev_config(mongodb_uri: String, db_name: String) -> AgentConfig { tenant_registry_url: None, plc_runtime: compliance_core::PlcRuntimeConfig::default(), werkbank_runner_token: Some(SecretString::from(TEST_RUNNER_TOKEN.to_string())), + breakpilot: compliance_core::config::BreakpilotConfig::default(), } } diff --git a/compliance-core/src/config.rs b/compliance-core/src/config.rs index 1389826..4eadad1 100644 --- a/compliance-core/src/config.rs +++ b/compliance-core/src/config.rs @@ -58,6 +58,33 @@ pub struct AgentConfig { /// jobs — NOT a Keycloak JWT, since a runner acts across tenants. When /// `None`, those endpoints are not mounted at all. pub werkbank_runner_token: Option, + /// Source for the OSCAL control catalog pulled from breakpilot-compliance + /// (drives the [`crate::traits::ControlsProvider`]). Disabled when + /// `base_url` is `None`. + pub breakpilot: BreakpilotConfig, +} + +/// Where to pull the OSCAL control catalog from breakpilot-compliance, and where +/// to snapshot it for deterministic / offline reuse. +#[derive(Debug, Clone)] +pub struct BreakpilotConfig { + /// Backend base URL (e.g. `http://backend-compliance:8002`). `None` disables + /// the OSCAL controls provider. + pub base_url: Option, + /// Optional bearer token for the catalog endpoint. + pub token: Option, + /// Directory for catalog snapshots. + pub snapshot_dir: String, +} + +impl Default for BreakpilotConfig { + fn default() -> Self { + Self { + base_url: None, + token: None, + snapshot_dir: "/data/compliance-scanner/oscal".to_string(), + } + } } /// Configuration for the ephemeral soft-PLC "provision-and-test" path (#183).