cdfbb62f9d
CI / Check (pull_request) Successful in 8m9s
CI / Detect Changes (pull_request) Has been skipped
CI / Deploy Agent (pull_request) Has been skipped
CI / Deploy Dashboard (pull_request) Has been skipped
CI / Deploy Docs (pull_request) Has been skipped
CI / Deploy MCP (pull_request) Has been skipped
Builds on PR M7.2-A. Every HTTP handler in compliance-agent/src/api/
now takes a TenantCtx extractor and pulls a tenant-scoped Database
from agent.db_pool.for_tenant(&ctx). The query bodies are unchanged —
`db.findings().find(doc! {...})` reads from the tenant's own physical
database, so the filter doc cannot leak data across tenants because
the wrong tenant's data is literally on a different db handle.
Changes
- New `dto::tenant_db(&agent, &tenant) -> Result<Database, StatusCode>`
helper. Every migrated handler calls it at the top of the body
instead of `let db = &agent.db;`. 500 on the rare pool failure;
4xx auth failures are already handled by the M7.1 status gate.
- New `api::server::inject_dev_tenant` middleware mounted only when
Keycloak is NOT configured. Synthesizes a TenantContext with
tenant_id = $DEV_TENANT_ID (default `dev`) so `cargo run` against
a bare Mongo + no KC still serves the API. Logged loudly as
"DO NOT use in any environment with real customer data".
- Test harness: TestServer mounts inject_dev_tenant so existing E2E
tests reach handlers; cleanup() now drops every <db_name>_*
per-tenant database, not just the legacy <db_name>.
Files migrated (handler count, all pass `cargo build`):
- chat.rs (3) — also rewires RagPipeline + EmbeddingStore to the
tenant DB's inner() so vector search is per-tenant
- dast.rs (5)
- findings.rs (5)
- graph.rs (7) — also rewires GraphStore inside trigger_build's
spawn to the tenant DB
- health.rs (1) — stats_overview migrated; public /health stays
un-scoped
- issues.rs (1)
- notifications.rs (5)
- pentest_handlers/session.rs (12) — both wizard + legacy paths,
plus pause/resume/stop/get_attack_chain/get_messages/
get_session_findings/lookup_repo. PentestOrchestrator now gets
the tenant DB clone in its spawn.
- pentest_handlers/export.rs (1) — fans out across sessions,
attack_chain_nodes, dast_findings, findings, sbom_entries,
graph_nodes from a single tenant_db acquisition
- pentest_handlers/stats.rs (1)
- pentest_handlers/stream.rs (1) — SSE handler verifies session
via the tenant DB before subscribing
- repos.rs (6)
- sbom.rs (5)
- scans.rs (1)
help_chat.rs has no DB queries and was skipped.
Test plan
- cargo fmt --all clean
- cargo clippy --workspace --exclude compliance-dashboard
-- -D warnings clean
- cargo test -p compliance-core --lib — 7 pass
- cargo test -p compliance-agent --lib — 228 pass
- cargo test -p compliance-agent --test tenant_isolation — 5 pass
(driver-level isolation still holds post-handler migration)
- cargo test -p compliance-agent --test tenant_status_middleware
— 6 pass
What's not yet migrated (PR-C / PR-D)
- scheduler.rs (6 sites), pipeline/orchestrator.rs (14),
pentest/orchestrator.rs (13), webhooks (gitea/github/gitlab),
trackers/jira.rs, pipeline/dedup.rs etc. — background paths
without a JWT-derived tenant context.
- agent.db is still in the ComplianceAgent struct as a transitional
handle for those paths. PR-D removes it once PR-C migrates the
background paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
106 lines
3.1 KiB
Rust
106 lines
3.1 KiB
Rust
use std::sync::Arc;
|
|
|
|
use axum::extract::Extension;
|
|
use axum::http::StatusCode;
|
|
use axum::Json;
|
|
use mongodb::bson::doc;
|
|
|
|
use compliance_core::models::pentest::*;
|
|
use compliance_core::tenant_ctx::TenantCtx;
|
|
|
|
use crate::agent::ComplianceAgent;
|
|
|
|
use super::super::dto::{collect_cursor_async, tenant_db, ApiResponse};
|
|
|
|
type AgentExt = Extension<Arc<ComplianceAgent>>;
|
|
|
|
/// GET /api/v1/pentest/stats — Aggregated pentest statistics
|
|
#[tracing::instrument(skip_all)]
|
|
pub async fn pentest_stats(
|
|
Extension(agent): AgentExt,
|
|
tenant: TenantCtx,
|
|
) -> Result<Json<ApiResponse<PentestStats>>, StatusCode> {
|
|
let db = tenant_db(&agent, &tenant).await?;
|
|
let db = &db;
|
|
|
|
let running_sessions = db
|
|
.pentest_sessions()
|
|
.count_documents(doc! { "status": "running" })
|
|
.await
|
|
.unwrap_or(0) as u32;
|
|
|
|
// Count DAST findings from pentest sessions
|
|
let total_vulnerabilities = db
|
|
.dast_findings()
|
|
.count_documents(doc! { "session_id": { "$exists": true, "$ne": null } })
|
|
.await
|
|
.unwrap_or(0) as u32;
|
|
|
|
// Aggregate tool invocations from all sessions
|
|
let sessions: Vec<PentestSession> = match db.pentest_sessions().find(doc! {}).await {
|
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
|
Err(_) => Vec::new(),
|
|
};
|
|
|
|
let total_tool_invocations: u32 = sessions.iter().map(|s| s.tool_invocations).sum();
|
|
let total_successes: u32 = sessions.iter().map(|s| s.tool_successes).sum();
|
|
let tool_success_rate = if total_tool_invocations == 0 {
|
|
100.0
|
|
} else {
|
|
(total_successes as f64 / total_tool_invocations as f64) * 100.0
|
|
};
|
|
|
|
// Severity distribution from pentest-related DAST findings
|
|
let critical = db
|
|
.dast_findings()
|
|
.count_documents(
|
|
doc! { "session_id": { "$exists": true, "$ne": null }, "severity": "critical" },
|
|
)
|
|
.await
|
|
.unwrap_or(0) as u32;
|
|
let high = db
|
|
.dast_findings()
|
|
.count_documents(
|
|
doc! { "session_id": { "$exists": true, "$ne": null }, "severity": "high" },
|
|
)
|
|
.await
|
|
.unwrap_or(0) as u32;
|
|
let medium = db
|
|
.dast_findings()
|
|
.count_documents(
|
|
doc! { "session_id": { "$exists": true, "$ne": null }, "severity": "medium" },
|
|
)
|
|
.await
|
|
.unwrap_or(0) as u32;
|
|
let low = db
|
|
.dast_findings()
|
|
.count_documents(doc! { "session_id": { "$exists": true, "$ne": null }, "severity": "low" })
|
|
.await
|
|
.unwrap_or(0) as u32;
|
|
let info = db
|
|
.dast_findings()
|
|
.count_documents(
|
|
doc! { "session_id": { "$exists": true, "$ne": null }, "severity": "info" },
|
|
)
|
|
.await
|
|
.unwrap_or(0) as u32;
|
|
|
|
Ok(Json(ApiResponse {
|
|
data: PentestStats {
|
|
running_sessions,
|
|
total_vulnerabilities,
|
|
total_tool_invocations,
|
|
tool_success_rate,
|
|
severity_distribution: SeverityDistribution {
|
|
critical,
|
|
high,
|
|
medium,
|
|
low,
|
|
info,
|
|
},
|
|
},
|
|
total: None,
|
|
page: None,
|
|
}))
|
|
}
|