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:
219
compliance-dast/src/agents/auth_bypass.rs
Normal file
219
compliance-dast/src/agents/auth_bypass.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user