1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum InputType {
13 #[default]
15 String,
16 Array,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct Input {
23 pub name: String,
25
26 #[serde(default, rename = "type")]
28 pub input_type: InputType,
29
30 #[serde(default)]
32 pub label: Option<String>,
33
34 #[serde(default)]
36 pub description: Option<String>,
37
38 #[serde(default)]
40 pub default: Option<String>,
41
42 #[serde(default = "default_true")]
44 pub required: bool,
45
46 #[serde(default)]
48 pub example: Option<String>,
49}
50
51fn default_true() -> bool {
52 true
53}
54
55pub fn parse_input_value(input: &str) -> Result<String, String> {
101 use serde_json::Value;
102
103 let trimmed = input.trim();
104
105 if trimmed.is_empty() {
107 return Err("Input value cannot be empty".to_string());
108 }
109
110 match serde_json::from_str::<Value>(trimmed) {
112 Ok(Value::String(s)) => {
113 Ok(s)
115 }
116 Ok(Value::Array(arr)) => {
117 let mut items = Vec::new();
119 for (idx, item) in arr.iter().enumerate() {
120 match item {
121 Value::String(s) => items.push(s.clone()),
122 _ => {
123 return Err(format!(
124 "Array item at index {} must be a string, found: {}",
125 idx,
126 match item {
127 Value::Null => "null",
128 Value::Bool(_) => "boolean",
129 Value::Number(_) => "number",
130 Value::Array(_) => "nested array",
131 Value::Object(_) => "object",
132 Value::String(_) => unreachable!(),
133 }
134 ));
135 }
136 }
137 }
138 Ok(items.join(","))
139 }
140 Ok(other) => {
141 Err(format!(
143 "Input must be a JSON string or array, found: {}",
144 match other {
145 Value::Null => "null",
146 Value::Bool(_) => "boolean",
147 Value::Number(_) => "number",
148 Value::Object(_) => "object",
149 _ => unreachable!(),
150 }
151 ))
152 }
153 Err(_) => {
154 Ok(trimmed.to_string())
157 }
158 }
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct Step {
164 pub name: String,
166
167 #[serde(default, rename = "type")]
169 pub step_type: StepType,
170
171 #[serde(default)]
173 pub query: Option<String>,
174
175 #[serde(default)]
177 pub timespan: Option<String>,
178
179 #[serde(default)]
181 pub request: Option<HttpRequest>,
182
183 #[serde(default)]
185 pub response: Option<HttpResponse>,
186
187 #[serde(default)]
189 pub source: Option<FileSource>,
190
191 #[serde(default)]
193 pub rate_limit: Option<RateLimitConfig>,
194
195 #[serde(default)]
197 pub on_error: Option<OnError>,
198
199 #[serde(default)]
201 pub depends_on: Vec<String>,
202
203 #[serde(default)]
205 pub when: Option<String>,
206
207 #[serde(default)]
209 pub options: Option<StepOptions>,
210
211 #[serde(default)]
215 pub examples: HashMap<String, ExampleValue>,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
220#[serde(untagged)]
221pub enum ExampleValue {
222 Single(String),
224 Array(Vec<String>),
226}
227
228#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
233#[serde(rename_all = "lowercase")]
234pub enum AcquisitionStepType {
235 #[default]
236 Kql,
237 Http,
238 File,
239}
240
241pub use AcquisitionStepType as StepType;
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct HttpRequest {
247 pub method: HttpMethod,
249
250 pub url: String,
252
253 #[serde(default)]
255 pub params: HashMap<String, String>,
256
257 #[serde(default)]
259 pub headers: HashMap<String, String>,
260
261 #[serde(default)]
263 pub body: Option<serde_json::Value>,
264
265 #[serde(default)]
267 pub auth: Option<AuthMethod>,
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
272#[serde(rename_all = "UPPERCASE")]
273pub enum HttpMethod {
274 Get,
275 Post,
276 Put,
277 Delete,
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
282#[serde(rename_all = "lowercase")]
283pub enum AuthMethod {
284 Azure,
286 None,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct HttpResponse {
293 #[serde(default)]
295 pub fields: HashMap<String, String>,
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct FileSource {
301 pub path: String,
303
304 #[serde(default)]
306 pub format: Option<FileFormat>,
307
308 #[serde(default)]
310 pub csv: Option<CsvOptions>,
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
315#[serde(rename_all = "lowercase")]
316pub enum FileFormat {
317 Csv,
318 Json,
319 Yaml,
320}
321
322#[derive(Debug, Clone, Default, Serialize, Deserialize)]
324pub struct CsvOptions {
325 #[serde(default)]
327 pub delimiter: Option<char>,
328
329 #[serde(default)]
331 pub has_header: Option<bool>,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct RateLimitConfig {
337 pub requests: u32,
339
340 pub per: RateLimitPeriod,
342}
343
344#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
346#[serde(rename_all = "lowercase")]
347pub enum RateLimitPeriod {
348 Second,
349 Minute,
350 Hour,
351}
352
353#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
355#[serde(rename_all = "lowercase")]
356pub enum OnError {
357 #[default]
359 Fail,
360 Skip,
362 Continue,
364}
365
366#[derive(Debug, Clone, Default, Serialize, Deserialize)]
368pub struct StepOptions {
369 #[serde(default)]
371 pub quote_style: Option<QuoteStyle>,
372
373 #[serde(default)]
375 pub dedupe: Option<bool>,
376
377 #[serde(default)]
379 pub chunk_size: Option<usize>,
380}
381
382#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
384#[serde(rename_all = "lowercase")]
385pub enum QuoteStyle {
386 #[default]
388 Single,
389 Double,
391 Verbatim,
393}
394
395impl QuoteStyle {
396 pub fn format_value(&self, value: &str) -> String {
398 match self {
399 QuoteStyle::Single => {
400 let escaped = value.replace('\'', "''");
401 format!("'{}'", escaped)
402 }
403 QuoteStyle::Double => {
404 let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
405 format!("\"{}\"", escaped)
406 }
407 QuoteStyle::Verbatim => {
408 let escaped = value.replace('\'', "''");
409 format!("@'{}'", escaped)
410 }
411 }
412 }
413
414 pub fn format_array(&self, values: &[String]) -> String {
416 values
417 .iter()
418 .map(|v| self.format_value(v))
419 .collect::<Vec<_>>()
420 .join(",")
421 }
422}
423
424#[derive(Debug, Clone, Default, Serialize, Deserialize)]
426pub struct OutputConfig {
427 #[serde(default)]
429 pub folder: Option<String>,
430}
431
432#[derive(Debug, Clone, Default, Serialize, Deserialize)]
434pub struct SecretsConfig {
435 #[serde(flatten)]
437 pub secrets: HashMap<String, String>,
438}
439
440#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
448 fn test_quote_styles() {
449 assert_eq!(QuoteStyle::Single.format_value("test"), "'test'");
450 assert_eq!(QuoteStyle::Single.format_value("O'Brien"), "'O''Brien'");
451 assert_eq!(QuoteStyle::Double.format_value("test"), "\"test\"");
452 assert_eq!(QuoteStyle::Verbatim.format_value("test"), "@'test'");
453 }
454
455 #[test]
456 fn test_parse_input_value_json_arrays() {
457 assert_eq!(
459 parse_input_value(r#"["8.8.8.8", "1.1.1.1"]"#).unwrap(),
460 "8.8.8.8,1.1.1.1"
461 );
462
463 assert_eq!(
465 parse_input_value(r#"["single"]"#).unwrap(),
466 "single"
467 );
468
469 assert_eq!(
471 parse_input_value(r#"[]"#).unwrap(),
472 ""
473 );
474
475 assert_eq!(
477 parse_input_value(r#"["item1" , "item2" , "item3"]"#).unwrap(),
478 "item1,item2,item3"
479 );
480
481 assert_eq!(
483 parse_input_value(r#"["item \"with\" quotes"]"#).unwrap(),
484 r#"item "with" quotes"#
485 );
486
487 assert_eq!(
489 parse_input_value(r#"["value,with,commas", "normal"]"#).unwrap(),
490 "value,with,commas,normal"
491 );
492 }
493
494 #[test]
495 fn test_parse_input_value_json_array_errors() {
496 assert!(parse_input_value(r#"[1, 2, 3]"#).is_err());
498 assert!(parse_input_value(r#"[true, false]"#).is_err());
499 assert!(parse_input_value(r#"[null]"#).is_err());
500 assert!(parse_input_value(r#"[{"key": "value"}]"#).is_err());
501 assert!(parse_input_value(r#"[["nested"]]"#).is_err());
502 }
503
504 #[test]
505 fn test_parse_input_value_json_strings() {
506 assert_eq!(
508 parse_input_value(r#""some value""#).unwrap(),
509 "some value"
510 );
511
512 assert_eq!(
514 parse_input_value(r#""value with \"quotes\"""#).unwrap(),
515 r#"value with "quotes""#
516 );
517
518 assert_eq!(
520 parse_input_value(r#""a,b,c""#).unwrap(),
521 "a,b,c"
522 );
523
524 assert_eq!(
526 parse_input_value(r#""path\\to\\file""#).unwrap(),
527 r#"path\to\file"#
528 );
529
530 assert_eq!(
532 parse_input_value(r#""hello\u0020world""#).unwrap(),
533 "hello world"
534 );
535
536 assert_eq!(parse_input_value(r#""""#).unwrap(), "");
538 }
539
540 #[test]
541 fn test_parse_input_value_invalid_json_fallback() {
542 assert_eq!(parse_input_value(r#"'single quotes'"#).unwrap(), "'single quotes'");
546
547 assert_eq!(parse_input_value(r#"[',',']"#).unwrap(), "[',',']");
549 }
550
551 #[test]
552 fn test_parse_input_value_json_type_rejection() {
553 assert!(parse_input_value("123").is_err());
555 assert!(parse_input_value("123.45").is_err());
556
557 assert!(parse_input_value("true").is_err());
559 assert!(parse_input_value("false").is_err());
560
561 assert!(parse_input_value("null").is_err());
563
564 assert!(parse_input_value(r#"{"key": "value"}"#).is_err());
566 }
567
568 #[test]
569 fn test_parse_input_value_raw_fallback() {
570 assert_eq!(parse_input_value("P7D").unwrap(), "P7D");
574 assert_eq!(parse_input_value("admin").unwrap(), "admin");
575
576 assert_eq!(
578 parse_input_value("8.8.8.8,1.1.1.1").unwrap(),
579 "8.8.8.8,1.1.1.1"
580 );
581
582 assert_eq!(
584 parse_input_value("https://example.com").unwrap(),
585 "https://example.com"
586 );
587
588 assert_eq!(parse_input_value("user@domain.com").unwrap(), "user@domain.com");
590 }
591
592 #[test]
593 fn test_parse_input_value_edge_cases() {
594 assert!(parse_input_value("").is_err());
596
597 assert!(parse_input_value(" ").is_err());
599
600 assert!(parse_input_value("7").is_err()); assert!(parse_input_value("true").is_err()); }
604}