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:
co-authored by
Claude Opus 4.6
parent
03ee69834d
commit
cea8f59e10
@@ -0,0 +1,195 @@
|
||||
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, warn};
|
||||
|
||||
/// SQL Injection testing agent
|
||||
pub struct SqlInjectionAgent {
|
||||
http: reqwest::Client,
|
||||
}
|
||||
|
||||
impl SqlInjectionAgent {
|
||||
pub fn new(http: reqwest::Client) -> Self {
|
||||
Self { http }
|
||||
}
|
||||
|
||||
/// Test payloads for SQL injection detection
|
||||
fn payloads(&self) -> Vec<(&str, &str)> {
|
||||
vec![
|
||||
("' OR '1'='1", "boolean-based blind"),
|
||||
("1' AND SLEEP(2)-- -", "time-based blind"),
|
||||
("' UNION SELECT NULL--", "union-based"),
|
||||
("1; DROP TABLE test--", "stacked queries"),
|
||||
("' OR 1=1#", "mysql boolean"),
|
||||
("1' ORDER BY 1--", "order by probe"),
|
||||
("') OR ('1'='1", "parenthesis bypass"),
|
||||
]
|
||||
}
|
||||
|
||||
/// Error patterns that indicate SQL injection
|
||||
fn error_patterns(&self) -> Vec<&str> {
|
||||
vec![
|
||||
"sql syntax",
|
||||
"mysql_fetch",
|
||||
"ORA-01756",
|
||||
"SQLite3::query",
|
||||
"pg_query",
|
||||
"unclosed quotation mark",
|
||||
"quoted string not properly terminated",
|
||||
"you have an error in your sql",
|
||||
"warning: mysql",
|
||||
"microsoft sql native client error",
|
||||
"postgresql query failed",
|
||||
"unterminated string",
|
||||
"syntax error at or near",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl DastAgent for SqlInjectionAgent {
|
||||
fn name(&self) -> &str {
|
||||
"sql_injection"
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
for endpoint in &context.endpoints {
|
||||
// Only test endpoints with parameters
|
||||
if endpoint.parameters.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for param in &endpoint.parameters {
|
||||
for (payload, technique) in self.payloads() {
|
||||
// Build the request with the injection payload
|
||||
let test_url = if endpoint.method == "GET" {
|
||||
format!(
|
||||
"{}?{}={}",
|
||||
endpoint.url,
|
||||
param.name,
|
||||
urlencoding::encode(payload)
|
||||
)
|
||||
} else {
|
||||
endpoint.url.clone()
|
||||
};
|
||||
|
||||
let request = if endpoint.method == "POST" {
|
||||
self.http
|
||||
.post(&endpoint.url)
|
||||
.form(&[(param.name.as_str(), payload)])
|
||||
} else {
|
||||
self.http.get(&test_url)
|
||||
};
|
||||
|
||||
let response = match request.send().await {
|
||||
Ok(r) => r,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let status = response.status().as_u16();
|
||||
let headers: std::collections::HashMap<String, String> = response
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
|
||||
.collect();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
|
||||
// Check for SQL error patterns in response
|
||||
let body_lower = body.to_lowercase();
|
||||
let is_vulnerable = self
|
||||
.error_patterns()
|
||||
.iter()
|
||||
.any(|pattern| body_lower.contains(pattern));
|
||||
|
||||
if is_vulnerable {
|
||||
let snippet = body.chars().take(500).collect::<String>();
|
||||
|
||||
let evidence = DastEvidence {
|
||||
request_method: endpoint.method.clone(),
|
||||
request_url: test_url.clone(),
|
||||
request_headers: None,
|
||||
request_body: if endpoint.method == "POST" {
|
||||
Some(format!("{}={}", param.name, payload))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
response_status: status,
|
||||
response_headers: Some(headers),
|
||||
response_snippet: Some(snippet),
|
||||
screenshot_path: None,
|
||||
payload: Some(payload.to_string()),
|
||||
response_time_ms: None,
|
||||
};
|
||||
|
||||
let mut finding = DastFinding::new(
|
||||
String::new(), // scan_run_id set by orchestrator
|
||||
target_id.clone(),
|
||||
DastVulnType::SqlInjection,
|
||||
format!("SQL Injection ({technique}) in parameter '{}'", param.name),
|
||||
format!(
|
||||
"SQL injection vulnerability detected in parameter '{}' at {} using {} technique. \
|
||||
The server returned SQL error messages in response to the injected payload.",
|
||||
param.name, endpoint.url, technique
|
||||
),
|
||||
Severity::Critical,
|
||||
endpoint.url.clone(),
|
||||
endpoint.method.clone(),
|
||||
);
|
||||
finding.parameter = Some(param.name.clone());
|
||||
finding.exploitable = true;
|
||||
finding.evidence = vec![evidence];
|
||||
finding.cwe = Some("CWE-89".to_string());
|
||||
finding.remediation = Some(
|
||||
"Use parameterized queries or prepared statements. \
|
||||
Never concatenate user input into SQL queries."
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
findings.push(finding);
|
||||
|
||||
warn!(
|
||||
endpoint = %endpoint.url,
|
||||
param = %param.name,
|
||||
technique,
|
||||
"SQL injection found"
|
||||
);
|
||||
|
||||
// Don't test more payloads for same param once confirmed
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!(findings = findings.len(), "SQL injection scan complete");
|
||||
Ok(findings)
|
||||
}
|
||||
}
|
||||
|
||||
/// URL-encode a string for query parameters
|
||||
mod urlencoding {
|
||||
pub fn encode(input: &str) -> String {
|
||||
let mut encoded = String::new();
|
||||
for byte in input.bytes() {
|
||||
match byte {
|
||||
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
|
||||
encoded.push(byte as char);
|
||||
}
|
||||
_ => {
|
||||
encoded.push_str(&format!("%{:02X}", byte));
|
||||
}
|
||||
}
|
||||
}
|
||||
encoded
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user