1use serde::{Deserialize, Serialize};
7use std::fmt;
8
9#[derive(Debug, Clone, Deserialize)]
13#[non_exhaustive]
14pub struct NewTaskResponse {
15 pub success: bool,
17 pub taskid: Option<String>,
19 pub message: Option<String>,
21}
22
23#[derive(Debug, Clone, Deserialize)]
25#[non_exhaustive]
26pub struct BasicResponse {
27 pub success: bool,
29 pub message: Option<String>,
31}
32
33#[derive(Debug, Clone, Deserialize)]
35#[non_exhaustive]
36pub struct StatusResponse {
37 pub success: bool,
39 pub status: Option<String>,
41 pub returncode: Option<i32>,
43}
44
45#[derive(Debug, Clone, Deserialize, Serialize)]
47#[non_exhaustive]
48pub struct SqlmapDataChunk {
49 pub r#type: i32,
51 pub value: serde_json::Value,
53}
54
55#[derive(Debug, Clone, Deserialize)]
57#[non_exhaustive]
58pub struct DataResponse {
59 pub success: bool,
61 pub data: Option<Vec<SqlmapDataChunk>>,
63 pub error: Option<Vec<String>>,
65}
66
67#[derive(Debug, Clone, Deserialize, Serialize)]
69#[non_exhaustive]
70pub struct LogEntry {
71 pub message: String,
73 pub level: String,
75 pub time: String,
77}
78
79#[derive(Debug, Clone, Deserialize)]
81#[non_exhaustive]
82pub struct LogResponse {
83 pub success: bool,
85 pub log: Option<Vec<LogEntry>>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
93#[non_exhaustive]
94pub struct SqlmapFinding {
95 pub parameter: String,
97 pub vulnerability_type: String,
99 pub payload: String,
101 pub details: serde_json::Value,
103}
104
105impl SqlmapFinding {
106 pub fn new(
108 parameter: impl Into<String>,
109 vulnerability_type: impl Into<String>,
110 payload: impl Into<String>,
111 details: serde_json::Value,
112 ) -> Self {
113 Self {
114 parameter: parameter.into(),
115 vulnerability_type: vulnerability_type.into(),
116 payload: payload.into(),
117 details,
118 }
119 }
120}
121
122impl fmt::Display for SqlmapFinding {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 write!(
125 f,
126 "[SQLi] {vtype} on param '{param}' — payload: {payload}",
127 vtype = self.vulnerability_type,
128 param = self.parameter,
129 payload = self.payload,
130 )
131 }
132}
133
134impl DataResponse {
135 pub fn findings(&self) -> Vec<SqlmapFinding> {
140 let Some(ref chunks) = self.data else {
141 return vec![];
142 };
143 let mut findings = Vec::new();
144
145 for chunk in chunks {
146 if chunk.r#type == 1 {
147 if let Some(arr) = chunk.value.as_array() {
148 for item in arr {
149 if let Some(obj) = item.as_object() {
150 let parameter = obj
151 .get("parameter")
152 .and_then(|v| v.as_str())
153 .unwrap_or("unknown")
154 .to_string();
155 let vulnerability_type = obj
156 .get("type")
157 .and_then(|v| v.as_str())
158 .unwrap_or("unknown")
159 .to_string();
160 let payload = obj
161 .get("payload")
162 .and_then(|v| v.as_str())
163 .unwrap_or("")
164 .to_string();
165 findings.push(SqlmapFinding {
166 parameter,
167 vulnerability_type,
168 payload,
169 details: item.clone(),
170 });
171 }
172 }
173 }
174 }
175 }
176
177 findings
178 }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
185#[non_exhaustive]
186pub enum OutputFormat {
187 Json,
189 JsonPretty,
191 Csv,
193 Markdown,
195 Plain,
197}
198
199pub fn format_findings(findings: &[SqlmapFinding], format: OutputFormat) -> String {
201 match format {
202 OutputFormat::Json => match serde_json::to_string(findings) {
203 Ok(json) => json,
204 Err(err) => format!("{{\"error\": \"serialization failed: {err}\"}}"),
205 },
206 OutputFormat::JsonPretty => match serde_json::to_string_pretty(findings) {
207 Ok(json) => json,
208 Err(err) => format!("{{\"error\": \"serialization failed: {err}\"}}"),
209 },
210 OutputFormat::Csv => {
211 let mut buf = String::from("parameter,vulnerability_type,payload\n");
212 for f in findings {
213 buf.push_str(&format!(
214 "{},{},{}\n",
215 csv_escape(&f.parameter),
216 csv_escape(&f.vulnerability_type),
217 csv_escape(&f.payload),
218 ));
219 }
220 buf
221 }
222 OutputFormat::Markdown => {
223 if findings.is_empty() {
224 return "No SQL injection findings.\n".to_string();
225 }
226 let mut buf = String::from("| Parameter | Type | Payload |\n");
227 buf.push_str("|-----------|------|----------|\n");
228 for f in findings {
229 buf.push_str(&format!(
230 "| `{}` | {} | `{}` |\n",
231 f.parameter,
232 f.vulnerability_type,
233 f.payload.replace('|', "\\|"),
234 ));
235 }
236 buf
237 }
238 OutputFormat::Plain => {
239 if findings.is_empty() {
240 return "No SQL injection findings detected.\n".to_string();
241 }
242 let mut buf = format!("=== {} SQLi Finding(s) ===\n\n", findings.len());
243 for (i, f) in findings.iter().enumerate() {
244 buf.push_str(&format!(
245 "#{} {} on param '{}'\n Payload: {}\n\n",
246 i + 1,
247 f.vulnerability_type,
248 f.parameter,
249 f.payload,
250 ));
251 }
252 buf
253 }
254 }
255}
256
257fn csv_escape(value: &str) -> String {
259 if value.contains(',') || value.contains('"') || value.contains('\n') {
260 format!("\"{}\"", value.replace('"', "\"\""))
261 } else {
262 value.to_string()
263 }
264}
265
266#[derive(Debug, Clone, Serialize, Default)]
287#[non_exhaustive]
288pub struct SqlmapOptions {
289 #[serde(skip_serializing_if = "Option::is_none")]
292 pub url: Option<String>,
293
294 #[serde(rename = "testParameter", skip_serializing_if = "Option::is_none")]
296 pub test_parameter: Option<String>,
297
298 #[serde(skip_serializing_if = "Option::is_none")]
301 pub dbms: Option<String>,
302
303 #[serde(skip_serializing_if = "Option::is_none")]
305 pub tech: Option<String>,
306
307 #[serde(skip_serializing_if = "Option::is_none")]
309 pub level: Option<i32>,
310
311 #[serde(skip_serializing_if = "Option::is_none")]
313 pub risk: Option<i32>,
314
315 #[serde(skip_serializing_if = "Option::is_none")]
317 pub string: Option<String>,
318
319 #[serde(rename = "notString", skip_serializing_if = "Option::is_none")]
321 pub not_string: Option<String>,
322
323 #[serde(skip_serializing_if = "Option::is_none")]
325 pub regexp: Option<String>,
326
327 #[serde(skip_serializing_if = "Option::is_none")]
329 pub code: Option<i32>,
330
331 #[serde(rename = "textOnly", skip_serializing_if = "Option::is_none")]
333 pub text_only: Option<bool>,
334
335 #[serde(skip_serializing_if = "Option::is_none")]
337 pub titles: Option<bool>,
338
339 #[serde(skip_serializing_if = "Option::is_none")]
342 pub cookie: Option<String>,
343
344 #[serde(skip_serializing_if = "Option::is_none")]
346 pub headers: Option<String>,
347
348 #[serde(skip_serializing_if = "Option::is_none")]
350 pub method: Option<String>,
351
352 #[serde(skip_serializing_if = "Option::is_none")]
354 pub data: Option<String>,
355
356 #[serde(rename = "randomAgent", skip_serializing_if = "Option::is_none")]
358 pub random_agent: Option<bool>,
359
360 #[serde(skip_serializing_if = "Option::is_none")]
362 pub proxy: Option<String>,
363
364 #[serde(skip_serializing_if = "Option::is_none")]
367 pub prefix: Option<String>,
368
369 #[serde(skip_serializing_if = "Option::is_none")]
371 pub suffix: Option<String>,
372
373 #[serde(skip_serializing_if = "Option::is_none")]
375 pub tamper: Option<String>,
376
377 #[serde(skip_serializing_if = "Option::is_none")]
379 pub skip: Option<String>,
380
381 #[serde(rename = "skipStatic", skip_serializing_if = "Option::is_none")]
383 pub skip_static: Option<bool>,
384
385 #[serde(skip_serializing_if = "Option::is_none")]
388 pub threads: Option<i32>,
389
390 #[serde(skip_serializing_if = "Option::is_none")]
392 pub verbose: Option<i32>,
393
394 #[serde(skip_serializing_if = "Option::is_none")]
396 pub batch: Option<bool>,
397
398 #[serde(skip_serializing_if = "Option::is_none")]
400 pub retries: Option<i32>,
401
402 #[serde(rename = "getDbs", skip_serializing_if = "Option::is_none")]
405 pub get_dbs: Option<bool>,
406
407 #[serde(rename = "getTables", skip_serializing_if = "Option::is_none")]
409 pub get_tables: Option<bool>,
410
411 #[serde(rename = "getColumns", skip_serializing_if = "Option::is_none")]
413 pub get_columns: Option<bool>,
414
415 #[serde(rename = "getUsers", skip_serializing_if = "Option::is_none")]
417 pub get_users: Option<bool>,
418
419 #[serde(rename = "getPasswordHashes", skip_serializing_if = "Option::is_none")]
421 pub get_passwords: Option<bool>,
422
423 #[serde(rename = "getPrivileges", skip_serializing_if = "Option::is_none")]
425 pub get_privileges: Option<bool>,
426
427 #[serde(rename = "isDba", skip_serializing_if = "Option::is_none")]
429 pub is_dba: Option<bool>,
430
431 #[serde(rename = "getCurrentUser", skip_serializing_if = "Option::is_none")]
433 pub current_user: Option<bool>,
434
435 #[serde(rename = "getCurrentDb", skip_serializing_if = "Option::is_none")]
437 pub current_db: Option<bool>,
438
439 #[serde(rename = "dumpAll", skip_serializing_if = "Option::is_none")]
441 pub dump_all: Option<bool>,
442
443 #[serde(rename = "dumpTable", skip_serializing_if = "Option::is_none")]
445 pub dump_table: Option<bool>,
446
447 #[serde(skip_serializing_if = "Option::is_none")]
449 pub search: Option<bool>,
450
451 #[serde(rename = "osShell", skip_serializing_if = "Option::is_none")]
454 pub os_shell: Option<bool>,
455
456 #[serde(rename = "sqlShell", skip_serializing_if = "Option::is_none")]
458 pub sql_shell: Option<bool>,
459
460 #[serde(rename = "fileRead", skip_serializing_if = "Option::is_none")]
462 pub file_read: Option<String>,
463
464 #[serde(rename = "fileWrite", skip_serializing_if = "Option::is_none")]
466 pub file_write: Option<String>,
467
468 #[serde(rename = "fileDest", skip_serializing_if = "Option::is_none")]
470 pub file_dest: Option<String>,
471
472 #[serde(skip_serializing_if = "Option::is_none")]
475 pub tor: Option<bool>,
476
477 #[serde(rename = "torPort", skip_serializing_if = "Option::is_none")]
479 pub tor_port: Option<i32>,
480
481 #[serde(rename = "torType", skip_serializing_if = "Option::is_none")]
483 pub tor_type: Option<String>,
484
485 #[serde(rename = "crawlDepth", skip_serializing_if = "Option::is_none")]
488 pub crawl_depth: Option<i32>,
489
490 #[serde(skip_serializing_if = "Option::is_none")]
492 pub scope: Option<String>,
493
494 #[serde(skip_serializing_if = "Option::is_none")]
496 pub forms: Option<bool>,
497
498 #[serde(rename = "secondUrl", skip_serializing_if = "Option::is_none")]
501 pub second_url: Option<String>,
502}
503
504#[derive(Debug, Clone, Default)]
509pub struct SqlmapOptionsBuilder {
510 inner: SqlmapOptions,
511}
512
513impl SqlmapOptions {
514 pub fn builder() -> SqlmapOptionsBuilder {
516 SqlmapOptionsBuilder::default()
517 }
518}
519
520macro_rules! builder_method {
522 ($name:ident, $field:ident, String) => {
523 pub fn $name(mut self, value: impl Into<String>) -> Self {
525 self.inner.$field = Some(value.into());
526 self
527 }
528 };
529 ($name:ident, $field:ident, bool) => {
530 pub fn $name(mut self, value: bool) -> Self {
532 self.inner.$field = Some(value);
533 self
534 }
535 };
536 ($name:ident, $field:ident, i32) => {
537 pub fn $name(mut self, value: i32) -> Self {
539 self.inner.$field = Some(value);
540 self
541 }
542 };
543}
544
545impl SqlmapOptionsBuilder {
546 builder_method!(url, url, String);
548 builder_method!(test_parameter, test_parameter, String);
549
550 builder_method!(dbms, dbms, String);
552 builder_method!(tech, tech, String);
553 builder_method!(level, level, i32);
554 builder_method!(risk, risk, i32);
555 builder_method!(string, string, String);
556 builder_method!(not_string, not_string, String);
557 builder_method!(regexp, regexp, String);
558 builder_method!(code, code, i32);
559 builder_method!(text_only, text_only, bool);
560 builder_method!(titles, titles, bool);
561
562 builder_method!(cookie, cookie, String);
564 builder_method!(headers, headers, String);
565 builder_method!(method, method, String);
566 builder_method!(data, data, String);
567 builder_method!(random_agent, random_agent, bool);
568 builder_method!(proxy, proxy, String);
569
570 builder_method!(prefix, prefix, String);
572 builder_method!(suffix, suffix, String);
573 builder_method!(tamper, tamper, String);
574 builder_method!(skip, skip, String);
575 builder_method!(skip_static, skip_static, bool);
576
577 builder_method!(threads, threads, i32);
579 builder_method!(verbose, verbose, i32);
580 builder_method!(batch, batch, bool);
581 builder_method!(retries, retries, i32);
582
583 builder_method!(get_dbs, get_dbs, bool);
585 builder_method!(get_tables, get_tables, bool);
586 builder_method!(get_columns, get_columns, bool);
587 builder_method!(get_users, get_users, bool);
588 builder_method!(get_passwords, get_passwords, bool);
589 builder_method!(get_privileges, get_privileges, bool);
590 builder_method!(is_dba, is_dba, bool);
591 builder_method!(current_user, current_user, bool);
592 builder_method!(current_db, current_db, bool);
593 builder_method!(dump_all, dump_all, bool);
594 builder_method!(dump_table, dump_table, bool);
595 builder_method!(search, search, bool);
596
597 builder_method!(os_shell, os_shell, bool);
599 builder_method!(sql_shell, sql_shell, bool);
600 builder_method!(file_read, file_read, String);
601 builder_method!(file_write, file_write, String);
602 builder_method!(file_dest, file_dest, String);
603
604 builder_method!(tor, tor, bool);
606 builder_method!(tor_port, tor_port, i32);
607 builder_method!(tor_type, tor_type, String);
608
609 builder_method!(crawl_depth, crawl_depth, i32);
611 builder_method!(scope, scope, String);
612 builder_method!(forms, forms, bool);
613
614 builder_method!(second_url, second_url, String);
616
617 pub fn build(self) -> SqlmapOptions {
619 self.inner
620 }
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626
627 #[test]
628 fn empty_data_response_gives_no_findings() {
629 let resp = DataResponse {
630 success: true,
631 data: None,
632 error: None,
633 };
634 assert!(resp.findings().is_empty());
635 }
636
637 #[test]
638 fn type_0_chunks_ignored() {
639 let resp = DataResponse {
640 success: true,
641 data: Some(vec![SqlmapDataChunk {
642 r#type: 0,
643 value: serde_json::json!("log message"),
644 }]),
645 error: None,
646 };
647 assert!(resp.findings().is_empty());
648 }
649
650 #[test]
651 fn type_1_chunk_parsed_as_finding() {
652 let resp = DataResponse {
653 success: true,
654 data: Some(vec![SqlmapDataChunk {
655 r#type: 1,
656 value: serde_json::json!([{
657 "parameter": "id",
658 "type": "boolean-based blind",
659 "payload": "id=1 AND 1=1"
660 }]),
661 }]),
662 error: None,
663 };
664 let findings = resp.findings();
665 assert_eq!(findings.len(), 1);
666 assert_eq!(findings[0].parameter, "id");
667 assert_eq!(findings[0].vulnerability_type, "boolean-based blind");
668 }
669
670 #[test]
671 fn builder_pattern_serializes_correctly() {
672 let opts = SqlmapOptions::builder()
673 .url("http://test.com?id=1")
674 .level(3)
675 .risk(2)
676 .batch(true)
677 .threads(4)
678 .tamper("space2comment")
679 .build();
680 let json = serde_json::to_string(&opts).expect("serialize");
681 assert!(json.contains("http://test.com"));
682 assert!(json.contains("\"level\":3"));
683 assert!(json.contains("\"threads\":4"));
684 assert!(json.contains("space2comment"));
685 assert!(!json.contains("dbms"));
687 }
688
689 #[test]
690 fn type_1_chunk_edge_cases() {
691 let resp = DataResponse {
692 success: true,
693 data: Some(vec![SqlmapDataChunk {
694 r#type: 1,
695 value: serde_json::json!([
696 { "parameter": "username" },
697 "string_instead_of_object_should_be_ignored",
698 { "type": "error-based" }
699 ]),
700 }]),
701 error: None,
702 };
703 let findings = resp.findings();
704 assert_eq!(findings.len(), 2);
705 assert_eq!(findings[0].parameter, "username");
706 assert_eq!(findings[0].vulnerability_type, "unknown");
707 assert_eq!(findings[1].parameter, "unknown");
708 assert_eq!(findings[1].vulnerability_type, "error-based");
709 }
710
711 #[test]
712 fn new_options_fields_serialize() {
713 let opts = SqlmapOptions::builder()
714 .tor(true)
715 .tor_port(9050)
716 .tor_type("SOCKS5")
717 .crawl_depth(3)
718 .second_url("http://verify.com")
719 .tamper("between,randomcase")
720 .retries(5)
721 .dump_all(true)
722 .file_read("/etc/passwd")
723 .build();
724 let json = serde_json::to_string(&opts).expect("serialize");
725 assert!(json.contains("\"tor\":true"));
726 assert!(json.contains("\"torPort\":9050"));
727 assert!(json.contains("\"crawlDepth\":3"));
728 assert!(json.contains("\"secondUrl\""));
729 assert!(json.contains("\"fileRead\""));
730 assert!(json.contains("\"dumpAll\":true"));
731 }
732
733 #[test]
734 fn finding_display() {
735 let finding = SqlmapFinding {
736 parameter: "id".into(),
737 vulnerability_type: "boolean-based blind".into(),
738 payload: "id=1 AND 1=1".into(),
739 details: serde_json::json!({}),
740 };
741 let display = format!("{finding}");
742 assert!(display.contains("boolean-based blind"));
743 assert!(display.contains("id"));
744 }
745
746 #[test]
747 fn format_csv_output() {
748 let findings = vec![SqlmapFinding {
749 parameter: "id".into(),
750 vulnerability_type: "error-based".into(),
751 payload: "' OR 1=1--".into(),
752 details: serde_json::json!({}),
753 }];
754 let csv = format_findings(&findings, OutputFormat::Csv);
755 assert!(csv.starts_with("parameter,vulnerability_type,payload\n"));
756 assert!(csv.contains("error-based"));
757 }
758
759 #[test]
760 fn format_plain_empty() {
761 let plain = format_findings(&[], OutputFormat::Plain);
762 assert_eq!(plain, "No SQL injection findings detected.\n");
763 }
764
765 #[test]
766 fn format_markdown_output() {
767 let findings = vec![SqlmapFinding {
768 parameter: "id".into(),
769 vulnerability_type: "UNION query".into(),
770 payload: "id=1 UNION SELECT 1,2--".into(),
771 details: serde_json::json!({}),
772 }];
773 let md = format_findings(&findings, OutputFormat::Markdown);
774 assert!(md.contains("| Parameter |"));
775 assert!(md.contains("UNION query"));
776 }
777}