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> {
141 let Some(ref chunks) = self.data else {
142 return vec![];
143 };
144 let mut findings = Vec::new();
145
146 for chunk in chunks {
147 if chunk.r#type != 1 {
148 continue;
149 }
150 let Some(arr) = chunk.value.as_array() else {
151 continue;
152 };
153 for item in arr {
154 let Some(obj) = item.as_object() else {
155 continue;
156 };
157 let parameter = obj
158 .get("parameter")
159 .and_then(|v| v.as_str())
160 .unwrap_or("unknown")
161 .to_string();
162
163 if let Some(nested) = obj.get("data").and_then(|v| v.as_array()) {
164 for tech in nested {
165 let Some(tech_obj) = tech.as_object() else {
166 continue;
167 };
168 let vulnerability_type = tech_obj
169 .get("technique")
170 .or_else(|| tech_obj.get("title"))
171 .and_then(|v| v.as_str())
172 .unwrap_or("unknown")
173 .to_string();
174 let payload = tech_obj
175 .get("payload")
176 .and_then(|v| v.as_str())
177 .unwrap_or("")
178 .to_string();
179 findings.push(SqlmapFinding {
180 parameter: parameter.clone(),
181 vulnerability_type,
182 payload,
183 details: tech.clone(),
184 });
185 }
186 continue;
187 }
188
189 let vulnerability_type = obj
191 .get("type")
192 .or_else(|| obj.get("title"))
193 .and_then(|v| v.as_str())
194 .unwrap_or("unknown")
195 .to_string();
196 let payload = obj
197 .get("payload")
198 .and_then(|v| v.as_str())
199 .unwrap_or("")
200 .to_string();
201 findings.push(SqlmapFinding {
202 parameter,
203 vulnerability_type,
204 payload,
205 details: item.clone(),
206 });
207 }
208 }
209
210 findings
211 }
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
218#[non_exhaustive]
219pub enum OutputFormat {
220 Json,
222 JsonPretty,
224 Csv,
226 Markdown,
228 Plain,
230}
231
232pub fn format_findings(findings: &[SqlmapFinding], format: OutputFormat) -> String {
234 match format {
235 OutputFormat::Json => match serde_json::to_string(findings) {
236 Ok(json) => json,
237 Err(err) => format!("{{\"error\": \"serialization failed: {err}\"}}"),
238 },
239 OutputFormat::JsonPretty => match serde_json::to_string_pretty(findings) {
240 Ok(json) => json,
241 Err(err) => format!("{{\"error\": \"serialization failed: {err}\"}}"),
242 },
243 OutputFormat::Csv => {
244 let mut buf = String::from("parameter,vulnerability_type,payload\n");
245 for f in findings {
246 buf.push_str(&format!(
247 "{},{},{}\n",
248 csv_escape(&f.parameter),
249 csv_escape(&f.vulnerability_type),
250 csv_escape(&f.payload),
251 ));
252 }
253 buf
254 }
255 OutputFormat::Markdown => {
256 if findings.is_empty() {
257 return "No SQL injection findings.\n".to_string();
258 }
259 let mut buf = String::from("| Parameter | Type | Payload |\n");
260 buf.push_str("|-----------|------|----------|\n");
261 for f in findings {
262 buf.push_str(&format!(
263 "| `{}` | {} | `{}` |\n",
264 f.parameter,
265 f.vulnerability_type,
266 f.payload.replace('|', "\\|"),
267 ));
268 }
269 buf
270 }
271 OutputFormat::Plain => {
272 if findings.is_empty() {
273 return "No SQL injection findings detected.\n".to_string();
274 }
275 let mut buf = format!("=== {} SQLi Finding(s) ===\n\n", findings.len());
276 for (i, f) in findings.iter().enumerate() {
277 buf.push_str(&format!(
278 "#{} {} on param '{}'\n Payload: {}\n\n",
279 i + 1,
280 f.vulnerability_type,
281 f.parameter,
282 f.payload,
283 ));
284 }
285 buf
286 }
287 }
288}
289
290fn csv_escape(value: &str) -> String {
292 if value.contains(',') || value.contains('"') || value.contains('\n') {
293 format!("\"{}\"", value.replace('"', "\"\""))
294 } else {
295 value.to_string()
296 }
297}
298
299#[derive(Debug, Clone, Serialize, Default)]
320#[non_exhaustive]
321pub struct SqlmapOptions {
322 #[serde(skip_serializing_if = "Option::is_none")]
325 pub url: Option<String>,
326
327 #[serde(rename = "testParameter", skip_serializing_if = "Option::is_none")]
329 pub test_parameter: Option<String>,
330
331 #[serde(skip_serializing_if = "Option::is_none")]
334 pub dbms: Option<String>,
335
336 #[serde(skip_serializing_if = "Option::is_none")]
338 pub tech: Option<String>,
339
340 #[serde(skip_serializing_if = "Option::is_none")]
342 pub level: Option<i32>,
343
344 #[serde(skip_serializing_if = "Option::is_none")]
346 pub risk: Option<i32>,
347
348 #[serde(skip_serializing_if = "Option::is_none")]
350 pub string: Option<String>,
351
352 #[serde(rename = "notString", skip_serializing_if = "Option::is_none")]
354 pub not_string: Option<String>,
355
356 #[serde(skip_serializing_if = "Option::is_none")]
358 pub regexp: Option<String>,
359
360 #[serde(skip_serializing_if = "Option::is_none")]
362 pub code: Option<i32>,
363
364 #[serde(rename = "textOnly", skip_serializing_if = "Option::is_none")]
366 pub text_only: Option<bool>,
367
368 #[serde(skip_serializing_if = "Option::is_none")]
370 pub titles: Option<bool>,
371
372 #[serde(skip_serializing_if = "Option::is_none")]
375 pub cookie: Option<String>,
376
377 #[serde(skip_serializing_if = "Option::is_none")]
379 pub headers: Option<String>,
380
381 #[serde(skip_serializing_if = "Option::is_none")]
383 pub method: Option<String>,
384
385 #[serde(skip_serializing_if = "Option::is_none")]
387 pub data: Option<String>,
388
389 #[serde(rename = "randomAgent", skip_serializing_if = "Option::is_none")]
391 pub random_agent: Option<bool>,
392
393 #[serde(skip_serializing_if = "Option::is_none")]
395 pub proxy: Option<String>,
396
397 #[serde(skip_serializing_if = "Option::is_none")]
400 pub prefix: Option<String>,
401
402 #[serde(skip_serializing_if = "Option::is_none")]
404 pub suffix: Option<String>,
405
406 #[serde(skip_serializing_if = "Option::is_none")]
408 pub tamper: Option<String>,
409
410 #[serde(skip_serializing_if = "Option::is_none")]
412 pub skip: Option<String>,
413
414 #[serde(rename = "skipStatic", skip_serializing_if = "Option::is_none")]
416 pub skip_static: Option<bool>,
417
418 #[serde(skip_serializing_if = "Option::is_none")]
421 pub threads: Option<i32>,
422
423 #[serde(skip_serializing_if = "Option::is_none")]
425 pub verbose: Option<i32>,
426
427 #[serde(skip_serializing_if = "Option::is_none")]
429 pub batch: Option<bool>,
430
431 #[serde(skip_serializing_if = "Option::is_none")]
433 pub retries: Option<i32>,
434
435 #[serde(rename = "getDbs", skip_serializing_if = "Option::is_none")]
438 pub get_dbs: Option<bool>,
439
440 #[serde(rename = "getTables", skip_serializing_if = "Option::is_none")]
442 pub get_tables: Option<bool>,
443
444 #[serde(rename = "getColumns", skip_serializing_if = "Option::is_none")]
446 pub get_columns: Option<bool>,
447
448 #[serde(rename = "getUsers", skip_serializing_if = "Option::is_none")]
450 pub get_users: Option<bool>,
451
452 #[serde(rename = "getPasswordHashes", skip_serializing_if = "Option::is_none")]
454 pub get_passwords: Option<bool>,
455
456 #[serde(rename = "getPrivileges", skip_serializing_if = "Option::is_none")]
458 pub get_privileges: Option<bool>,
459
460 #[serde(rename = "isDba", skip_serializing_if = "Option::is_none")]
462 pub is_dba: Option<bool>,
463
464 #[serde(rename = "getCurrentUser", skip_serializing_if = "Option::is_none")]
466 pub current_user: Option<bool>,
467
468 #[serde(rename = "getCurrentDb", skip_serializing_if = "Option::is_none")]
470 pub current_db: Option<bool>,
471
472 #[serde(rename = "dumpAll", skip_serializing_if = "Option::is_none")]
474 pub dump_all: Option<bool>,
475
476 #[serde(rename = "dumpTable", skip_serializing_if = "Option::is_none")]
478 pub dump_table: Option<bool>,
479
480 #[serde(skip_serializing_if = "Option::is_none")]
482 pub search: Option<bool>,
483
484 #[serde(rename = "osShell", skip_serializing_if = "Option::is_none")]
487 pub os_shell: Option<bool>,
488
489 #[serde(rename = "sqlShell", skip_serializing_if = "Option::is_none")]
491 pub sql_shell: Option<bool>,
492
493 #[serde(rename = "fileRead", skip_serializing_if = "Option::is_none")]
495 pub file_read: Option<String>,
496
497 #[serde(rename = "fileWrite", skip_serializing_if = "Option::is_none")]
499 pub file_write: Option<String>,
500
501 #[serde(rename = "fileDest", skip_serializing_if = "Option::is_none")]
503 pub file_dest: Option<String>,
504
505 #[serde(skip_serializing_if = "Option::is_none")]
508 pub tor: Option<bool>,
509
510 #[serde(rename = "torPort", skip_serializing_if = "Option::is_none")]
512 pub tor_port: Option<i32>,
513
514 #[serde(rename = "torType", skip_serializing_if = "Option::is_none")]
516 pub tor_type: Option<String>,
517
518 #[serde(rename = "crawlDepth", skip_serializing_if = "Option::is_none")]
521 pub crawl_depth: Option<i32>,
522
523 #[serde(skip_serializing_if = "Option::is_none")]
525 pub scope: Option<String>,
526
527 #[serde(skip_serializing_if = "Option::is_none")]
529 pub forms: Option<bool>,
530
531 #[serde(rename = "secondUrl", skip_serializing_if = "Option::is_none")]
534 pub second_url: Option<String>,
535}
536
537#[derive(Debug, Clone, Default)]
542pub struct SqlmapOptionsBuilder {
543 inner: SqlmapOptions,
544}
545
546impl SqlmapOptions {
547 pub fn builder() -> SqlmapOptionsBuilder {
549 SqlmapOptionsBuilder::default()
550 }
551}
552
553macro_rules! builder_method {
555 ($name:ident, $field:ident, String) => {
556 pub fn $name(mut self, value: impl Into<String>) -> Self {
558 self.inner.$field = Some(value.into());
559 self
560 }
561 };
562 ($name:ident, $field:ident, bool) => {
563 pub fn $name(mut self, value: bool) -> Self {
565 self.inner.$field = Some(value);
566 self
567 }
568 };
569 ($name:ident, $field:ident, i32) => {
570 pub fn $name(mut self, value: i32) -> Self {
572 self.inner.$field = Some(value);
573 self
574 }
575 };
576}
577
578impl SqlmapOptionsBuilder {
579 builder_method!(url, url, String);
581 builder_method!(test_parameter, test_parameter, String);
582
583 builder_method!(dbms, dbms, String);
585 builder_method!(tech, tech, String);
586 builder_method!(level, level, i32);
587 builder_method!(risk, risk, i32);
588 builder_method!(string, string, String);
589 builder_method!(not_string, not_string, String);
590 builder_method!(regexp, regexp, String);
591 builder_method!(code, code, i32);
592 builder_method!(text_only, text_only, bool);
593 builder_method!(titles, titles, bool);
594
595 builder_method!(cookie, cookie, String);
597 builder_method!(headers, headers, String);
598 builder_method!(method, method, String);
599 builder_method!(data, data, String);
600 builder_method!(random_agent, random_agent, bool);
601 builder_method!(proxy, proxy, String);
602
603 builder_method!(prefix, prefix, String);
605 builder_method!(suffix, suffix, String);
606 builder_method!(tamper, tamper, String);
607 builder_method!(skip, skip, String);
608 builder_method!(skip_static, skip_static, bool);
609
610 builder_method!(threads, threads, i32);
612 builder_method!(verbose, verbose, i32);
613 builder_method!(batch, batch, bool);
614 builder_method!(retries, retries, i32);
615
616 builder_method!(get_dbs, get_dbs, bool);
618 builder_method!(get_tables, get_tables, bool);
619 builder_method!(get_columns, get_columns, bool);
620 builder_method!(get_users, get_users, bool);
621 builder_method!(get_passwords, get_passwords, bool);
622 builder_method!(get_privileges, get_privileges, bool);
623 builder_method!(is_dba, is_dba, bool);
624 builder_method!(current_user, current_user, bool);
625 builder_method!(current_db, current_db, bool);
626 builder_method!(dump_all, dump_all, bool);
627 builder_method!(dump_table, dump_table, bool);
628 builder_method!(search, search, bool);
629
630 builder_method!(os_shell, os_shell, bool);
632 builder_method!(sql_shell, sql_shell, bool);
633 builder_method!(file_read, file_read, String);
634 builder_method!(file_write, file_write, String);
635 builder_method!(file_dest, file_dest, String);
636
637 builder_method!(tor, tor, bool);
639 builder_method!(tor_port, tor_port, i32);
640 builder_method!(tor_type, tor_type, String);
641
642 builder_method!(crawl_depth, crawl_depth, i32);
644 builder_method!(scope, scope, String);
645 builder_method!(forms, forms, bool);
646
647 builder_method!(second_url, second_url, String);
649
650 pub fn build(self) -> SqlmapOptions {
652 self.inner
653 }
654}
655
656#[cfg(test)]
657mod tests {
658 use super::*;
659
660 #[test]
661 fn empty_data_response_gives_no_findings() {
662 let resp = DataResponse {
663 success: true,
664 data: None,
665 error: None,
666 };
667 assert!(resp.findings().is_empty());
668 }
669
670 #[test]
671 fn type_0_chunks_ignored() {
672 let resp = DataResponse {
673 success: true,
674 data: Some(vec![SqlmapDataChunk {
675 r#type: 0,
676 value: serde_json::json!("log message"),
677 }]),
678 error: None,
679 };
680 assert!(resp.findings().is_empty());
681 }
682
683 #[test]
684 fn type_1_nested_techniques_expanded() {
685 let resp = DataResponse {
686 success: true,
687 data: Some(vec![SqlmapDataChunk {
688 r#type: 1,
689 value: serde_json::json!([{
690 "place": "GET",
691 "parameter": "id",
692 "data": [
693 {
694 "title": "AND boolean-based blind - WHERE or HAVING clause",
695 "payload": "id=1 AND 8888=8888"
696 },
697 {
698 "technique": "time-based blind",
699 "payload": "id=1 AND SLEEP(5)"
700 }
701 ]
702 }]),
703 }]),
704 error: None,
705 };
706 let findings = resp.findings();
707 assert_eq!(findings.len(), 2);
708 assert_eq!(findings[0].parameter, "id");
709 assert_eq!(
710 findings[0].vulnerability_type,
711 "AND boolean-based blind - WHERE or HAVING clause"
712 );
713 assert_eq!(findings[0].payload, "id=1 AND 8888=8888");
714 assert_eq!(findings[1].vulnerability_type, "time-based blind");
715 assert_eq!(findings[1].payload, "id=1 AND SLEEP(5)");
716 }
717
718 #[test]
719 fn type_1_chunk_parsed_as_finding() {
720 let resp = DataResponse {
721 success: true,
722 data: Some(vec![SqlmapDataChunk {
723 r#type: 1,
724 value: serde_json::json!([{
725 "parameter": "id",
726 "type": "boolean-based blind",
727 "payload": "id=1 AND 1=1"
728 }]),
729 }]),
730 error: None,
731 };
732 let findings = resp.findings();
733 assert_eq!(findings.len(), 1);
734 assert_eq!(findings[0].parameter, "id");
735 assert_eq!(findings[0].vulnerability_type, "boolean-based blind");
736 }
737
738 #[test]
739 fn builder_pattern_serializes_correctly() {
740 let opts = SqlmapOptions::builder()
741 .url("http://test.com?id=1")
742 .level(3)
743 .risk(2)
744 .batch(true)
745 .threads(4)
746 .tamper("space2comment")
747 .build();
748 let json = serde_json::to_string(&opts).expect("serialize");
749 assert!(json.contains("http://test.com"));
750 assert!(json.contains("\"level\":3"));
751 assert!(json.contains("\"threads\":4"));
752 assert!(json.contains("space2comment"));
753 assert!(!json.contains("dbms"));
755 }
756
757 #[test]
758 fn type_1_chunk_edge_cases() {
759 let resp = DataResponse {
760 success: true,
761 data: Some(vec![SqlmapDataChunk {
762 r#type: 1,
763 value: serde_json::json!([
764 { "parameter": "username" },
765 "string_instead_of_object_should_be_ignored",
766 { "type": "error-based" }
767 ]),
768 }]),
769 error: None,
770 };
771 let findings = resp.findings();
772 assert_eq!(findings.len(), 2);
773 assert_eq!(findings[0].parameter, "username");
774 assert_eq!(findings[0].vulnerability_type, "unknown");
775 assert_eq!(findings[1].parameter, "unknown");
776 assert_eq!(findings[1].vulnerability_type, "error-based");
777 }
778
779 #[test]
780 fn new_options_fields_serialize() {
781 let opts = SqlmapOptions::builder()
782 .tor(true)
783 .tor_port(9050)
784 .tor_type("SOCKS5")
785 .crawl_depth(3)
786 .second_url("http://verify.com")
787 .tamper("between,randomcase")
788 .retries(5)
789 .dump_all(true)
790 .file_read("/etc/passwd")
791 .build();
792 let json = serde_json::to_string(&opts).expect("serialize");
793 assert!(json.contains("\"tor\":true"));
794 assert!(json.contains("\"torPort\":9050"));
795 assert!(json.contains("\"crawlDepth\":3"));
796 assert!(json.contains("\"secondUrl\""));
797 assert!(json.contains("\"fileRead\""));
798 assert!(json.contains("\"dumpAll\":true"));
799 }
800
801 #[test]
802 fn finding_display() {
803 let finding = SqlmapFinding {
804 parameter: "id".into(),
805 vulnerability_type: "boolean-based blind".into(),
806 payload: "id=1 AND 1=1".into(),
807 details: serde_json::json!({}),
808 };
809 let display = format!("{finding}");
810 assert!(display.contains("boolean-based blind"));
811 assert!(display.contains("id"));
812 }
813
814 #[test]
815 fn format_csv_output() {
816 let findings = vec![SqlmapFinding {
817 parameter: "id".into(),
818 vulnerability_type: "error-based".into(),
819 payload: "' OR 1=1--".into(),
820 details: serde_json::json!({}),
821 }];
822 let csv = format_findings(&findings, OutputFormat::Csv);
823 assert!(csv.starts_with("parameter,vulnerability_type,payload\n"));
824 assert!(csv.contains("error-based"));
825 }
826
827 #[test]
828 fn format_plain_empty() {
829 let plain = format_findings(&[], OutputFormat::Plain);
830 assert_eq!(plain, "No SQL injection findings detected.\n");
831 }
832
833 #[test]
834 fn format_markdown_output() {
835 let findings = vec![SqlmapFinding {
836 parameter: "id".into(),
837 vulnerability_type: "UNION query".into(),
838 payload: "id=1 UNION SELECT 1,2--".into(),
839 details: serde_json::json!({}),
840 }];
841 let md = format_findings(&findings, OutputFormat::Markdown);
842 assert!(md.contains("| Parameter |"));
843 assert!(md.contains("UNION query"));
844 }
845}