use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct NewTaskResponse {
pub success: bool,
pub taskid: Option<String>,
pub message: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct BasicResponse {
pub success: bool,
pub message: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct StatusResponse {
pub success: bool,
pub status: Option<String>,
pub returncode: Option<i32>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[non_exhaustive]
pub struct SqlmapDataChunk {
pub r#type: i32,
pub value: serde_json::Value,
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct DataResponse {
pub success: bool,
pub data: Option<Vec<SqlmapDataChunk>>,
pub error: Option<Vec<String>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[non_exhaustive]
pub struct LogEntry {
pub message: String,
pub level: String,
pub time: String,
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct LogResponse {
pub success: bool,
pub log: Option<Vec<LogEntry>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SqlmapFinding {
pub parameter: String,
pub vulnerability_type: String,
pub payload: String,
pub details: serde_json::Value,
}
impl SqlmapFinding {
pub fn new(
parameter: impl Into<String>,
vulnerability_type: impl Into<String>,
payload: impl Into<String>,
details: serde_json::Value,
) -> Self {
Self {
parameter: parameter.into(),
vulnerability_type: vulnerability_type.into(),
payload: payload.into(),
details,
}
}
}
impl fmt::Display for SqlmapFinding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[SQLi] {vtype} on param '{param}' — payload: {payload}",
vtype = self.vulnerability_type,
param = self.parameter,
payload = self.payload,
)
}
}
impl DataResponse {
pub fn findings(&self) -> Vec<SqlmapFinding> {
let Some(ref chunks) = self.data else {
return vec![];
};
let mut findings = Vec::new();
for chunk in chunks {
if chunk.r#type == 1 {
if let Some(arr) = chunk.value.as_array() {
for item in arr {
if let Some(obj) = item.as_object() {
let parameter = obj
.get("parameter")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
let vulnerability_type = obj
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
let payload = obj
.get("payload")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
findings.push(SqlmapFinding {
parameter,
vulnerability_type,
payload,
details: item.clone(),
});
}
}
}
}
}
findings
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum OutputFormat {
Json,
JsonPretty,
Csv,
Markdown,
Plain,
}
pub fn format_findings(findings: &[SqlmapFinding], format: OutputFormat) -> String {
match format {
OutputFormat::Json => match serde_json::to_string(findings) {
Ok(json) => json,
Err(err) => format!("{{\"error\": \"serialization failed: {err}\"}}"),
},
OutputFormat::JsonPretty => match serde_json::to_string_pretty(findings) {
Ok(json) => json,
Err(err) => format!("{{\"error\": \"serialization failed: {err}\"}}"),
},
OutputFormat::Csv => {
let mut buf = String::from("parameter,vulnerability_type,payload\n");
for f in findings {
buf.push_str(&format!(
"{},{},{}\n",
csv_escape(&f.parameter),
csv_escape(&f.vulnerability_type),
csv_escape(&f.payload),
));
}
buf
}
OutputFormat::Markdown => {
if findings.is_empty() {
return "No SQL injection findings.\n".to_string();
}
let mut buf = String::from("| Parameter | Type | Payload |\n");
buf.push_str("|-----------|------|----------|\n");
for f in findings {
buf.push_str(&format!(
"| `{}` | {} | `{}` |\n",
f.parameter,
f.vulnerability_type,
f.payload.replace('|', "\\|"),
));
}
buf
}
OutputFormat::Plain => {
if findings.is_empty() {
return "No SQL injection findings detected.\n".to_string();
}
let mut buf = format!("=== {} SQLi Finding(s) ===\n\n", findings.len());
for (i, f) in findings.iter().enumerate() {
buf.push_str(&format!(
"#{} {} on param '{}'\n Payload: {}\n\n",
i + 1,
f.vulnerability_type,
f.parameter,
f.payload,
));
}
buf
}
}
}
fn csv_escape(value: &str) -> String {
if value.contains(',') || value.contains('"') || value.contains('\n') {
format!("\"{}\"", value.replace('"', "\"\""))
} else {
value.to_string()
}
}
#[derive(Debug, Clone, Serialize, Default)]
#[non_exhaustive]
pub struct SqlmapOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(rename = "testParameter", skip_serializing_if = "Option::is_none")]
pub test_parameter: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dbms: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tech: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub level: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub risk: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub string: Option<String>,
#[serde(rename = "notString", skip_serializing_if = "Option::is_none")]
pub not_string: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub regexp: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<i32>,
#[serde(rename = "textOnly", skip_serializing_if = "Option::is_none")]
pub text_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub titles: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cookie: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub method: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<String>,
#[serde(rename = "randomAgent", skip_serializing_if = "Option::is_none")]
pub random_agent: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub proxy: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub suffix: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tamper: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub skip: Option<String>,
#[serde(rename = "skipStatic", skip_serializing_if = "Option::is_none")]
pub skip_static: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub threads: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verbose: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub batch: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retries: Option<i32>,
#[serde(rename = "getDbs", skip_serializing_if = "Option::is_none")]
pub get_dbs: Option<bool>,
#[serde(rename = "getTables", skip_serializing_if = "Option::is_none")]
pub get_tables: Option<bool>,
#[serde(rename = "getColumns", skip_serializing_if = "Option::is_none")]
pub get_columns: Option<bool>,
#[serde(rename = "getUsers", skip_serializing_if = "Option::is_none")]
pub get_users: Option<bool>,
#[serde(rename = "getPasswordHashes", skip_serializing_if = "Option::is_none")]
pub get_passwords: Option<bool>,
#[serde(rename = "getPrivileges", skip_serializing_if = "Option::is_none")]
pub get_privileges: Option<bool>,
#[serde(rename = "isDba", skip_serializing_if = "Option::is_none")]
pub is_dba: Option<bool>,
#[serde(rename = "getCurrentUser", skip_serializing_if = "Option::is_none")]
pub current_user: Option<bool>,
#[serde(rename = "getCurrentDb", skip_serializing_if = "Option::is_none")]
pub current_db: Option<bool>,
#[serde(rename = "dumpAll", skip_serializing_if = "Option::is_none")]
pub dump_all: Option<bool>,
#[serde(rename = "dumpTable", skip_serializing_if = "Option::is_none")]
pub dump_table: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search: Option<bool>,
#[serde(rename = "osShell", skip_serializing_if = "Option::is_none")]
pub os_shell: Option<bool>,
#[serde(rename = "sqlShell", skip_serializing_if = "Option::is_none")]
pub sql_shell: Option<bool>,
#[serde(rename = "fileRead", skip_serializing_if = "Option::is_none")]
pub file_read: Option<String>,
#[serde(rename = "fileWrite", skip_serializing_if = "Option::is_none")]
pub file_write: Option<String>,
#[serde(rename = "fileDest", skip_serializing_if = "Option::is_none")]
pub file_dest: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tor: Option<bool>,
#[serde(rename = "torPort", skip_serializing_if = "Option::is_none")]
pub tor_port: Option<i32>,
#[serde(rename = "torType", skip_serializing_if = "Option::is_none")]
pub tor_type: Option<String>,
#[serde(rename = "crawlDepth", skip_serializing_if = "Option::is_none")]
pub crawl_depth: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub forms: Option<bool>,
#[serde(rename = "secondUrl", skip_serializing_if = "Option::is_none")]
pub second_url: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct SqlmapOptionsBuilder {
inner: SqlmapOptions,
}
impl SqlmapOptions {
pub fn builder() -> SqlmapOptionsBuilder {
SqlmapOptionsBuilder::default()
}
}
macro_rules! builder_method {
($name:ident, $field:ident, String) => {
pub fn $name(mut self, value: impl Into<String>) -> Self {
self.inner.$field = Some(value.into());
self
}
};
($name:ident, $field:ident, bool) => {
pub fn $name(mut self, value: bool) -> Self {
self.inner.$field = Some(value);
self
}
};
($name:ident, $field:ident, i32) => {
pub fn $name(mut self, value: i32) -> Self {
self.inner.$field = Some(value);
self
}
};
}
impl SqlmapOptionsBuilder {
builder_method!(url, url, String);
builder_method!(test_parameter, test_parameter, String);
builder_method!(dbms, dbms, String);
builder_method!(tech, tech, String);
builder_method!(level, level, i32);
builder_method!(risk, risk, i32);
builder_method!(string, string, String);
builder_method!(not_string, not_string, String);
builder_method!(regexp, regexp, String);
builder_method!(code, code, i32);
builder_method!(text_only, text_only, bool);
builder_method!(titles, titles, bool);
builder_method!(cookie, cookie, String);
builder_method!(headers, headers, String);
builder_method!(method, method, String);
builder_method!(data, data, String);
builder_method!(random_agent, random_agent, bool);
builder_method!(proxy, proxy, String);
builder_method!(prefix, prefix, String);
builder_method!(suffix, suffix, String);
builder_method!(tamper, tamper, String);
builder_method!(skip, skip, String);
builder_method!(skip_static, skip_static, bool);
builder_method!(threads, threads, i32);
builder_method!(verbose, verbose, i32);
builder_method!(batch, batch, bool);
builder_method!(retries, retries, i32);
builder_method!(get_dbs, get_dbs, bool);
builder_method!(get_tables, get_tables, bool);
builder_method!(get_columns, get_columns, bool);
builder_method!(get_users, get_users, bool);
builder_method!(get_passwords, get_passwords, bool);
builder_method!(get_privileges, get_privileges, bool);
builder_method!(is_dba, is_dba, bool);
builder_method!(current_user, current_user, bool);
builder_method!(current_db, current_db, bool);
builder_method!(dump_all, dump_all, bool);
builder_method!(dump_table, dump_table, bool);
builder_method!(search, search, bool);
builder_method!(os_shell, os_shell, bool);
builder_method!(sql_shell, sql_shell, bool);
builder_method!(file_read, file_read, String);
builder_method!(file_write, file_write, String);
builder_method!(file_dest, file_dest, String);
builder_method!(tor, tor, bool);
builder_method!(tor_port, tor_port, i32);
builder_method!(tor_type, tor_type, String);
builder_method!(crawl_depth, crawl_depth, i32);
builder_method!(scope, scope, String);
builder_method!(forms, forms, bool);
builder_method!(second_url, second_url, String);
pub fn build(self) -> SqlmapOptions {
self.inner
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_data_response_gives_no_findings() {
let resp = DataResponse {
success: true,
data: None,
error: None,
};
assert!(resp.findings().is_empty());
}
#[test]
fn type_0_chunks_ignored() {
let resp = DataResponse {
success: true,
data: Some(vec![SqlmapDataChunk {
r#type: 0,
value: serde_json::json!("log message"),
}]),
error: None,
};
assert!(resp.findings().is_empty());
}
#[test]
fn type_1_chunk_parsed_as_finding() {
let resp = DataResponse {
success: true,
data: Some(vec![SqlmapDataChunk {
r#type: 1,
value: serde_json::json!([{
"parameter": "id",
"type": "boolean-based blind",
"payload": "id=1 AND 1=1"
}]),
}]),
error: None,
};
let findings = resp.findings();
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].parameter, "id");
assert_eq!(findings[0].vulnerability_type, "boolean-based blind");
}
#[test]
fn builder_pattern_serializes_correctly() {
let opts = SqlmapOptions::builder()
.url("http://test.com?id=1")
.level(3)
.risk(2)
.batch(true)
.threads(4)
.tamper("space2comment")
.build();
let json = serde_json::to_string(&opts).expect("serialize");
assert!(json.contains("http://test.com"));
assert!(json.contains("\"level\":3"));
assert!(json.contains("\"threads\":4"));
assert!(json.contains("space2comment"));
assert!(!json.contains("dbms"));
}
#[test]
fn type_1_chunk_edge_cases() {
let resp = DataResponse {
success: true,
data: Some(vec![SqlmapDataChunk {
r#type: 1,
value: serde_json::json!([
{ "parameter": "username" },
"string_instead_of_object_should_be_ignored",
{ "type": "error-based" }
]),
}]),
error: None,
};
let findings = resp.findings();
assert_eq!(findings.len(), 2);
assert_eq!(findings[0].parameter, "username");
assert_eq!(findings[0].vulnerability_type, "unknown");
assert_eq!(findings[1].parameter, "unknown");
assert_eq!(findings[1].vulnerability_type, "error-based");
}
#[test]
fn new_options_fields_serialize() {
let opts = SqlmapOptions::builder()
.tor(true)
.tor_port(9050)
.tor_type("SOCKS5")
.crawl_depth(3)
.second_url("http://verify.com")
.tamper("between,randomcase")
.retries(5)
.dump_all(true)
.file_read("/etc/passwd")
.build();
let json = serde_json::to_string(&opts).expect("serialize");
assert!(json.contains("\"tor\":true"));
assert!(json.contains("\"torPort\":9050"));
assert!(json.contains("\"crawlDepth\":3"));
assert!(json.contains("\"secondUrl\""));
assert!(json.contains("\"fileRead\""));
assert!(json.contains("\"dumpAll\":true"));
}
#[test]
fn finding_display() {
let finding = SqlmapFinding {
parameter: "id".into(),
vulnerability_type: "boolean-based blind".into(),
payload: "id=1 AND 1=1".into(),
details: serde_json::json!({}),
};
let display = format!("{finding}");
assert!(display.contains("boolean-based blind"));
assert!(display.contains("id"));
}
#[test]
fn format_csv_output() {
let findings = vec![SqlmapFinding {
parameter: "id".into(),
vulnerability_type: "error-based".into(),
payload: "' OR 1=1--".into(),
details: serde_json::json!({}),
}];
let csv = format_findings(&findings, OutputFormat::Csv);
assert!(csv.starts_with("parameter,vulnerability_type,payload\n"));
assert!(csv.contains("error-based"));
}
#[test]
fn format_plain_empty() {
let plain = format_findings(&[], OutputFormat::Plain);
assert_eq!(plain, "No SQL injection findings detected.\n");
}
#[test]
fn format_markdown_output() {
let findings = vec![SqlmapFinding {
parameter: "id".into(),
vulnerability_type: "UNION query".into(),
payload: "id=1 UNION SELECT 1,2--".into(),
details: serde_json::json!({}),
}];
let md = format_findings(&findings, OutputFormat::Markdown);
assert!(md.contains("| Parameter |"));
assert!(md.contains("UNION query"));
}
}