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>
196 lines
7.3 KiB
Rust
196 lines
7.3 KiB
Rust
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
|
|
}
|
|
}
|