Skip to main content

sqlmap_rs/
types.rs

1//! Core type definitions for SQLMap REST API (`sqlmapapi`) payloads.
2//!
3//! Provides strictly-typed request/response structures, a comprehensive
4//! options builder, and multi-format output for scan results.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9// ── Response types ───────────────────────────────────────────────
10
11/// Response when creating a new task.
12#[derive(Debug, Clone, Deserialize)]
13#[non_exhaustive]
14pub struct NewTaskResponse {
15    /// True if task creation succeeded.
16    pub success: bool,
17    /// The unique execution ID assigned to this task.
18    pub taskid: Option<String>,
19    /// Error or informational message.
20    pub message: Option<String>,
21}
22
23/// Generic success response from the API.
24#[derive(Debug, Clone, Deserialize)]
25#[non_exhaustive]
26pub struct BasicResponse {
27    /// True if operation succeeded.
28    pub success: bool,
29    /// Detailed message.
30    pub message: Option<String>,
31}
32
33/// Response containing current execution status.
34#[derive(Debug, Clone, Deserialize)]
35#[non_exhaustive]
36pub struct StatusResponse {
37    /// True if request succeeded.
38    pub success: bool,
39    /// Current engine status ("running", "terminated", etc.).
40    pub status: Option<String>,
41    /// Underlying process exit code (populated on termination).
42    pub returncode: Option<i32>,
43}
44
45/// A chunk of extracted data reported by the SQLMap engine.
46#[derive(Debug, Clone, Deserialize, Serialize)]
47#[non_exhaustive]
48pub struct SqlmapDataChunk {
49    /// Chunk type: 0 (log), 1 (vulnerabilities), 2 (target info), etc.
50    pub r#type: i32,
51    /// The actual JSON payload chunk.
52    pub value: serde_json::Value,
53}
54
55/// Final payload block returning all gathered data for a task.
56#[derive(Debug, Clone, Deserialize)]
57#[non_exhaustive]
58pub struct DataResponse {
59    /// True if fetch succeeded.
60    pub success: bool,
61    /// The aggregated data chunks representing injection results.
62    pub data: Option<Vec<SqlmapDataChunk>>,
63    /// Array of structured errors from the engine.
64    pub error: Option<Vec<String>>,
65    /// Error or informational message when `success` is false.
66    pub message: Option<String>,
67}
68
69/// A log entry from the sqlmap scan execution.
70#[derive(Debug, Clone, Deserialize, Serialize)]
71#[non_exhaustive]
72pub struct LogEntry {
73    /// Log message text.
74    pub message: String,
75    /// Log level string (e.g. "INFO", "WARNING", "ERROR").
76    pub level: String,
77    /// Timestamp of the log entry.
78    pub time: String,
79}
80
81/// Response from the log endpoint.
82#[derive(Debug, Clone, Deserialize)]
83#[non_exhaustive]
84pub struct LogResponse {
85    /// True if fetch succeeded.
86    pub success: bool,
87    /// Array of log entries.
88    pub log: Option<Vec<LogEntry>>,
89    /// Error or informational message when `success` is false.
90    pub message: Option<String>,
91}
92
93// ── Finding types ────────────────────────────────────────────────
94
95/// A parsed finding representing a confirmed SQL injection.
96#[derive(Debug, Clone, Serialize, Deserialize)]
97#[non_exhaustive]
98pub struct SqlmapFinding {
99    /// Original parameter attacked.
100    pub parameter: String,
101    /// Classification of injection technique.
102    pub vulnerability_type: String,
103    /// Raw payload executed against the target.
104    pub payload: String,
105    /// Arbitrary engine output block with full details.
106    pub details: serde_json::Value,
107}
108
109impl SqlmapFinding {
110    /// Creates a new finding with the given fields.
111    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    /// Extract structured findings from the raw data chunks.
144    ///
145    /// Type 1 chunks are sqlmapapi TECHNIQUES payloads: each value entry is an
146    /// injection point with a nested `data[]` of `{title|technique, payload}`.
147    /// Flat legacy objects with top-level `type`/`payload` are still accepted.
148    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                // Legacy flat shape (non-api fixtures / older payloads).
200                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// ── Output formatting ────────────────────────────────────────────
227
228/// Supported output formats for scan results.
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
230#[non_exhaustive]
231pub enum OutputFormat {
232    /// Compact single-line JSON.
233    Json,
234    /// Pretty-printed JSON.
235    JsonPretty,
236    /// Comma-separated values with header row.
237    Csv,
238    /// GitHub-flavored Markdown table.
239    Markdown,
240    /// Human-readable plain text report.
241    Plain,
242}
243
244/// Format a slice of findings in the specified output format.
245pub 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
302/// Escape a value for CSV output.
303fn 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// ── SqlmapOptions ────────────────────────────────────────────────
312
313/// Configuration payload mapped directly to SQLMap CLI arguments.
314///
315/// All fields are optional and use `skip_serializing_if` so only
316/// explicitly set values are sent to the REST API.
317///
318/// # Examples
319///
320/// ```rust
321/// use sqlmap_rs::SqlmapOptions;
322///
323/// let opts = SqlmapOptions::builder()
324///     .url("http://example.com/api?id=1")
325///     .level(3)
326///     .risk(2)
327///     .batch(true)
328///     .threads(4)
329///     .build();
330/// ```
331#[derive(Debug, Clone, Serialize, Default)]
332#[non_exhaustive]
333pub struct SqlmapOptions {
334    // ── Target ──
335    /// The target URL.
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub url: Option<String>,
338
339    /// Target specific parameter(s), e.g. "id".
340    #[serde(rename = "testParameter", skip_serializing_if = "Option::is_none")]
341    pub test_parameter: Option<String>,
342
343    // ── Detection ──
344    /// Specific DBMS backend, e.g. "MySQL".
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub dbms: Option<String>,
347
348    /// Payload techniques to test (B=Boolean, E=Error, U=UNION, S=Stacked, T=Time, Q=Inline query).
349    #[serde(rename = "technique", skip_serializing_if = "Option::is_none")]
350    pub tech: Option<String>,
351
352    /// Level of tests to perform (1-5, default 1).
353    #[serde(skip_serializing_if = "Option::is_none")]
354    pub level: Option<i32>,
355
356    /// Payload risk (1-3, default 1).
357    #[serde(skip_serializing_if = "Option::is_none")]
358    pub risk: Option<i32>,
359
360    /// String to match for True on boolean-based blind injection.
361    #[serde(skip_serializing_if = "Option::is_none")]
362    pub string: Option<String>,
363
364    /// String to match for False on boolean-based blind injection.
365    #[serde(rename = "notString", skip_serializing_if = "Option::is_none")]
366    pub not_string: Option<String>,
367
368    /// Regex to match for True on boolean-based blind injection.
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub regexp: Option<String>,
371
372    /// HTTP code to match for True query.
373    #[serde(skip_serializing_if = "Option::is_none")]
374    pub code: Option<i32>,
375
376    /// Compare responses using text only.
377    #[serde(rename = "textOnly", skip_serializing_if = "Option::is_none")]
378    pub text_only: Option<bool>,
379
380    /// Compare responses using titles only.
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub titles: Option<bool>,
383
384    // ── Request ──
385    /// HTTP Cookie header value.
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub cookie: Option<String>,
388
389    /// HTTP headers string.
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub headers: Option<String>,
392
393    /// Force specific HTTP method.
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub method: Option<String>,
396
397    /// POST data string.
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub data: Option<String>,
400
401    /// Use randomly selected User-Agent.
402    #[serde(rename = "randomAgent", skip_serializing_if = "Option::is_none")]
403    pub random_agent: Option<bool>,
404
405    /// HTTP proxy URL.
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub proxy: Option<String>,
408
409    // ── Injection ──
410    /// Injection payload prefix string.
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub prefix: Option<String>,
413
414    /// Injection payload suffix string.
415    #[serde(skip_serializing_if = "Option::is_none")]
416    pub suffix: Option<String>,
417
418    /// Tamper script(s) for WAF evasion.
419    #[serde(skip_serializing_if = "Option::is_none")]
420    pub tamper: Option<String>,
421
422    /// Skip testing specific parameters.
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub skip: Option<String>,
425
426    /// Skip testing parameters that appear static.
427    #[serde(rename = "skipStatic", skip_serializing_if = "Option::is_none")]
428    pub skip_static: Option<bool>,
429
430    // ── Performance ──
431    /// Number of concurrent threads (default 1).
432    #[serde(skip_serializing_if = "Option::is_none")]
433    pub threads: Option<i32>,
434
435    /// Output verbosity level (1-6).
436    #[serde(skip_serializing_if = "Option::is_none")]
437    pub verbose: Option<i32>,
438
439    /// Do not ask for user input (must be true for automation).
440    #[serde(skip_serializing_if = "Option::is_none")]
441    pub batch: Option<bool>,
442
443    /// Number of retries on connection timeout.
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub retries: Option<i32>,
446
447    // ── Enumeration ──
448    /// Enumerate DBMS databases.
449    #[serde(rename = "getDbs", skip_serializing_if = "Option::is_none")]
450    pub get_dbs: Option<bool>,
451
452    /// Enumerate DBMS database tables.
453    #[serde(rename = "getTables", skip_serializing_if = "Option::is_none")]
454    pub get_tables: Option<bool>,
455
456    /// Enumerate DBMS database columns.
457    #[serde(rename = "getColumns", skip_serializing_if = "Option::is_none")]
458    pub get_columns: Option<bool>,
459
460    /// Enumerate DBMS users.
461    #[serde(rename = "getUsers", skip_serializing_if = "Option::is_none")]
462    pub get_users: Option<bool>,
463
464    /// Enumerate DBMS users password hashes.
465    #[serde(rename = "getPasswordHashes", skip_serializing_if = "Option::is_none")]
466    pub get_passwords: Option<bool>,
467
468    /// Enumerate DBMS users privileges.
469    #[serde(rename = "getPrivileges", skip_serializing_if = "Option::is_none")]
470    pub get_privileges: Option<bool>,
471
472    /// Check if the DBMS user is DBA.
473    #[serde(rename = "isDba", skip_serializing_if = "Option::is_none")]
474    pub is_dba: Option<bool>,
475
476    /// Retrieve the current DBMS user.
477    #[serde(rename = "getCurrentUser", skip_serializing_if = "Option::is_none")]
478    pub current_user: Option<bool>,
479
480    /// Retrieve the current DBMS database.
481    #[serde(rename = "getCurrentDb", skip_serializing_if = "Option::is_none")]
482    pub current_db: Option<bool>,
483
484    /// Dump all DBMS databases tables entries.
485    #[serde(rename = "dumpAll", skip_serializing_if = "Option::is_none")]
486    pub dump_all: Option<bool>,
487
488    /// Dump DBMS database table entries.
489    #[serde(rename = "dumpTable", skip_serializing_if = "Option::is_none")]
490    pub dump_table: Option<bool>,
491
492    /// Search for database/table/column names.
493    #[serde(skip_serializing_if = "Option::is_none")]
494    pub search: Option<bool>,
495
496    // ── OS Access ──
497    /// Prompt for an interactive OS shell.
498    #[serde(rename = "osShell", skip_serializing_if = "Option::is_none")]
499    pub os_shell: Option<bool>,
500
501    /// Prompt for an interactive SQL shell.
502    #[serde(rename = "sqlShell", skip_serializing_if = "Option::is_none")]
503    pub sql_shell: Option<bool>,
504
505    /// Read a file from the DBMS file system.
506    #[serde(rename = "fileRead", skip_serializing_if = "Option::is_none")]
507    pub file_read: Option<String>,
508
509    /// Write a file to the DBMS file system.
510    #[serde(rename = "fileWrite", skip_serializing_if = "Option::is_none")]
511    pub file_write: Option<String>,
512
513    /// Destination path for file write on the DBMS.
514    #[serde(rename = "fileDest", skip_serializing_if = "Option::is_none")]
515    pub file_dest: Option<String>,
516
517    // ── Networking ──
518    /// Use Tor for anonymity.
519    #[serde(skip_serializing_if = "Option::is_none")]
520    pub tor: Option<bool>,
521
522    /// Tor proxy port.
523    #[serde(rename = "torPort", skip_serializing_if = "Option::is_none")]
524    pub tor_port: Option<i32>,
525
526    /// Tor proxy type (HTTP, SOCKS4, SOCKS5).
527    #[serde(rename = "torType", skip_serializing_if = "Option::is_none")]
528    pub tor_type: Option<String>,
529
530    // ── Crawling ──
531    /// Crawl the website from the target URL to given depth.
532    #[serde(rename = "crawlDepth", skip_serializing_if = "Option::is_none")]
533    pub crawl_depth: Option<i32>,
534
535    /// Regex to filter target URLs during crawling.
536    #[serde(skip_serializing_if = "Option::is_none")]
537    pub scope: Option<String>,
538
539    /// Parse and test forms on target pages.
540    #[serde(skip_serializing_if = "Option::is_none")]
541    pub forms: Option<bool>,
542
543    // ── Second-order ──
544    /// URL for second-order injection verification.
545    #[serde(rename = "secondUrl", skip_serializing_if = "Option::is_none")]
546    pub second_url: Option<String>,
547}
548
549/// Builder for constructing [`SqlmapOptions`] with a fluent API.
550///
551/// Every field has a corresponding setter method. Call [`.build()`](SqlmapOptionsBuilder::build)
552/// to finalize.
553#[derive(Debug, Clone, Default)]
554pub struct SqlmapOptionsBuilder {
555    inner: SqlmapOptions,
556}
557
558impl SqlmapOptions {
559    /// Create a new options builder.
560    pub fn builder() -> SqlmapOptionsBuilder {
561        SqlmapOptionsBuilder::default()
562    }
563}
564
565/// Macro to generate builder methods for Option<T> fields.
566macro_rules! builder_method {
567    ($name:ident, $field:ident, String) => {
568        /// Sets the `$name` option.
569        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        /// Sets the `$name` option.
576        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        /// Sets the `$name` option.
583        pub fn $name(mut self, value: i32) -> Self {
584            self.inner.$field = Some(value);
585            self
586        }
587    };
588}
589
590impl SqlmapOptionsBuilder {
591    // Target
592    builder_method!(url, url, String);
593    builder_method!(test_parameter, test_parameter, String);
594
595    // Detection
596    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    // Request
608    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    // Injection
616    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    // Performance
623    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    // Enumeration
629    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    // OS Access
643    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    // Networking
650    builder_method!(tor, tor, bool);
651    builder_method!(tor_port, tor_port, i32);
652    builder_method!(tor_type, tor_type, String);
653
654    // Crawling
655    builder_method!(crawl_depth, crawl_depth, i32);
656    builder_method!(scope, scope, String);
657    builder_method!(forms, forms, bool);
658
659    // Second-order
660    builder_method!(second_url, second_url, String);
661
662    /// Finalize and return the configured [`SqlmapOptions`].
663    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        // None fields should be skipped.
770        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}