Add DAST, graph modules, toast notifications, and dashboard enhancements

Add DAST scanning and code knowledge graph features across the stack:
- compliance-dast and compliance-graph workspace crates
- Agent API handlers and routes for DAST targets/scans and graph builds
- Core models and traits for DAST and graph domains
- Dashboard pages for DAST targets/findings/overview and graph explorer/impact
- Toast notification system with auto-dismiss for async action feedback
- Button click animations and disabled states for better UX

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-03-04 13:53:50 +01:00
parent 03ee69834d
commit cea8f59e10
69 changed files with 8745 additions and 54 deletions

View File

@@ -0,0 +1,219 @@
use compliance_core::error::CoreError;
use compliance_core::models::dast::{DastEvidence, DastFinding, DastTarget, DastVulnType};
use compliance_core::models::Severity;
use compliance_core::traits::dast_agent::{DastAgent, DastContext};
use tracing::info;
/// Authentication bypass testing agent
pub struct AuthBypassAgent {
http: reqwest::Client,
}
impl AuthBypassAgent {
pub fn new(http: reqwest::Client) -> Self {
Self { http }
}
}
impl DastAgent for AuthBypassAgent {
fn name(&self) -> &str {
"auth_bypass"
}
async fn run(
&self,
target: &DastTarget,
context: &DastContext,
) -> Result<Vec<DastFinding>, CoreError> {
let mut findings = Vec::new();
let target_id = target
.id
.map(|oid| oid.to_hex())
.unwrap_or_else(|| "unknown".to_string());
// Test 1: Access protected endpoints without authentication
for endpoint in &context.endpoints {
if !endpoint.requires_auth {
continue;
}
// Try accessing without auth
let response = match self.http.get(&endpoint.url).send().await {
Ok(r) => r,
Err(_) => continue,
};
let status = response.status().as_u16();
// If we get 200 on a supposedly auth-required endpoint
if status == 200 {
let body = response.text().await.unwrap_or_default();
let snippet = body.chars().take(500).collect::<String>();
let evidence = DastEvidence {
request_method: "GET".to_string(),
request_url: endpoint.url.clone(),
request_headers: None,
request_body: None,
response_status: status,
response_headers: None,
response_snippet: Some(snippet),
screenshot_path: None,
payload: None,
response_time_ms: None,
};
let mut finding = DastFinding::new(
String::new(),
target_id.clone(),
DastVulnType::AuthBypass,
format!("Authentication bypass on {}", endpoint.url),
format!(
"Protected endpoint {} returned HTTP 200 without authentication credentials.",
endpoint.url
),
Severity::Critical,
endpoint.url.clone(),
"GET".to_string(),
);
finding.exploitable = true;
finding.evidence = vec![evidence];
finding.cwe = Some("CWE-287".to_string());
finding.remediation = Some(
"Ensure all protected endpoints validate authentication tokens. \
Implement server-side authentication checks that cannot be bypassed."
.to_string(),
);
findings.push(finding);
}
}
// Test 2: HTTP method tampering
let methods = ["PUT", "PATCH", "DELETE", "OPTIONS"];
for endpoint in &context.endpoints {
if endpoint.method != "GET" && endpoint.method != "POST" {
continue;
}
for method in &methods {
let response = match self
.http
.request(
reqwest::Method::from_bytes(method.as_bytes())
.unwrap_or(reqwest::Method::GET),
&endpoint.url,
)
.send()
.await
{
Ok(r) => r,
Err(_) => continue,
};
let status = response.status().as_u16();
// If a non-standard method returns 200 when it shouldn't
if status == 200 && *method == "DELETE" && !target.allow_destructive {
let evidence = DastEvidence {
request_method: method.to_string(),
request_url: endpoint.url.clone(),
request_headers: None,
request_body: None,
response_status: status,
response_headers: None,
response_snippet: None,
screenshot_path: None,
payload: None,
response_time_ms: None,
};
let mut finding = DastFinding::new(
String::new(),
target_id.clone(),
DastVulnType::AuthBypass,
format!("HTTP method tampering: {} accepted on {}", method, endpoint.url),
format!(
"Endpoint {} accepts {} requests which may bypass access controls.",
endpoint.url, method
),
Severity::Medium,
endpoint.url.clone(),
method.to_string(),
);
finding.evidence = vec![evidence];
finding.cwe = Some("CWE-288".to_string());
findings.push(finding);
}
}
}
// Test 3: Path traversal for auth bypass
let traversal_paths = [
"/../admin",
"/..;/admin",
"/%2e%2e/admin",
"/admin%00",
"/ADMIN",
"/Admin",
];
for path in &traversal_paths {
let test_url = format!("{}{}", target.base_url.trim_end_matches('/'), path);
let response = match self.http.get(&test_url).send().await {
Ok(r) => r,
Err(_) => continue,
};
let status = response.status().as_u16();
if status == 200 {
let body = response.text().await.unwrap_or_default();
// Check if response looks like an admin page
let body_lower = body.to_lowercase();
if body_lower.contains("admin")
|| body_lower.contains("dashboard")
|| body_lower.contains("management")
{
let snippet = body.chars().take(500).collect::<String>();
let evidence = DastEvidence {
request_method: "GET".to_string(),
request_url: test_url.clone(),
request_headers: None,
request_body: None,
response_status: status,
response_headers: None,
response_snippet: Some(snippet),
screenshot_path: None,
payload: Some(path.to_string()),
response_time_ms: None,
};
let mut finding = DastFinding::new(
String::new(),
target_id.clone(),
DastVulnType::AuthBypass,
format!("Path traversal auth bypass: {path}"),
format!(
"Possible authentication bypass via path traversal. \
Accessing '{}' returned admin-like content.",
test_url
),
Severity::High,
test_url,
"GET".to_string(),
);
finding.evidence = vec![evidence];
finding.cwe = Some("CWE-22".to_string());
findings.push(finding);
break;
}
}
}
info!(findings = findings.len(), "Auth bypass scan complete");
Ok(findings)
}
}