Files
compliance-scanner-agent/compliance-dast/src/agents/injection.rs
Sharang Parnerkar cea8f59e10 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>
2026-03-04 13:53:50 +01:00

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
}
}