feat(oscal): live assessment endpoint POST /api/v1/oscal/assess (#212)
CI / Check (push) Skipped
CI / Detect Changes (push) Successful in 2s
CI / Deploy Docs (push) Skipped
CI / Deploy Agent (push) Successful in 3m43s
CI / Deploy Dashboard (push) Successful in 2m36s
CI / Deploy MCP (push) Successful in 1m48s

This commit was merged in pull request #212.
This commit is contained in:
2026-07-20 19:13:47 +00:00
parent ca4e31cb65
commit 5285fb67ae
7 changed files with 133 additions and 1 deletions
+1
View File
@@ -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;
@@ -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()
}
+1
View File
@@ -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",