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 pub message: Option<String>,
67}
68
69#[derive(Debug, Clone, Deserialize, Serialize)]
71#[non_exhaustive]
72pub struct LogEntry {
73 pub message: String,
75 pub level: String,
77 pub time: String,
79}
80
81#[derive(Debug, Clone, Deserialize)]
83#[non_exhaustive]
84pub struct LogResponse {
85 pub success: bool,
87 pub log: Option<Vec<LogEntry>>,
89 pub message: Option<String>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
97#[non_exhaustive]
98pub struct SqlmapFinding {
99 pub parameter: String,
101 pub vulnerability_type: String,
103 pub payload: String,
105 pub details: serde_json::Value,
107}
108
109impl SqlmapFinding {
110 pub fn new(
112 parameter: impl Into<String>,
113 vulnerability_type: impl Into<String>,
114 payload: impl Into<String>,
115 details: serde_json::Value,
116 ) -> Self {
117 Self {
118 parameter: parameter.into(),
119 vulnerability_type: vulnerability_type.into(),
120 payload: payload.into(),
121 details,
122 }
123 }
124}
125
126impl fmt::Display for SqlmapFinding {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 write!(
129 f,
130 "[SQLi] {vtype} on param '{param}' — payload: {payload}",
131 vtype = self.vulnerability_type,
132 param = self.parameter,
133 payload = self.payload,
134 )
135 }
136}
137
138fn is_confirmed_finding(vulnerability_type: &str, payload: &str) -> bool {
139 !payload.is_empty() && vulnerability_type != "unknown"
140}
141
142impl DataResponse {
143 pub fn findings(&self) -> Vec<SqlmapFinding> {
149 let Some(ref chunks) = self.data else {
150 return vec![];
151 };
152 let mut findings = Vec::new();
153
154 for chunk in chunks {
155 if chunk.r#type != 1 {
156 continue;
157 }
158 let Some(arr) = chunk.value.as_array() else {
159 continue;
160 };
161 for item in arr {
162 let Some(obj) = item.as_object() else {
163 continue;
164 };
165 let parameter = obj
166 .get("parameter")
167 .and_then(|v| v.as_str())
168 .unwrap_or("unknown")
169 .to_string();
170
171 if let Some(nested) = obj.get("data").and_then(|v| v.as_array()) {
172 for tech in nested {
173 let Some(tech_obj) = tech.as_object() else {
174 continue;
175 };
176 let vulnerability_type = tech_obj
177 .get("technique")
178 .or_else(|| tech_obj.get("title"))
179 .and_then(|v| v.as_str())
180 .unwrap_or("unknown")
181 .to_string();
182 let payload = tech_obj
183 .get("payload")
184 .and_then(|v| v.as_str())
185 .unwrap_or("")
186 .to_string();
187 if is_confirmed_finding(&vulnerability_type, &payload) {
188 findings.push(SqlmapFinding {
189 parameter: parameter.clone(),
190 vulnerability_type,
191 payload,
192 details: tech.clone(),
193 });
194 }
195 }
196 continue;
197 }
198
199 let vulnerability_type = obj
201 .get("type")
202 .or_else(|| obj.get("title"))
203 .and_then(|v| v.as_str())
204 .unwrap_or("unknown")
205 .to_string();
206 let payload = obj
207 .get("payload")
208 .and_then(|v| v.as_str())
209 .unwrap_or("")
210 .to_string();
211 if is_confirmed_finding(&vulnerability_type, &payload) {
212 findings.push(SqlmapFinding {
213 parameter,
214 vulnerability_type,
215 payload,
216 details: item.clone(),
217 });
218 }
219 }
220 }
221
222 findings
223 }
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
230#[non_exhaustive]
231pub enum OutputFormat {
232 Json,
234 JsonPretty,
236 Csv,
238 Markdown,
240 Plain,
242}
243
244pub fn format_findings(findings: &[SqlmapFinding], format: OutputFormat) -> String {
246 match format {
247 OutputFormat::Json => match serde_json::to_string(findings) {
248 Ok(json) => json,
249 Err(err) => format!("{{\"error\": \"serialization failed: {err}\"}}"),
250 },
251 OutputFormat::JsonPretty => match serde_json::to_string_pretty(findings) {
252 Ok(json) => json,
253 Err(err) => format!("{{\"error\": \"serialization failed: {err}\"}}"),
254 },
255 OutputFormat::Csv => {
256 let mut buf = String::from("parameter,vulnerability_type,payload\n");
257 for f in findings {
258 buf.push_str(&format!(
259 "{},{},{}\n",
260 csv_escape(&f.parameter),
261 csv_escape(&f.vulnerability_type),
262 csv_escape(&f.payload),
263 ));
264 }
265 buf
266 }
267 OutputFormat::Markdown => {
268 if findings.is_empty() {
269 return "No SQL injection findings.\n".to_string();
270 }
271 let mut buf = String::from("| Parameter | Type | Payload |\n");
272 buf.push_str("|-----------|------|----------|\n");
273 for f in findings {
274 buf.push_str(&format!(
275 "| `{}` | {} | `{}` |\n",
276 f.parameter,
277 f.vulnerability_type,
278 f.payload.replace('|', "\\|"),
279 ));
280 }
281 buf
282 }
283 OutputFormat::Plain => {
284 if findings.is_empty() {
285 return "No SQL injection findings detected.\n".to_string();
286 }
287 let mut buf = format!("=== {} SQLi Finding(s) ===\n\n", findings.len());
288 for (i, f) in findings.iter().enumerate() {
289 buf.push_str(&format!(
290 "#{} {} on param '{}'\n Payload: {}\n\n",
291 i + 1,
292 f.vulnerability_type,
293 f.parameter,
294 f.payload,
295 ));
296 }
297 buf
298 }
299 }
300}
301
302fn csv_escape(value: &str) -> String {
304 if value.contains(',') || value.contains('"') || value.contains('\n') {
305 format!("\"{}\"", value.replace('"', "\"\""))
306 } else {
307 value.to_string()
308 }
309}
310
311#[derive(Debug, Clone, Serialize, Default)]
332#[non_exhaustive]
333pub struct SqlmapOptions {
334 #[serde(skip_serializing_if = "Option::is_none")]
337 pub url: Option<String>,
338
339 #[serde(rename = "testParameter", skip_serializing_if = "Option::is_none")]
341 pub test_parameter: Option<String>,
342
343 #[serde(skip_serializing_if = "Option::is_none")]
346 pub dbms: Option<String>,
347
348 #[serde(rename = "technique", skip_serializing_if = "Option::is_none")]
350 pub tech: Option<String>,
351
352 #[serde(skip_serializing_if = "Option::is_none")]
354 pub level: Option<i32>,
355
356 #[serde(skip_serializing_if = "Option::is_none")]
358 pub risk: Option<i32>,
359
360 #[serde(skip_serializing_if = "Option::is_none")]
362 pub string: Option<String>,
363
364 #[serde(rename = "notString", skip_serializing_if = "Option::is_none")]
366 pub not_string: Option<String>,
367
368 #[serde(skip_serializing_if = "Option::is_none")]
370 pub regexp: Option<String>,
371
372 #[serde(skip_serializing_if = "Option::is_none")]
374 pub code: Option<i32>,
375
376 #[serde(rename = "textOnly", skip_serializing_if = "Option::is_none")]
378 pub text_only: Option<bool>,
379
380 #[serde(skip_serializing_if = "Option::is_none")]
382 pub titles: Option<bool>,
383
384 #[serde(skip_serializing_if = "Option::is_none")]
387 pub cookie: Option<String>,
388
389 #[serde(skip_serializing_if = "Option::is_none")]
391 pub headers: Option<String>,
392
393 #[serde(skip_serializing_if = "Option::is_none")]
395 pub method: Option<String>,
396
397 #[serde(skip_serializing_if = "Option::is_none")]
399 pub data: Option<String>,
400
401 #[serde(rename = "randomAgent", skip_serializing_if = "Option::is_none")]
403 pub random_agent: Option<bool>,
404
405 #[serde(skip_serializing_if = "Option::is_none")]
407 pub proxy: Option<String>,
408
409 #[serde(skip_serializing_if = "Option::is_none")]
412 pub prefix: Option<String>,
413
414 #[serde(skip_serializing_if = "Option::is_none")]
416 pub suffix: Option<String>,
417
418 #[serde(skip_serializing_if = "Option::is_none")]
420 pub tamper: Option<String>,
421
422 #[serde(skip_serializing_if = "Option::is_none")]
424 pub skip: Option<String>,
425
426 #[serde(rename = "skipStatic", skip_serializing_if = "Option::is_none")]
428 pub skip_static: Option<bool>,
429
430 #[serde(skip_serializing_if = "Option::is_none")]
433 pub threads: Option<i32>,
434
435 #[serde(skip_serializing_if = "Option::is_none")]
437 pub verbose: Option<i32>,
438
439 #[serde(skip_serializing_if = "Option::is_none")]
441 pub batch: Option<bool>,
442
443 #[serde(skip_serializing_if = "Option::is_none")]
445 pub retries: Option<i32>,
446
447 #[serde(rename = "getDbs", skip_serializing_if = "Option::is_none")]
450 pub get_dbs: Option<bool>,
451
452 #[serde(rename = "getTables", skip_serializing_if = "Option::is_none")]
454 pub get_tables: Option<bool>,
455
456 #[serde(rename = "getColumns", skip_serializing_if = "Option::is_none")]
458 pub get_columns: Option<bool>,
459
460 #[serde(rename = "getUsers", skip_serializing_if = "Option::is_none")]
462 pub get_users: Option<bool>,
463
464 #[serde(rename = "getPasswordHashes", skip_serializing_if = "Option::is_none")]
466 pub get_passwords: Option<bool>,
467
468 #[serde(rename = "getPrivileges", skip_serializing_if = "Option::is_none")]
470 pub get_privileges: Option<bool>,
471
472 #[serde(rename = "isDba", skip_serializing_if = "Option::is_none")]
474 pub is_dba: Option<bool>,
475
476 #[serde(rename = "getCurrentUser", skip_serializing_if = "Option::is_none")]
478 pub current_user: Option<bool>,
479
480 #[serde(rename = "getCurrentDb", skip_serializing_if = "Option::is_none")]
482 pub current_db: Option<bool>,
483
484 #[serde(rename = "dumpAll", skip_serializing_if = "Option::is_none")]
486 pub dump_all: Option<bool>,
487
488 #[serde(rename = "dumpTable", skip_serializing_if = "Option::is_none")]
490 pub dump_table: Option<bool>,
491
492 #[serde(skip_serializing_if = "Option::is_none")]
494 pub search: Option<bool>,
495
496 #[serde(rename = "osShell", skip_serializing_if = "Option::is_none")]
499 pub os_shell: Option<bool>,
500
501 #[serde(rename = "sqlShell", skip_serializing_if = "Option::is_none")]
503 pub sql_shell: Option<bool>,
504
505 #[serde(rename = "fileRead", skip_serializing_if = "Option::is_none")]
507 pub file_read: Option<String>,
508
509 #[serde(rename = "fileWrite", skip_serializing_if = "Option::is_none")]
511 pub file_write: Option<String>,
512
513 #[serde(rename = "fileDest", skip_serializing_if = "Option::is_none")]
515 pub file_dest: Option<String>,
516
517 #[serde(skip_serializing_if = "Option::is_none")]
520 pub tor: Option<bool>,
521
522 #[serde(rename = "torPort", skip_serializing_if = "Option::is_none")]
524 pub tor_port: Option<i32>,
525
526 #[serde(rename = "torType", skip_serializing_if = "Option::is_none")]
528 pub tor_type: Option<String>,
529
530 #[serde(rename = "crawlDepth", skip_serializing_if = "Option::is_none")]
533 pub crawl_depth: Option<i32>,
534
535 #[serde(skip_serializing_if = "Option::is_none")]
537 pub scope: Option<String>,
538
539 #[serde(skip_serializing_if = "Option::is_none")]
541 pub forms: Option<bool>,
542
543 #[serde(rename = "secondUrl", skip_serializing_if = "Option::is_none")]
546 pub second_url: Option<String>,
547}
548
549#[derive(Debug, Clone, Default)]
554pub struct SqlmapOptionsBuilder {
555 inner: SqlmapOptions,
556}
557
558impl SqlmapOptions {
559 pub fn builder() -> SqlmapOptionsBuilder {
561 SqlmapOptionsBuilder::default()
562 }
563}
564
565macro_rules! builder_method {
567 ($name:ident, $field:ident, String) => {
568 pub fn $name(mut self, value: impl Into<String>) -> Self {
570 self.inner.$field = Some(value.into());
571 self
572 }
573 };
574 ($name:ident, $field:ident, bool) => {
575 pub fn $name(mut self, value: bool) -> Self {
577 self.inner.$field = Some(value);
578 self
579 }
580 };
581 ($name:ident, $field:ident, i32) => {
582 pub fn $name(mut self, value: i32) -> Self {
584 self.inner.$field = Some(value);
585 self
586 }
587 };
588}
589
590impl SqlmapOptionsBuilder {
591 builder_method!(url, url, String);
593 builder_method!(test_parameter, test_parameter, String);
594
595 builder_method!(dbms, dbms, String);
597 builder_method!(tech, tech, String);
598 builder_method!(level, level, i32);
599 builder_method!(risk, risk, i32);
600 builder_method!(string, string, String);
601 builder_method!(not_string, not_string, String);
602 builder_method!(regexp, regexp, String);
603 builder_method!(code, code, i32);
604 builder_method!(text_only, text_only, bool);
605 builder_method!(titles, titles, bool);
606
607 builder_method!(cookie, cookie, String);
609 builder_method!(headers, headers, String);
610 builder_method!(method, method, String);
611 builder_method!(data, data, String);
612 builder_method!(random_agent, random_agent, bool);
613 builder_method!(proxy, proxy, String);
614
615 builder_method!(prefix, prefix, String);
617 builder_method!(suffix, suffix, String);
618 builder_method!(tamper, tamper, String);
619 builder_method!(skip, skip, String);
620 builder_method!(skip_static, skip_static, bool);
621
622 builder_method!(threads, threads, i32);
624 builder_method!(verbose, verbose, i32);
625 builder_method!(batch, batch, bool);
626 builder_method!(retries, retries, i32);
627
628 builder_method!(get_dbs, get_dbs, bool);
630 builder_method!(get_tables, get_tables, bool);
631 builder_method!(get_columns, get_columns, bool);
632 builder_method!(get_users, get_users, bool);
633 builder_method!(get_passwords, get_passwords, bool);
634 builder_method!(get_privileges, get_privileges, bool);
635 builder_method!(is_dba, is_dba, bool);
636 builder_method!(current_user, current_user, bool);
637 builder_method!(current_db, current_db, bool);
638 builder_method!(dump_all, dump_all, bool);
639 builder_method!(dump_table, dump_table, bool);
640 builder_method!(search, search, bool);
641
642 builder_method!(os_shell, os_shell, bool);
644 builder_method!(sql_shell, sql_shell, bool);
645 builder_method!(file_read, file_read, String);
646 builder_method!(file_write, file_write, String);
647 builder_method!(file_dest, file_dest, String);
648
649 builder_method!(tor, tor, bool);
651 builder_method!(tor_port, tor_port, i32);
652 builder_method!(tor_type, tor_type, String);
653
654 builder_method!(crawl_depth, crawl_depth, i32);
656 builder_method!(scope, scope, String);
657 builder_method!(forms, forms, bool);
658
659 builder_method!(second_url, second_url, String);
661
662 pub fn build(self) -> SqlmapOptions {
664 self.inner
665 }
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671
672 #[test]
673 fn empty_data_response_gives_no_findings() {
674 let resp = DataResponse {
675 success: true,
676 data: None,
677 error: None,
678 message: None,
679 };
680 assert!(resp.findings().is_empty());
681 }
682
683 #[test]
684 fn type_0_chunks_ignored() {
685 let resp = DataResponse {
686 success: true,
687 data: Some(vec![SqlmapDataChunk {
688 r#type: 0,
689 value: serde_json::json!("log message"),
690 }]),
691 error: None,
692 message: None,
693 };
694 assert!(resp.findings().is_empty());
695 }
696
697 #[test]
698 fn type_1_nested_techniques_expanded() {
699 let resp = DataResponse {
700 success: true,
701 data: Some(vec![SqlmapDataChunk {
702 r#type: 1,
703 value: serde_json::json!([{
704 "place": "GET",
705 "parameter": "id",
706 "data": [
707 {
708 "title": "AND boolean-based blind - WHERE or HAVING clause",
709 "payload": "id=1 AND 8888=8888"
710 },
711 {
712 "technique": "time-based blind",
713 "payload": "id=1 AND SLEEP(5)"
714 }
715 ]
716 }]),
717 }]),
718 error: None,
719 message: None,
720 };
721 let findings = resp.findings();
722 assert_eq!(findings.len(), 2);
723 assert_eq!(findings[0].parameter, "id");
724 assert_eq!(
725 findings[0].vulnerability_type,
726 "AND boolean-based blind - WHERE or HAVING clause"
727 );
728 assert_eq!(findings[0].payload, "id=1 AND 8888=8888");
729 assert_eq!(findings[1].vulnerability_type, "time-based blind");
730 assert_eq!(findings[1].payload, "id=1 AND SLEEP(5)");
731 }
732
733 #[test]
734 fn type_1_chunk_parsed_as_finding() {
735 let resp = DataResponse {
736 success: true,
737 data: Some(vec![SqlmapDataChunk {
738 r#type: 1,
739 value: serde_json::json!([{
740 "parameter": "id",
741 "type": "boolean-based blind",
742 "payload": "id=1 AND 1=1"
743 }]),
744 }]),
745 error: None,
746 message: None,
747 };
748 let findings = resp.findings();
749 assert_eq!(findings.len(), 1);
750 assert_eq!(findings[0].parameter, "id");
751 assert_eq!(findings[0].vulnerability_type, "boolean-based blind");
752 }
753
754 #[test]
755 fn builder_pattern_serializes_correctly() {
756 let opts = SqlmapOptions::builder()
757 .url("http://test.com?id=1")
758 .level(3)
759 .risk(2)
760 .batch(true)
761 .threads(4)
762 .tamper("space2comment")
763 .build();
764 let json = serde_json::to_string(&opts).expect("serialize");
765 assert!(json.contains("http://test.com"));
766 assert!(json.contains("\"level\":3"));
767 assert!(json.contains("\"threads\":4"));
768 assert!(json.contains("space2comment"));
769 assert!(!json.contains("dbms"));
771 }
772
773 #[test]
774 fn tech_serializes_as_technique() {
775 let opts = SqlmapOptions::builder().tech("BEUSTQ").build();
776 let json = serde_json::to_string(&opts).expect("serialize");
777 assert!(json.contains("\"technique\":\"BEUSTQ\""));
778 assert!(!json.contains("\"tech\""));
779 }
780
781 #[test]
782 fn type_1_chunk_edge_cases() {
783 let resp = DataResponse {
784 success: true,
785 data: Some(vec![SqlmapDataChunk {
786 r#type: 1,
787 value: serde_json::json!([
788 { "parameter": "username" },
789 "string_instead_of_object_should_be_ignored",
790 { "type": "error-based" },
791 {
792 "parameter": "id",
793 "type": "boolean-based blind",
794 "payload": "id=1 AND 1=1"
795 }
796 ]),
797 }]),
798 error: None,
799 message: None,
800 };
801 let findings = resp.findings();
802 assert_eq!(findings.len(), 1);
803 assert_eq!(findings[0].parameter, "id");
804 assert_eq!(findings[0].vulnerability_type, "boolean-based blind");
805 assert_eq!(findings[0].payload, "id=1 AND 1=1");
806 }
807
808 #[test]
809 fn new_options_fields_serialize() {
810 let opts = SqlmapOptions::builder()
811 .tor(true)
812 .tor_port(9050)
813 .tor_type("SOCKS5")
814 .crawl_depth(3)
815 .second_url("http://verify.com")
816 .tamper("between,randomcase")
817 .retries(5)
818 .dump_all(true)
819 .file_read("/etc/passwd")
820 .build();
821 let json = serde_json::to_string(&opts).expect("serialize");
822 assert!(json.contains("\"tor\":true"));
823 assert!(json.contains("\"torPort\":9050"));
824 assert!(json.contains("\"crawlDepth\":3"));
825 assert!(json.contains("\"secondUrl\""));
826 assert!(json.contains("\"fileRead\""));
827 assert!(json.contains("\"dumpAll\":true"));
828 }
829
830 #[test]
831 fn finding_display() {
832 let finding = SqlmapFinding {
833 parameter: "id".into(),
834 vulnerability_type: "boolean-based blind".into(),
835 payload: "id=1 AND 1=1".into(),
836 details: serde_json::json!({}),
837 };
838 let display = format!("{finding}");
839 assert!(display.contains("boolean-based blind"));
840 assert!(display.contains("id"));
841 }
842
843 #[test]
844 fn format_csv_output() {
845 let findings = vec![SqlmapFinding {
846 parameter: "id".into(),
847 vulnerability_type: "error-based".into(),
848 payload: "' OR 1=1--".into(),
849 details: serde_json::json!({}),
850 }];
851 let csv = format_findings(&findings, OutputFormat::Csv);
852 assert!(csv.starts_with("parameter,vulnerability_type,payload\n"));
853 assert!(csv.contains("error-based"));
854 }
855
856 #[test]
857 fn format_plain_empty() {
858 let plain = format_findings(&[], OutputFormat::Plain);
859 assert_eq!(plain, "No SQL injection findings detected.\n");
860 }
861
862 #[test]
863 fn format_markdown_output() {
864 let findings = vec![SqlmapFinding {
865 parameter: "id".into(),
866 vulnerability_type: "UNION query".into(),
867 payload: "id=1 UNION SELECT 1,2--".into(),
868 details: serde_json::json!({}),
869 }];
870 let md = format_findings(&findings, OutputFormat::Markdown);
871 assert!(md.contains("| Parameter |"));
872 assert!(md.contains("UNION query"));
873 }
874}