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 pub message: Option<String>,
45}
46
47#[derive(Debug, Clone, Deserialize, Serialize)]
49#[non_exhaustive]
50pub struct SqlmapDataChunk {
51 pub r#type: i32,
53 pub value: serde_json::Value,
55}
56
57#[derive(Debug, Clone, Deserialize)]
59#[non_exhaustive]
60pub struct DataResponse {
61 pub success: bool,
63 pub data: Option<Vec<SqlmapDataChunk>>,
65 pub error: Option<Vec<String>>,
67 pub message: Option<String>,
69}
70
71#[derive(Debug, Clone, Deserialize, Serialize)]
73#[non_exhaustive]
74pub struct LogEntry {
75 pub message: String,
77 pub level: String,
79 pub time: String,
81}
82
83#[derive(Debug, Clone, Deserialize)]
85#[non_exhaustive]
86pub struct LogResponse {
87 pub success: bool,
89 pub log: Option<Vec<LogEntry>>,
91 pub message: Option<String>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
99#[non_exhaustive]
100pub struct SqlmapFinding {
101 pub parameter: String,
103 pub vulnerability_type: String,
105 pub payload: String,
107 pub details: serde_json::Value,
109}
110
111impl SqlmapFinding {
112 pub fn new(
114 parameter: impl Into<String>,
115 vulnerability_type: impl Into<String>,
116 payload: impl Into<String>,
117 details: serde_json::Value,
118 ) -> Self {
119 Self {
120 parameter: parameter.into(),
121 vulnerability_type: vulnerability_type.into(),
122 payload: payload.into(),
123 details,
124 }
125 }
126}
127
128impl fmt::Display for SqlmapFinding {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 write!(
131 f,
132 "[SQLi] {vtype} on param '{param}' - payload: {payload}",
133 vtype = self.vulnerability_type,
134 param = self.parameter,
135 payload = self.payload,
136 )
137 }
138}
139
140fn is_confirmed_finding(vulnerability_type: &str, payload: &str) -> bool {
141 !payload.is_empty() && vulnerability_type != "unknown"
142}
143
144fn technique_entries(data: &serde_json::Value) -> Vec<&serde_json::Value> {
145 if let Some(arr) = data.as_array() {
146 return arr.iter().collect();
147 }
148 if let Some(map) = data.as_object() {
149 let mut entries: Vec<_> = map.iter().collect();
150 entries.sort_by(
151 |(a, _), (b, _)| match (a.parse::<u64>(), b.parse::<u64>()) {
152 (Ok(a_num), Ok(b_num)) => a_num.cmp(&b_num),
153 (Ok(_), Err(_)) => std::cmp::Ordering::Less,
154 (Err(_), Ok(_)) => std::cmp::Ordering::Greater,
155 (Err(_), Err(_)) => a.cmp(b),
156 },
157 );
158 return entries.into_iter().map(|(_, v)| v).collect();
159 }
160 vec![]
161}
162
163fn push_technique_finding(
164 findings: &mut Vec<SqlmapFinding>,
165 parameter: &str,
166 tech: &serde_json::Value,
167) {
168 let Some(tech_obj) = tech.as_object() else {
169 return;
170 };
171 let vulnerability_type = tech_obj
172 .get("technique")
173 .or_else(|| tech_obj.get("title"))
174 .and_then(|v| v.as_str())
175 .unwrap_or("unknown")
176 .to_string();
177 let payload = tech_obj
178 .get("payload")
179 .and_then(|v| v.as_str())
180 .unwrap_or("")
181 .to_string();
182 if is_confirmed_finding(&vulnerability_type, &payload) {
183 findings.push(SqlmapFinding {
184 parameter: parameter.to_string(),
185 vulnerability_type,
186 payload,
187 details: tech.clone(),
188 });
189 }
190}
191
192impl DataResponse {
193 pub fn findings(&self) -> Vec<SqlmapFinding> {
199 let Some(ref chunks) = self.data else {
200 return vec![];
201 };
202 let mut findings = Vec::new();
203
204 for chunk in chunks {
205 if chunk.r#type != 1 {
206 continue;
207 }
208 let Some(arr) = chunk.value.as_array() else {
209 continue;
210 };
211 for item in arr {
212 let Some(obj) = item.as_object() else {
213 continue;
214 };
215 let parameter = obj
216 .get("parameter")
217 .and_then(|v| v.as_str())
218 .unwrap_or("unknown")
219 .to_string();
220
221 if let Some(data_val) = obj.get("data") {
222 if data_val.is_array() || data_val.is_object() {
223 for tech in technique_entries(data_val) {
224 push_technique_finding(&mut findings, ¶meter, tech);
225 }
226 continue;
227 }
228 }
229
230 let vulnerability_type = obj
232 .get("type")
233 .or_else(|| obj.get("title"))
234 .and_then(|v| v.as_str())
235 .unwrap_or("unknown")
236 .to_string();
237 let payload = obj
238 .get("payload")
239 .and_then(|v| v.as_str())
240 .unwrap_or("")
241 .to_string();
242 if is_confirmed_finding(&vulnerability_type, &payload) {
243 findings.push(SqlmapFinding {
244 parameter,
245 vulnerability_type,
246 payload,
247 details: item.clone(),
248 });
249 }
250 }
251 }
252
253 findings
254 }
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
261#[non_exhaustive]
262pub enum OutputFormat {
263 Json,
265 JsonPretty,
267 Csv,
269 Markdown,
271 Plain,
273}
274
275pub fn format_findings(findings: &[SqlmapFinding], format: OutputFormat) -> String {
277 match format {
278 OutputFormat::Json => {
279 serde_json::to_string(findings).unwrap_or_else(|_| "[]".into())
281 }
282 OutputFormat::JsonPretty => {
283 serde_json::to_string_pretty(findings).unwrap_or_else(|_| "[]".into())
284 }
285 OutputFormat::Csv => {
286 let mut buf = String::from("parameter,vulnerability_type,payload\n");
287 for f in findings {
288 buf.push_str(&format!(
289 "{},{},{}\n",
290 csv_escape(&f.parameter),
291 csv_escape(&f.vulnerability_type),
292 csv_escape(&f.payload),
293 ));
294 }
295 buf
296 }
297 OutputFormat::Markdown => {
298 if findings.is_empty() {
299 return "No SQL injection findings.\n".to_string();
300 }
301 let mut buf = String::from("| Parameter | Type | Payload |\n");
302 buf.push_str("|-----------|------|----------|\n");
303 for f in findings {
304 buf.push_str(&format!(
305 "| `{}` | `{}` | `{}` |\n",
306 markdown_escape(&f.parameter),
307 markdown_escape(&f.vulnerability_type),
308 markdown_escape(&f.payload),
309 ));
310 }
311 buf
312 }
313 OutputFormat::Plain => {
314 if findings.is_empty() {
315 return "No SQL injection findings detected.\n".to_string();
316 }
317 let mut buf = format!("=== {} SQLi Finding(s) ===\n\n", findings.len());
318 for (i, f) in findings.iter().enumerate() {
319 buf.push_str(&format!(
320 "#{} {} on param '{}'\n Payload: {}\n\n",
321 i + 1,
322 f.vulnerability_type,
323 f.parameter,
324 f.payload,
325 ));
326 }
327 buf
328 }
329 }
330}
331
332fn csv_escape(value: &str) -> String {
334 if value.contains(',') || value.contains('"') || value.contains('\n') {
335 format!("\"{}\"", value.replace('"', "\"\""))
336 } else {
337 value.to_string()
338 }
339}
340
341fn markdown_escape(value: &str) -> String {
343 value.replace('|', "\\|")
344}
345
346#[derive(Debug, Clone, Serialize, Default)]
367#[non_exhaustive]
368pub struct SqlmapOptions {
369 #[serde(skip_serializing_if = "Option::is_none")]
372 pub url: Option<String>,
373
374 #[serde(rename = "testParameter", skip_serializing_if = "Option::is_none")]
376 pub test_parameter: Option<String>,
377
378 #[serde(skip_serializing_if = "Option::is_none")]
381 pub dbms: Option<String>,
382
383 #[serde(rename = "technique", skip_serializing_if = "Option::is_none")]
385 pub tech: Option<String>,
386
387 #[serde(skip_serializing_if = "Option::is_none")]
389 pub level: Option<i32>,
390
391 #[serde(skip_serializing_if = "Option::is_none")]
393 pub risk: Option<i32>,
394
395 #[serde(skip_serializing_if = "Option::is_none")]
397 pub string: Option<String>,
398
399 #[serde(rename = "notString", skip_serializing_if = "Option::is_none")]
401 pub not_string: Option<String>,
402
403 #[serde(skip_serializing_if = "Option::is_none")]
405 pub regexp: Option<String>,
406
407 #[serde(skip_serializing_if = "Option::is_none")]
409 pub code: Option<i32>,
410
411 #[serde(rename = "textOnly", skip_serializing_if = "Option::is_none")]
413 pub text_only: Option<bool>,
414
415 #[serde(skip_serializing_if = "Option::is_none")]
417 pub titles: Option<bool>,
418
419 #[serde(skip_serializing_if = "Option::is_none")]
422 pub cookie: Option<String>,
423
424 #[serde(skip_serializing_if = "Option::is_none")]
426 pub headers: Option<String>,
427
428 #[serde(skip_serializing_if = "Option::is_none")]
430 pub method: Option<String>,
431
432 #[serde(skip_serializing_if = "Option::is_none")]
434 pub data: Option<String>,
435
436 #[serde(rename = "randomAgent", skip_serializing_if = "Option::is_none")]
438 pub random_agent: Option<bool>,
439
440 #[serde(skip_serializing_if = "Option::is_none")]
442 pub proxy: Option<String>,
443
444 #[serde(skip_serializing_if = "Option::is_none")]
447 pub prefix: Option<String>,
448
449 #[serde(skip_serializing_if = "Option::is_none")]
451 pub suffix: Option<String>,
452
453 #[serde(skip_serializing_if = "Option::is_none")]
455 pub tamper: Option<String>,
456
457 #[serde(skip_serializing_if = "Option::is_none")]
459 pub skip: Option<String>,
460
461 #[serde(rename = "skipStatic", skip_serializing_if = "Option::is_none")]
463 pub skip_static: Option<bool>,
464
465 #[serde(skip_serializing_if = "Option::is_none")]
468 pub threads: Option<i32>,
469
470 #[serde(skip_serializing_if = "Option::is_none")]
472 pub verbose: Option<i32>,
473
474 #[serde(skip_serializing_if = "Option::is_none")]
476 pub batch: Option<bool>,
477
478 #[serde(skip_serializing_if = "Option::is_none")]
480 pub retries: Option<i32>,
481
482 #[serde(rename = "getDbs", skip_serializing_if = "Option::is_none")]
485 pub get_dbs: Option<bool>,
486
487 #[serde(rename = "getTables", skip_serializing_if = "Option::is_none")]
489 pub get_tables: Option<bool>,
490
491 #[serde(rename = "getColumns", skip_serializing_if = "Option::is_none")]
493 pub get_columns: Option<bool>,
494
495 #[serde(rename = "getUsers", skip_serializing_if = "Option::is_none")]
497 pub get_users: Option<bool>,
498
499 #[serde(rename = "getPasswordHashes", skip_serializing_if = "Option::is_none")]
501 pub get_passwords: Option<bool>,
502
503 #[serde(rename = "getPrivileges", skip_serializing_if = "Option::is_none")]
505 pub get_privileges: Option<bool>,
506
507 #[serde(rename = "isDba", skip_serializing_if = "Option::is_none")]
509 pub is_dba: Option<bool>,
510
511 #[serde(rename = "getCurrentUser", skip_serializing_if = "Option::is_none")]
513 pub current_user: Option<bool>,
514
515 #[serde(rename = "getCurrentDb", skip_serializing_if = "Option::is_none")]
517 pub current_db: Option<bool>,
518
519 #[serde(rename = "dumpAll", skip_serializing_if = "Option::is_none")]
521 pub dump_all: Option<bool>,
522
523 #[serde(rename = "dumpTable", skip_serializing_if = "Option::is_none")]
525 pub dump_table: Option<bool>,
526
527 #[serde(skip_serializing_if = "Option::is_none")]
529 pub search: Option<bool>,
530
531 #[serde(rename = "osShell", skip_serializing_if = "Option::is_none")]
534 pub os_shell: Option<bool>,
535
536 #[serde(rename = "sqlShell", skip_serializing_if = "Option::is_none")]
538 pub sql_shell: Option<bool>,
539
540 #[serde(rename = "fileRead", skip_serializing_if = "Option::is_none")]
542 pub file_read: Option<String>,
543
544 #[serde(rename = "fileWrite", skip_serializing_if = "Option::is_none")]
546 pub file_write: Option<String>,
547
548 #[serde(rename = "fileDest", skip_serializing_if = "Option::is_none")]
550 pub file_dest: Option<String>,
551
552 #[serde(skip_serializing_if = "Option::is_none")]
555 pub tor: Option<bool>,
556
557 #[serde(rename = "torPort", skip_serializing_if = "Option::is_none")]
559 pub tor_port: Option<i32>,
560
561 #[serde(rename = "torType", skip_serializing_if = "Option::is_none")]
563 pub tor_type: Option<String>,
564
565 #[serde(rename = "crawlDepth", skip_serializing_if = "Option::is_none")]
568 pub crawl_depth: Option<i32>,
569
570 #[serde(skip_serializing_if = "Option::is_none")]
572 pub scope: Option<String>,
573
574 #[serde(skip_serializing_if = "Option::is_none")]
576 pub forms: Option<bool>,
577
578 #[serde(rename = "secondUrl", skip_serializing_if = "Option::is_none")]
581 pub second_url: Option<String>,
582}
583
584#[derive(Debug, Clone, Default)]
589pub struct SqlmapOptionsBuilder {
590 inner: SqlmapOptions,
591}
592
593impl SqlmapOptions {
594 pub fn builder() -> SqlmapOptionsBuilder {
596 SqlmapOptionsBuilder::default()
597 }
598}
599
600macro_rules! builder_method {
602 ($name:ident, $field:ident, String) => {
603 pub fn $name(mut self, value: impl Into<String>) -> Self {
605 self.inner.$field = Some(value.into());
606 self
607 }
608 };
609 ($name:ident, $field:ident, bool) => {
610 pub fn $name(mut self, value: bool) -> Self {
612 self.inner.$field = Some(value);
613 self
614 }
615 };
616 ($name:ident, $field:ident, i32) => {
617 pub fn $name(mut self, value: i32) -> Self {
619 self.inner.$field = Some(value);
620 self
621 }
622 };
623}
624
625impl SqlmapOptionsBuilder {
626 builder_method!(url, url, String);
628 builder_method!(test_parameter, test_parameter, String);
629
630 builder_method!(dbms, dbms, String);
632 builder_method!(tech, tech, String);
633 builder_method!(level, level, i32);
634 builder_method!(risk, risk, i32);
635 builder_method!(string, string, String);
636 builder_method!(not_string, not_string, String);
637 builder_method!(regexp, regexp, String);
638 builder_method!(code, code, i32);
639 builder_method!(text_only, text_only, bool);
640 builder_method!(titles, titles, bool);
641
642 builder_method!(cookie, cookie, String);
644 builder_method!(headers, headers, String);
645 builder_method!(method, method, String);
646 builder_method!(data, data, String);
647 builder_method!(random_agent, random_agent, bool);
648 builder_method!(proxy, proxy, String);
649
650 builder_method!(prefix, prefix, String);
652 builder_method!(suffix, suffix, String);
653 builder_method!(tamper, tamper, String);
654 builder_method!(skip, skip, String);
655 builder_method!(skip_static, skip_static, bool);
656
657 builder_method!(threads, threads, i32);
659 builder_method!(verbose, verbose, i32);
660 builder_method!(batch, batch, bool);
661 builder_method!(retries, retries, i32);
662
663 builder_method!(get_dbs, get_dbs, bool);
665 builder_method!(get_tables, get_tables, bool);
666 builder_method!(get_columns, get_columns, bool);
667 builder_method!(get_users, get_users, bool);
668 builder_method!(get_passwords, get_passwords, bool);
669 builder_method!(get_privileges, get_privileges, bool);
670 builder_method!(is_dba, is_dba, bool);
671 builder_method!(current_user, current_user, bool);
672 builder_method!(current_db, current_db, bool);
673 builder_method!(dump_all, dump_all, bool);
674 builder_method!(dump_table, dump_table, bool);
675 builder_method!(search, search, bool);
676
677 builder_method!(os_shell, os_shell, bool);
679 builder_method!(sql_shell, sql_shell, bool);
680 builder_method!(file_read, file_read, String);
681 builder_method!(file_write, file_write, String);
682 builder_method!(file_dest, file_dest, String);
683
684 builder_method!(tor, tor, bool);
686 builder_method!(tor_port, tor_port, i32);
687 builder_method!(tor_type, tor_type, String);
688
689 builder_method!(crawl_depth, crawl_depth, i32);
691 builder_method!(scope, scope, String);
692 builder_method!(forms, forms, bool);
693
694 builder_method!(second_url, second_url, String);
696
697 pub fn build(self) -> SqlmapOptions {
699 self.inner
700 }
701}
702
703#[cfg(test)]
704mod tests {
705 use super::*;
706
707 #[test]
708 fn empty_data_response_gives_no_findings() {
709 let resp = DataResponse {
710 success: true,
711 data: None,
712 error: None,
713 message: None,
714 };
715 assert!(resp.findings().is_empty());
716 }
717
718 #[test]
719 fn type_0_chunks_ignored() {
720 let resp = DataResponse {
721 success: true,
722 data: Some(vec![SqlmapDataChunk {
723 r#type: 0,
724 value: serde_json::json!("log message"),
725 }]),
726 error: None,
727 message: None,
728 };
729 assert!(resp.findings().is_empty());
730 }
731
732 #[test]
733 fn type_1_nested_techniques_expanded() {
734 let resp = DataResponse {
735 success: true,
736 data: Some(vec![SqlmapDataChunk {
737 r#type: 1,
738 value: serde_json::json!([{
739 "place": "GET",
740 "parameter": "id",
741 "data": [
742 {
743 "title": "AND boolean-based blind - WHERE or HAVING clause",
744 "payload": "id=1 AND 8888=8888"
745 },
746 {
747 "technique": "time-based blind",
748 "payload": "id=1 AND SLEEP(5)"
749 }
750 ]
751 }]),
752 }]),
753 error: None,
754 message: None,
755 };
756 let findings = resp.findings();
757 assert_eq!(findings.len(), 2);
758 assert_eq!(findings[0].parameter, "id");
759 assert_eq!(
760 findings[0].vulnerability_type,
761 "AND boolean-based blind - WHERE or HAVING clause"
762 );
763 assert_eq!(findings[0].payload, "id=1 AND 8888=8888");
764 assert_eq!(findings[1].vulnerability_type, "time-based blind");
765 assert_eq!(findings[1].payload, "id=1 AND SLEEP(5)");
766 }
767
768 #[test]
769 fn type_1_chunk_parsed_as_finding() {
770 let resp = DataResponse {
771 success: true,
772 data: Some(vec![SqlmapDataChunk {
773 r#type: 1,
774 value: serde_json::json!([{
775 "parameter": "id",
776 "type": "boolean-based blind",
777 "payload": "id=1 AND 1=1"
778 }]),
779 }]),
780 error: None,
781 message: None,
782 };
783 let findings = resp.findings();
784 assert_eq!(findings.len(), 1);
785 assert_eq!(findings[0].parameter, "id");
786 assert_eq!(findings[0].vulnerability_type, "boolean-based blind");
787 }
788
789 #[test]
790 fn builder_pattern_serializes_correctly() {
791 let opts = SqlmapOptions::builder()
792 .url("http://test.com?id=1")
793 .level(3)
794 .risk(2)
795 .batch(true)
796 .threads(4)
797 .tamper("space2comment")
798 .build();
799 let json = serde_json::to_string(&opts).expect("serialize");
800 assert!(json.contains("http://test.com"));
801 assert!(json.contains("\"level\":3"));
802 assert!(json.contains("\"threads\":4"));
803 assert!(json.contains("space2comment"));
804 assert!(!json.contains("dbms"));
806 }
807
808 #[test]
809 fn tech_serializes_as_technique() {
810 let opts = SqlmapOptions::builder().tech("BEUSTQ").build();
811 let json = serde_json::to_string(&opts).expect("serialize");
812 assert!(json.contains("\"technique\":\"BEUSTQ\""));
813 assert!(!json.contains("\"tech\""));
814 }
815
816 #[test]
817 fn type_1_chunk_edge_cases() {
818 let resp = DataResponse {
819 success: true,
820 data: Some(vec![SqlmapDataChunk {
821 r#type: 1,
822 value: serde_json::json!([
823 { "parameter": "username" },
824 "string_instead_of_object_should_be_ignored",
825 { "type": "error-based" },
826 {
827 "parameter": "id",
828 "type": "boolean-based blind",
829 "payload": "id=1 AND 1=1"
830 }
831 ]),
832 }]),
833 error: None,
834 message: None,
835 };
836 let findings = resp.findings();
837 assert_eq!(findings.len(), 1);
838 assert_eq!(findings[0].parameter, "id");
839 assert_eq!(findings[0].vulnerability_type, "boolean-based blind");
840 assert_eq!(findings[0].payload, "id=1 AND 1=1");
841 }
842
843 #[test]
844 fn new_options_fields_serialize() {
845 let opts = SqlmapOptions::builder()
846 .tor(true)
847 .tor_port(9050)
848 .tor_type("SOCKS5")
849 .crawl_depth(3)
850 .second_url("http://verify.com")
851 .tamper("between,randomcase")
852 .retries(5)
853 .dump_all(true)
854 .file_read("/etc/passwd")
855 .build();
856 let json = serde_json::to_string(&opts).expect("serialize");
857 assert!(json.contains("\"tor\":true"));
858 assert!(json.contains("\"torPort\":9050"));
859 assert!(json.contains("\"crawlDepth\":3"));
860 assert!(json.contains("\"secondUrl\""));
861 assert!(json.contains("\"fileRead\""));
862 assert!(json.contains("\"dumpAll\":true"));
863 }
864
865 #[test]
866 fn finding_display() {
867 let finding = SqlmapFinding {
868 parameter: "id".into(),
869 vulnerability_type: "boolean-based blind".into(),
870 payload: "id=1 AND 1=1".into(),
871 details: serde_json::json!({}),
872 };
873 let display = format!("{finding}");
874 assert!(display.contains("boolean-based blind"));
875 assert!(display.contains("id"));
876 }
877
878 #[test]
879 fn format_csv_output() {
880 let findings = vec![SqlmapFinding {
881 parameter: "id".into(),
882 vulnerability_type: "error-based".into(),
883 payload: "' OR 1=1--".into(),
884 details: serde_json::json!({}),
885 }];
886 let csv = format_findings(&findings, OutputFormat::Csv);
887 assert!(csv.starts_with("parameter,vulnerability_type,payload\n"));
888 assert!(csv.contains("error-based"));
889 }
890
891 #[test]
892 fn format_plain_empty() {
893 let plain = format_findings(&[], OutputFormat::Plain);
894 assert_eq!(plain, "No SQL injection findings detected.\n");
895 }
896
897 #[test]
898 fn format_markdown_output() {
899 let findings = vec![SqlmapFinding {
900 parameter: "id".into(),
901 vulnerability_type: "UNION query".into(),
902 payload: "id=1 UNION SELECT 1,2--".into(),
903 details: serde_json::json!({}),
904 }];
905 let md = format_findings(&findings, OutputFormat::Markdown);
906 assert!(md.contains("| Parameter |"));
907 assert!(md.contains("UNION query"));
908 }
909}