feat(agent): live OSCAL assessment endpoint POST /api/v1/oscal/assess
CI / Check (push) Skipped
CI / Check (pull_request) Successful in 6m5s
CI / Detect Changes (pull_request) Skipped
CI / Deploy Agent (pull_request) Skipped
CI / Deploy Dashboard (pull_request) Skipped
CI / Deploy Docs (pull_request) Skipped
CI / Deploy MCP (pull_request) Skipped
CI / Check (push) Skipped
CI / Check (pull_request) Successful in 6m5s
CI / Detect Changes (pull_request) Skipped
CI / Deploy Agent (pull_request) Skipped
CI / Deploy Dashboard (pull_request) Skipped
CI / Deploy Docs (pull_request) Skipped
CI / Deploy MCP (pull_request) Skipped
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ca4e31cb65
commit
2d20ad7a6e
@@ -10,6 +10,7 @@ pub mod issues;
|
|||||||
pub mod mcp_tokens;
|
pub mod mcp_tokens;
|
||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
pub mod onboarding;
|
pub mod onboarding;
|
||||||
|
pub mod oscal;
|
||||||
pub mod pentest_handlers;
|
pub mod pentest_handlers;
|
||||||
pub use pentest_handlers as pentest;
|
pub use pentest_handlers as pentest;
|
||||||
pub mod sbom;
|
pub mod sbom;
|
||||||
|
|||||||
@@ -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<ComplianceFramework>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `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<AssessRequest>,
|
||||||
|
) -> 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<Finding> = 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()
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ use crate::api::handlers;
|
|||||||
pub fn build_router() -> Router {
|
pub fn build_router() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/api/v1/health", get(handlers::health))
|
.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/stats/overview", get(handlers::stats_overview))
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/settings/ssh-public-key",
|
"/api/v1/settings/ssh-public-key",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use compliance_core::config::PlcRuntimeConfig;
|
use compliance_core::config::{BreakpilotConfig, PlcRuntimeConfig};
|
||||||
use compliance_core::AgentConfig;
|
use compliance_core::AgentConfig;
|
||||||
use secrecy::SecretString;
|
use secrecy::SecretString;
|
||||||
|
|
||||||
@@ -66,6 +66,7 @@ pub fn load_config() -> Result<AgentConfig, AgentError> {
|
|||||||
tenant_registry_url: env_var_opt("TENANT_REGISTRY_URL"),
|
tenant_registry_url: env_var_opt("TENANT_REGISTRY_URL"),
|
||||||
plc_runtime: load_plc_runtime_config(),
|
plc_runtime: load_plc_runtime_config(),
|
||||||
werkbank_runner_token: env_secret_opt("WERKBANK_RUNNER_TOKEN"),
|
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),
|
.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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -344,6 +344,7 @@ mod tests {
|
|||||||
tenant_registry_url: None,
|
tenant_registry_url: None,
|
||||||
plc_runtime: compliance_core::PlcRuntimeConfig::default(),
|
plc_runtime: compliance_core::PlcRuntimeConfig::default(),
|
||||||
werkbank_runner_token: None,
|
werkbank_runner_token: None,
|
||||||
|
breakpilot: compliance_core::config::BreakpilotConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ pub fn dev_config(mongodb_uri: String, db_name: String) -> AgentConfig {
|
|||||||
tenant_registry_url: None,
|
tenant_registry_url: None,
|
||||||
plc_runtime: compliance_core::PlcRuntimeConfig::default(),
|
plc_runtime: compliance_core::PlcRuntimeConfig::default(),
|
||||||
werkbank_runner_token: Some(SecretString::from(TEST_RUNNER_TOKEN.to_string())),
|
werkbank_runner_token: Some(SecretString::from(TEST_RUNNER_TOKEN.to_string())),
|
||||||
|
breakpilot: compliance_core::config::BreakpilotConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,33 @@ pub struct AgentConfig {
|
|||||||
/// jobs — NOT a Keycloak JWT, since a runner acts across tenants. When
|
/// jobs — NOT a Keycloak JWT, since a runner acts across tenants. When
|
||||||
/// `None`, those endpoints are not mounted at all.
|
/// `None`, those endpoints are not mounted at all.
|
||||||
pub werkbank_runner_token: Option<SecretString>,
|
pub werkbank_runner_token: Option<SecretString>,
|
||||||
|
/// 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<String>,
|
||||||
|
/// Optional bearer token for the catalog endpoint.
|
||||||
|
pub token: Option<SecretString>,
|
||||||
|
/// 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).
|
/// Configuration for the ephemeral soft-PLC "provision-and-test" path (#183).
|
||||||
|
|||||||
Reference in New Issue
Block a user