Skip to main content

mockforge_bench/
command.rs

1//! Bench command implementation
2
3use crate::crud_flow::{CrudFlowConfig, CrudFlowDetector};
4use crate::data_driven::{DataDistribution, DataDrivenConfig, DataDrivenGenerator, DataMapping};
5use crate::dynamic_params::{DynamicParamProcessor, DynamicPlaceholder};
6use crate::error::{BenchError, Result};
7use crate::executor::K6Executor;
8use crate::invalid_data::{InvalidDataConfig, InvalidDataGenerator};
9use crate::k6_gen::{K6Config, K6ScriptGenerator};
10use crate::mock_integration::{
11    MockIntegrationConfig, MockIntegrationGenerator, MockServerDetector,
12};
13use crate::owasp_api::{OwaspApiConfig, OwaspApiGenerator, OwaspCategory, ReportFormat};
14use crate::parallel_executor::{AggregatedResults, ParallelExecutor};
15use crate::parallel_requests::{ParallelConfig, ParallelRequestGenerator};
16use crate::param_overrides::ParameterOverrides;
17use crate::reporter::TerminalReporter;
18use crate::request_gen::RequestGenerator;
19use crate::scenarios::LoadScenario;
20use crate::security_payloads::{
21    SecurityCategory, SecurityPayload, SecurityPayloads, SecurityTestConfig, SecurityTestGenerator,
22};
23use crate::spec_dependencies::{
24    topological_sort, DependencyDetector, ExtractedValues, SpecDependencyConfig,
25};
26use crate::spec_parser::SpecParser;
27use crate::target_parser::parse_targets_file;
28use crate::wafbench::WafBenchLoader;
29use mockforge_core::openapi::multi_spec::{
30    load_specs_from_directory, load_specs_from_files, merge_specs, ConflictStrategy,
31};
32use mockforge_core::openapi::spec::OpenApiSpec;
33use std::collections::{HashMap, HashSet};
34use std::path::PathBuf;
35use std::str::FromStr;
36
37/// Bench command configuration
38pub struct BenchCommand {
39    /// OpenAPI spec file(s) - can specify multiple
40    pub spec: Vec<PathBuf>,
41    /// Directory containing OpenAPI spec files (discovers .json, .yaml, .yml files)
42    pub spec_dir: Option<PathBuf>,
43    /// Conflict resolution strategy when merging multiple specs: "error" (default), "first", "last"
44    pub merge_conflicts: String,
45    /// Spec mode: "merge" (default) combines all specs, "sequential" runs them in order
46    pub spec_mode: String,
47    /// Dependency configuration file for cross-spec value passing (used with sequential mode)
48    pub dependency_config: Option<PathBuf>,
49    pub target: String,
50    /// API base path prefix (e.g., "/api" or "/v2/api")
51    /// If None, extracts from OpenAPI spec's servers URL
52    pub base_path: Option<String>,
53    pub duration: String,
54    pub vus: u32,
55    pub scenario: String,
56    pub operations: Option<String>,
57    /// Exclude operations from testing (comma-separated)
58    ///
59    /// Supports "METHOD /path" or just "METHOD" to exclude all operations of that type.
60    pub exclude_operations: Option<String>,
61    pub auth: Option<String>,
62    pub headers: Option<String>,
63    pub output: PathBuf,
64    pub generate_only: bool,
65    pub script_output: Option<PathBuf>,
66    pub threshold_percentile: String,
67    pub threshold_ms: u64,
68    pub max_error_rate: f64,
69    pub verbose: bool,
70    pub skip_tls_verify: bool,
71    /// Optional file containing multiple targets
72    pub targets_file: Option<PathBuf>,
73    /// Maximum number of parallel executions (for multi-target mode)
74    pub max_concurrency: Option<u32>,
75    /// Results format: "per-target", "aggregated", or "both"
76    pub results_format: String,
77    /// Optional file containing parameter value overrides (JSON or YAML)
78    ///
79    /// Allows users to provide custom values for path parameters, query parameters,
80    /// headers, and request bodies instead of auto-generated placeholder values.
81    pub params_file: Option<PathBuf>,
82
83    // === CRUD Flow Options ===
84    /// Enable CRUD flow mode
85    pub crud_flow: bool,
86    /// Custom CRUD flow configuration file
87    pub flow_config: Option<PathBuf>,
88    /// Fields to extract from responses
89    pub extract_fields: Option<String>,
90
91    // === Parallel Execution Options ===
92    /// Number of resources to create in parallel
93    pub parallel_create: Option<u32>,
94
95    // === Data-Driven Testing Options ===
96    /// Test data file (CSV or JSON)
97    pub data_file: Option<PathBuf>,
98    /// Data distribution strategy
99    pub data_distribution: String,
100    /// Data column to field mappings
101    pub data_mappings: Option<String>,
102    /// Enable per-URI control mode (each row specifies method, uri, body, etc.)
103    pub per_uri_control: bool,
104
105    // === Invalid Data Testing Options ===
106    /// Percentage of requests with invalid data
107    pub error_rate: Option<f64>,
108    /// Types of invalid data to generate
109    pub error_types: Option<String>,
110
111    // === Security Testing Options ===
112    /// Enable security testing
113    pub security_test: bool,
114    /// Custom security payloads file
115    pub security_payloads: Option<PathBuf>,
116    /// Security test categories
117    pub security_categories: Option<String>,
118    /// Fields to target for security injection
119    pub security_target_fields: Option<String>,
120
121    // === WAFBench Integration ===
122    /// WAFBench test directory or glob pattern for loading CRS attack patterns
123    pub wafbench_dir: Option<String>,
124    /// Cycle through ALL WAFBench payloads instead of random sampling
125    pub wafbench_cycle_all: bool,
126
127    // === OpenAPI 3.0.0 Conformance Testing ===
128    /// Enable conformance testing mode
129    pub conformance: bool,
130    /// API key for conformance security tests
131    pub conformance_api_key: Option<String>,
132    /// Basic auth credentials for conformance security tests (user:pass)
133    pub conformance_basic_auth: Option<String>,
134    /// Conformance report output file
135    pub conformance_report: PathBuf,
136    /// Conformance categories to test (comma-separated, e.g. "parameters,security")
137    pub conformance_categories: Option<String>,
138    /// Conformance report format: "json" or "sarif"
139    pub conformance_report_format: String,
140
141    // === OWASP API Security Top 10 Testing ===
142    /// Enable OWASP API Security Top 10 testing mode
143    pub owasp_api_top10: bool,
144    /// OWASP API categories to test (comma-separated)
145    pub owasp_categories: Option<String>,
146    /// Authorization header name for OWASP auth tests
147    pub owasp_auth_header: String,
148    /// Valid authorization token for OWASP baseline requests
149    pub owasp_auth_token: Option<String>,
150    /// File containing admin/privileged paths to test
151    pub owasp_admin_paths: Option<PathBuf>,
152    /// Fields containing resource IDs for BOLA testing
153    pub owasp_id_fields: Option<String>,
154    /// OWASP report output file
155    pub owasp_report: Option<PathBuf>,
156    /// OWASP report format (json, sarif)
157    pub owasp_report_format: String,
158    /// Number of iterations per VU for OWASP tests (default: 1)
159    pub owasp_iterations: u32,
160}
161
162impl BenchCommand {
163    /// Load and merge specs from --spec files and --spec-dir
164    pub async fn load_and_merge_specs(&self) -> Result<OpenApiSpec> {
165        let mut all_specs: Vec<(PathBuf, OpenApiSpec)> = Vec::new();
166
167        // Load specs from --spec flags
168        if !self.spec.is_empty() {
169            let specs = load_specs_from_files(self.spec.clone())
170                .await
171                .map_err(|e| BenchError::Other(format!("Failed to load spec files: {}", e)))?;
172            all_specs.extend(specs);
173        }
174
175        // Load specs from --spec-dir if provided
176        if let Some(spec_dir) = &self.spec_dir {
177            let dir_specs = load_specs_from_directory(spec_dir).await.map_err(|e| {
178                BenchError::Other(format!("Failed to load specs from directory: {}", e))
179            })?;
180            all_specs.extend(dir_specs);
181        }
182
183        if all_specs.is_empty() {
184            return Err(BenchError::Other(
185                "No spec files provided. Use --spec or --spec-dir.".to_string(),
186            ));
187        }
188
189        // If only one spec, return it directly (extract just the OpenApiSpec)
190        if all_specs.len() == 1 {
191            // Safe to unwrap because we just checked len() == 1
192            return Ok(all_specs.into_iter().next().expect("checked len() == 1 above").1);
193        }
194
195        // Merge multiple specs
196        let conflict_strategy = match self.merge_conflicts.as_str() {
197            "first" => ConflictStrategy::First,
198            "last" => ConflictStrategy::Last,
199            _ => ConflictStrategy::Error,
200        };
201
202        merge_specs(all_specs, conflict_strategy)
203            .map_err(|e| BenchError::Other(format!("Failed to merge specs: {}", e)))
204    }
205
206    /// Get a display name for the spec(s)
207    fn get_spec_display_name(&self) -> String {
208        if self.spec.len() == 1 {
209            self.spec[0].to_string_lossy().to_string()
210        } else if !self.spec.is_empty() {
211            format!("{} spec files", self.spec.len())
212        } else if let Some(dir) = &self.spec_dir {
213            format!("specs from {}", dir.display())
214        } else {
215            "no specs".to_string()
216        }
217    }
218
219    /// Execute the bench command
220    pub async fn execute(&self) -> Result<()> {
221        // Check if we're in multi-target mode
222        if let Some(targets_file) = &self.targets_file {
223            return self.execute_multi_target(targets_file).await;
224        }
225
226        // Check if we're in sequential spec mode (for dependency handling)
227        if self.spec_mode == "sequential" && (self.spec.len() > 1 || self.spec_dir.is_some()) {
228            return self.execute_sequential_specs().await;
229        }
230
231        // Single target mode (existing behavior)
232        // Print header
233        TerminalReporter::print_header(
234            &self.get_spec_display_name(),
235            &self.target,
236            0, // Will be updated later
237            &self.scenario,
238            Self::parse_duration(&self.duration)?,
239        );
240
241        // Validate k6 installation
242        if !K6Executor::is_k6_installed() {
243            TerminalReporter::print_error("k6 is not installed");
244            TerminalReporter::print_warning(
245                "Install k6 from: https://k6.io/docs/get-started/installation/",
246            );
247            return Err(BenchError::K6NotFound);
248        }
249
250        // Check for conformance testing mode (before spec loading — conformance doesn't need a user spec)
251        if self.conformance {
252            return self.execute_conformance_test().await;
253        }
254
255        // Load and parse spec(s)
256        TerminalReporter::print_progress("Loading OpenAPI specification(s)...");
257        let merged_spec = self.load_and_merge_specs().await?;
258        let parser = SpecParser::from_spec(merged_spec);
259        if self.spec.len() > 1 || self.spec_dir.is_some() {
260            TerminalReporter::print_success(&format!(
261                "Loaded and merged {} specification(s)",
262                self.spec.len() + self.spec_dir.as_ref().map(|_| 1).unwrap_or(0)
263            ));
264        } else {
265            TerminalReporter::print_success("Specification loaded");
266        }
267
268        // Check for mock server integration
269        let mock_config = self.build_mock_config().await;
270        if mock_config.is_mock_server {
271            TerminalReporter::print_progress("Mock server integration enabled");
272        }
273
274        // Check for CRUD flow mode
275        if self.crud_flow {
276            return self.execute_crud_flow(&parser).await;
277        }
278
279        // Check for OWASP API Top 10 testing mode
280        if self.owasp_api_top10 {
281            return self.execute_owasp_test(&parser).await;
282        }
283
284        // Get operations
285        TerminalReporter::print_progress("Extracting API operations...");
286        let mut operations = if let Some(filter) = &self.operations {
287            parser.filter_operations(filter)?
288        } else {
289            parser.get_operations()
290        };
291
292        // Apply exclusions if provided
293        if let Some(exclude) = &self.exclude_operations {
294            let before_count = operations.len();
295            operations = parser.exclude_operations(operations, exclude)?;
296            let excluded_count = before_count - operations.len();
297            if excluded_count > 0 {
298                TerminalReporter::print_progress(&format!(
299                    "Excluded {} operations matching '{}'",
300                    excluded_count, exclude
301                ));
302            }
303        }
304
305        if operations.is_empty() {
306            return Err(BenchError::Other("No operations found in spec".to_string()));
307        }
308
309        TerminalReporter::print_success(&format!("Found {} operations", operations.len()));
310
311        // Load parameter overrides if provided
312        let param_overrides = if let Some(params_file) = &self.params_file {
313            TerminalReporter::print_progress("Loading parameter overrides...");
314            let overrides = ParameterOverrides::from_file(params_file)?;
315            TerminalReporter::print_success(&format!(
316                "Loaded parameter overrides ({} operation-specific, {} defaults)",
317                overrides.operations.len(),
318                if overrides.defaults.is_empty() { 0 } else { 1 }
319            ));
320            Some(overrides)
321        } else {
322            None
323        };
324
325        // Generate request templates
326        TerminalReporter::print_progress("Generating request templates...");
327        let templates: Vec<_> = operations
328            .iter()
329            .map(|op| {
330                let op_overrides = param_overrides.as_ref().map(|po| {
331                    po.get_for_operation(op.operation_id.as_deref(), &op.method, &op.path)
332                });
333                RequestGenerator::generate_template_with_overrides(op, op_overrides.as_ref())
334            })
335            .collect::<Result<Vec<_>>>()?;
336        TerminalReporter::print_success("Request templates generated");
337
338        // Parse headers
339        let custom_headers = self.parse_headers()?;
340
341        // Resolve base path (CLI option takes priority over spec's servers URL)
342        let base_path = self.resolve_base_path(&parser);
343        if let Some(ref bp) = base_path {
344            TerminalReporter::print_progress(&format!("Using base path: {}", bp));
345        }
346
347        // Generate k6 script
348        TerminalReporter::print_progress("Generating k6 load test script...");
349        let scenario =
350            LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
351
352        let security_testing_enabled = self.security_test || self.wafbench_dir.is_some();
353
354        let k6_config = K6Config {
355            target_url: self.target.clone(),
356            base_path,
357            scenario,
358            duration_secs: Self::parse_duration(&self.duration)?,
359            max_vus: self.vus,
360            threshold_percentile: self.threshold_percentile.clone(),
361            threshold_ms: self.threshold_ms,
362            max_error_rate: self.max_error_rate,
363            auth_header: self.auth.clone(),
364            custom_headers,
365            skip_tls_verify: self.skip_tls_verify,
366            security_testing_enabled,
367        };
368
369        let generator = K6ScriptGenerator::new(k6_config, templates);
370        let mut script = generator.generate()?;
371        TerminalReporter::print_success("k6 script generated");
372
373        // Check if any advanced features are enabled
374        let has_advanced_features = self.data_file.is_some()
375            || self.error_rate.is_some()
376            || self.security_test
377            || self.parallel_create.is_some()
378            || self.wafbench_dir.is_some();
379
380        // Enhance script with advanced features
381        if has_advanced_features {
382            script = self.generate_enhanced_script(&script)?;
383        }
384
385        // Add mock server integration code
386        if mock_config.is_mock_server {
387            let setup_code = MockIntegrationGenerator::generate_setup(&mock_config);
388            let teardown_code = MockIntegrationGenerator::generate_teardown(&mock_config);
389            let helper_code = MockIntegrationGenerator::generate_vu_id_helper();
390
391            // Insert mock server code after imports
392            if let Some(import_end) = script.find("export const options") {
393                script.insert_str(
394                    import_end,
395                    &format!(
396                        "\n// === Mock Server Integration ===\n{}\n{}\n{}\n",
397                        helper_code, setup_code, teardown_code
398                    ),
399                );
400            }
401        }
402
403        // Validate the generated script
404        TerminalReporter::print_progress("Validating k6 script...");
405        let validation_errors = K6ScriptGenerator::validate_script(&script);
406        if !validation_errors.is_empty() {
407            TerminalReporter::print_error("Script validation failed");
408            for error in &validation_errors {
409                eprintln!("  {}", error);
410            }
411            return Err(BenchError::Other(format!(
412                "Generated k6 script has {} validation error(s). Please check the output above.",
413                validation_errors.len()
414            )));
415        }
416        TerminalReporter::print_success("Script validation passed");
417
418        // Write script to file
419        let script_path = if let Some(output) = &self.script_output {
420            output.clone()
421        } else {
422            self.output.join("k6-script.js")
423        };
424
425        if let Some(parent) = script_path.parent() {
426            std::fs::create_dir_all(parent)?;
427        }
428        std::fs::write(&script_path, &script)?;
429        TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
430
431        // If generate-only mode, exit here
432        if self.generate_only {
433            println!("\nScript generated successfully. Run it with:");
434            println!("  k6 run {}", script_path.display());
435            return Ok(());
436        }
437
438        // Execute k6
439        TerminalReporter::print_progress("Executing load test...");
440        let executor = K6Executor::new()?;
441
442        std::fs::create_dir_all(&self.output)?;
443
444        let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
445
446        // Print results
447        let duration_secs = Self::parse_duration(&self.duration)?;
448        TerminalReporter::print_summary(&results, duration_secs);
449
450        println!("\nResults saved to: {}", self.output.display());
451
452        Ok(())
453    }
454
455    /// Execute multi-target bench testing
456    async fn execute_multi_target(&self, targets_file: &PathBuf) -> Result<()> {
457        TerminalReporter::print_progress("Parsing targets file...");
458        let targets = parse_targets_file(targets_file)?;
459        let num_targets = targets.len();
460        TerminalReporter::print_success(&format!("Loaded {} targets", num_targets));
461
462        if targets.is_empty() {
463            return Err(BenchError::Other("No targets found in file".to_string()));
464        }
465
466        // Determine max concurrency
467        let max_concurrency = self.max_concurrency.unwrap_or(10) as usize;
468        let max_concurrency = max_concurrency.min(num_targets); // Don't exceed number of targets
469
470        // Print header for multi-target mode
471        TerminalReporter::print_header(
472            &self.get_spec_display_name(),
473            &format!("{} targets", num_targets),
474            0,
475            &self.scenario,
476            Self::parse_duration(&self.duration)?,
477        );
478
479        // Create parallel executor
480        let executor = ParallelExecutor::new(
481            BenchCommand {
482                // Clone all fields except targets_file (we don't need it in the executor)
483                spec: self.spec.clone(),
484                spec_dir: self.spec_dir.clone(),
485                merge_conflicts: self.merge_conflicts.clone(),
486                spec_mode: self.spec_mode.clone(),
487                dependency_config: self.dependency_config.clone(),
488                target: self.target.clone(), // Not used in multi-target mode, but kept for compatibility
489                base_path: self.base_path.clone(),
490                duration: self.duration.clone(),
491                vus: self.vus,
492                scenario: self.scenario.clone(),
493                operations: self.operations.clone(),
494                exclude_operations: self.exclude_operations.clone(),
495                auth: self.auth.clone(),
496                headers: self.headers.clone(),
497                output: self.output.clone(),
498                generate_only: self.generate_only,
499                script_output: self.script_output.clone(),
500                threshold_percentile: self.threshold_percentile.clone(),
501                threshold_ms: self.threshold_ms,
502                max_error_rate: self.max_error_rate,
503                verbose: self.verbose,
504                skip_tls_verify: self.skip_tls_verify,
505                targets_file: None,
506                max_concurrency: None,
507                results_format: self.results_format.clone(),
508                params_file: self.params_file.clone(),
509                crud_flow: self.crud_flow,
510                flow_config: self.flow_config.clone(),
511                extract_fields: self.extract_fields.clone(),
512                parallel_create: self.parallel_create,
513                data_file: self.data_file.clone(),
514                data_distribution: self.data_distribution.clone(),
515                data_mappings: self.data_mappings.clone(),
516                per_uri_control: self.per_uri_control,
517                error_rate: self.error_rate,
518                error_types: self.error_types.clone(),
519                security_test: self.security_test,
520                security_payloads: self.security_payloads.clone(),
521                security_categories: self.security_categories.clone(),
522                security_target_fields: self.security_target_fields.clone(),
523                wafbench_dir: self.wafbench_dir.clone(),
524                wafbench_cycle_all: self.wafbench_cycle_all,
525                owasp_api_top10: self.owasp_api_top10,
526                owasp_categories: self.owasp_categories.clone(),
527                owasp_auth_header: self.owasp_auth_header.clone(),
528                owasp_auth_token: self.owasp_auth_token.clone(),
529                owasp_admin_paths: self.owasp_admin_paths.clone(),
530                owasp_id_fields: self.owasp_id_fields.clone(),
531                owasp_report: self.owasp_report.clone(),
532                owasp_report_format: self.owasp_report_format.clone(),
533                owasp_iterations: self.owasp_iterations,
534                conformance: false,
535                conformance_api_key: None,
536                conformance_basic_auth: None,
537                conformance_report: PathBuf::from("conformance-report.json"),
538                conformance_categories: None,
539                conformance_report_format: "json".to_string(),
540            },
541            targets,
542            max_concurrency,
543        );
544
545        // Execute all targets
546        let aggregated_results = executor.execute_all().await?;
547
548        // Organize and report results
549        self.report_multi_target_results(&aggregated_results)?;
550
551        Ok(())
552    }
553
554    /// Report results for multi-target execution
555    fn report_multi_target_results(&self, results: &AggregatedResults) -> Result<()> {
556        // Print summary
557        TerminalReporter::print_multi_target_summary(results);
558
559        // Save aggregated summary if requested
560        if self.results_format == "aggregated" || self.results_format == "both" {
561            let summary_path = self.output.join("aggregated_summary.json");
562            let summary_json = serde_json::json!({
563                "total_targets": results.total_targets,
564                "successful_targets": results.successful_targets,
565                "failed_targets": results.failed_targets,
566                "aggregated_metrics": {
567                    "total_requests": results.aggregated_metrics.total_requests,
568                    "total_failed_requests": results.aggregated_metrics.total_failed_requests,
569                    "avg_duration_ms": results.aggregated_metrics.avg_duration_ms,
570                    "p95_duration_ms": results.aggregated_metrics.p95_duration_ms,
571                    "p99_duration_ms": results.aggregated_metrics.p99_duration_ms,
572                    "error_rate": results.aggregated_metrics.error_rate,
573                    "total_rps": results.aggregated_metrics.total_rps,
574                    "avg_rps": results.aggregated_metrics.avg_rps,
575                    "total_vus_max": results.aggregated_metrics.total_vus_max,
576                },
577                "target_results": results.target_results.iter().map(|r| {
578                    serde_json::json!({
579                        "target_url": r.target_url,
580                        "target_index": r.target_index,
581                        "success": r.success,
582                        "error": r.error,
583                        "total_requests": r.results.total_requests,
584                        "failed_requests": r.results.failed_requests,
585                        "avg_duration_ms": r.results.avg_duration_ms,
586                        "min_duration_ms": r.results.min_duration_ms,
587                        "med_duration_ms": r.results.med_duration_ms,
588                        "p90_duration_ms": r.results.p90_duration_ms,
589                        "p95_duration_ms": r.results.p95_duration_ms,
590                        "p99_duration_ms": r.results.p99_duration_ms,
591                        "max_duration_ms": r.results.max_duration_ms,
592                        "rps": r.results.rps,
593                        "vus_max": r.results.vus_max,
594                        "output_dir": r.output_dir.to_string_lossy(),
595                    })
596                }).collect::<Vec<_>>(),
597            });
598
599            std::fs::write(&summary_path, serde_json::to_string_pretty(&summary_json)?)?;
600            TerminalReporter::print_success(&format!(
601                "Aggregated summary saved to: {}",
602                summary_path.display()
603            ));
604        }
605
606        println!("\nResults saved to: {}", self.output.display());
607        println!("  - Per-target results: {}", self.output.join("target_*").display());
608        if self.results_format == "aggregated" || self.results_format == "both" {
609            println!(
610                "  - Aggregated summary: {}",
611                self.output.join("aggregated_summary.json").display()
612            );
613        }
614
615        Ok(())
616    }
617
618    /// Parse duration string (e.g., "30s", "5m", "1h") to seconds
619    pub fn parse_duration(duration: &str) -> Result<u64> {
620        let duration = duration.trim();
621
622        if let Some(secs) = duration.strip_suffix('s') {
623            secs.parse::<u64>()
624                .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
625        } else if let Some(mins) = duration.strip_suffix('m') {
626            mins.parse::<u64>()
627                .map(|m| m * 60)
628                .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
629        } else if let Some(hours) = duration.strip_suffix('h') {
630            hours
631                .parse::<u64>()
632                .map(|h| h * 3600)
633                .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
634        } else {
635            // Try parsing as seconds without suffix
636            duration
637                .parse::<u64>()
638                .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
639        }
640    }
641
642    /// Parse headers from command line format (Key:Value,Key2:Value2)
643    pub fn parse_headers(&self) -> Result<HashMap<String, String>> {
644        let mut headers = HashMap::new();
645
646        if let Some(header_str) = &self.headers {
647            for pair in header_str.split(',') {
648                let parts: Vec<&str> = pair.splitn(2, ':').collect();
649                if parts.len() != 2 {
650                    return Err(BenchError::Other(format!(
651                        "Invalid header format: '{}'. Expected 'Key:Value'",
652                        pair
653                    )));
654                }
655                headers.insert(parts[0].trim().to_string(), parts[1].trim().to_string());
656            }
657        }
658
659        Ok(headers)
660    }
661
662    /// Resolve the effective base path for API endpoints
663    ///
664    /// Priority:
665    /// 1. CLI --base-path option (if provided, even if empty string)
666    /// 2. Base path extracted from OpenAPI spec's servers URL
667    /// 3. None (no base path)
668    ///
669    /// An empty string from CLI explicitly disables base path.
670    fn resolve_base_path(&self, parser: &SpecParser) -> Option<String> {
671        // CLI option takes priority (including empty string to disable)
672        if let Some(cli_base_path) = &self.base_path {
673            if cli_base_path.is_empty() {
674                // Empty string explicitly means "no base path"
675                return None;
676            }
677            return Some(cli_base_path.clone());
678        }
679
680        // Fall back to spec's base path
681        parser.get_base_path()
682    }
683
684    /// Build mock server integration configuration
685    async fn build_mock_config(&self) -> MockIntegrationConfig {
686        // Check if target looks like a mock server
687        if MockServerDetector::looks_like_mock_server(&self.target) {
688            // Try to detect if it's actually a MockForge server
689            if let Ok(info) = MockServerDetector::detect(&self.target).await {
690                if info.is_mockforge {
691                    TerminalReporter::print_success(&format!(
692                        "Detected MockForge server (version: {})",
693                        info.version.as_deref().unwrap_or("unknown")
694                    ));
695                    return MockIntegrationConfig::mock_server();
696                }
697            }
698        }
699        MockIntegrationConfig::real_api()
700    }
701
702    /// Build CRUD flow configuration
703    fn build_crud_flow_config(&self) -> Option<CrudFlowConfig> {
704        if !self.crud_flow {
705            return None;
706        }
707
708        // If flow_config file is provided, load it
709        if let Some(config_path) = &self.flow_config {
710            match CrudFlowConfig::from_file(config_path) {
711                Ok(config) => return Some(config),
712                Err(e) => {
713                    TerminalReporter::print_warning(&format!(
714                        "Failed to load flow config: {}. Using auto-detection.",
715                        e
716                    ));
717                }
718            }
719        }
720
721        // Parse extract fields
722        let extract_fields = self
723            .extract_fields
724            .as_ref()
725            .map(|f| f.split(',').map(|s| s.trim().to_string()).collect())
726            .unwrap_or_else(|| vec!["id".to_string(), "uuid".to_string()]);
727
728        Some(CrudFlowConfig {
729            flows: Vec::new(), // Will be auto-detected
730            default_extract_fields: extract_fields,
731        })
732    }
733
734    /// Build data-driven testing configuration
735    fn build_data_driven_config(&self) -> Option<DataDrivenConfig> {
736        let data_file = self.data_file.as_ref()?;
737
738        let distribution = DataDistribution::from_str(&self.data_distribution)
739            .unwrap_or(DataDistribution::UniquePerVu);
740
741        let mappings = self
742            .data_mappings
743            .as_ref()
744            .map(|m| DataMapping::parse_mappings(m).unwrap_or_default())
745            .unwrap_or_default();
746
747        Some(DataDrivenConfig {
748            file_path: data_file.to_string_lossy().to_string(),
749            distribution,
750            mappings,
751            csv_has_header: true,
752            per_uri_control: self.per_uri_control,
753            per_uri_columns: crate::data_driven::PerUriColumns::default(),
754        })
755    }
756
757    /// Build invalid data testing configuration
758    fn build_invalid_data_config(&self) -> Option<InvalidDataConfig> {
759        let error_rate = self.error_rate?;
760
761        let error_types = self
762            .error_types
763            .as_ref()
764            .map(|types| InvalidDataConfig::parse_error_types(types).unwrap_or_default())
765            .unwrap_or_default();
766
767        Some(InvalidDataConfig {
768            error_rate,
769            error_types,
770            target_fields: Vec::new(),
771        })
772    }
773
774    /// Build security testing configuration
775    fn build_security_config(&self) -> Option<SecurityTestConfig> {
776        if !self.security_test {
777            return None;
778        }
779
780        let categories = self
781            .security_categories
782            .as_ref()
783            .map(|cats| SecurityTestConfig::parse_categories(cats).unwrap_or_default())
784            .unwrap_or_else(|| {
785                let mut default = std::collections::HashSet::new();
786                default.insert(SecurityCategory::SqlInjection);
787                default.insert(SecurityCategory::Xss);
788                default
789            });
790
791        let target_fields = self
792            .security_target_fields
793            .as_ref()
794            .map(|fields| fields.split(',').map(|f| f.trim().to_string()).collect())
795            .unwrap_or_default();
796
797        let custom_payloads_file =
798            self.security_payloads.as_ref().map(|p| p.to_string_lossy().to_string());
799
800        Some(SecurityTestConfig {
801            enabled: true,
802            categories,
803            target_fields,
804            custom_payloads_file,
805            include_high_risk: false,
806        })
807    }
808
809    /// Build parallel execution configuration
810    fn build_parallel_config(&self) -> Option<ParallelConfig> {
811        let count = self.parallel_create?;
812
813        Some(ParallelConfig::new(count))
814    }
815
816    /// Load WAFBench payloads from the specified directory or pattern
817    fn load_wafbench_payloads(&self) -> Vec<SecurityPayload> {
818        let Some(ref wafbench_dir) = self.wafbench_dir else {
819            return Vec::new();
820        };
821
822        let mut loader = WafBenchLoader::new();
823
824        if let Err(e) = loader.load_from_pattern(wafbench_dir) {
825            TerminalReporter::print_warning(&format!("Failed to load WAFBench tests: {}", e));
826            return Vec::new();
827        }
828
829        let stats = loader.stats();
830
831        if stats.files_processed == 0 {
832            TerminalReporter::print_warning(&format!(
833                "No WAFBench YAML files found matching '{}'",
834                wafbench_dir
835            ));
836            // Also report any parse errors that may explain why no files were processed
837            if !stats.parse_errors.is_empty() {
838                TerminalReporter::print_warning("Some files were found but failed to parse:");
839                for error in &stats.parse_errors {
840                    TerminalReporter::print_warning(&format!("  - {}", error));
841                }
842            }
843            return Vec::new();
844        }
845
846        TerminalReporter::print_progress(&format!(
847            "Loaded {} WAFBench files, {} test cases, {} payloads",
848            stats.files_processed, stats.test_cases_loaded, stats.payloads_extracted
849        ));
850
851        // Print category breakdown
852        for (category, count) in &stats.by_category {
853            TerminalReporter::print_progress(&format!("  - {}: {} tests", category, count));
854        }
855
856        // Report any parse errors
857        for error in &stats.parse_errors {
858            TerminalReporter::print_warning(&format!("  Parse error: {}", error));
859        }
860
861        loader.to_security_payloads()
862    }
863
864    /// Generate enhanced k6 script with advanced features
865    pub(crate) fn generate_enhanced_script(&self, base_script: &str) -> Result<String> {
866        let mut enhanced_script = base_script.to_string();
867        let mut additional_code = String::new();
868
869        // Add data-driven testing code
870        if let Some(config) = self.build_data_driven_config() {
871            TerminalReporter::print_progress("Adding data-driven testing support...");
872            additional_code.push_str(&DataDrivenGenerator::generate_setup(&config));
873            additional_code.push('\n');
874            TerminalReporter::print_success("Data-driven testing enabled");
875        }
876
877        // Add invalid data generation code
878        if let Some(config) = self.build_invalid_data_config() {
879            TerminalReporter::print_progress("Adding invalid data testing support...");
880            additional_code.push_str(&InvalidDataGenerator::generate_invalidation_logic());
881            additional_code.push('\n');
882            additional_code
883                .push_str(&InvalidDataGenerator::generate_should_invalidate(config.error_rate));
884            additional_code.push('\n');
885            additional_code
886                .push_str(&InvalidDataGenerator::generate_type_selection(&config.error_types));
887            additional_code.push('\n');
888            TerminalReporter::print_success(&format!(
889                "Invalid data testing enabled ({}% error rate)",
890                (self.error_rate.unwrap_or(0.0) * 100.0) as u32
891            ));
892        }
893
894        // Add security testing code
895        let security_config = self.build_security_config();
896        let wafbench_payloads = self.load_wafbench_payloads();
897        let security_requested = security_config.is_some() || self.wafbench_dir.is_some();
898
899        if security_config.is_some() || !wafbench_payloads.is_empty() {
900            TerminalReporter::print_progress("Adding security testing support...");
901
902            // Combine built-in payloads with WAFBench payloads
903            let mut payload_list: Vec<SecurityPayload> = Vec::new();
904
905            if let Some(ref config) = security_config {
906                payload_list.extend(SecurityPayloads::get_payloads(config));
907            }
908
909            // Add WAFBench payloads
910            if !wafbench_payloads.is_empty() {
911                TerminalReporter::print_progress(&format!(
912                    "Loading {} WAFBench attack patterns...",
913                    wafbench_payloads.len()
914                ));
915                payload_list.extend(wafbench_payloads);
916            }
917
918            let target_fields =
919                security_config.as_ref().map(|c| c.target_fields.clone()).unwrap_or_default();
920
921            additional_code.push_str(&SecurityTestGenerator::generate_payload_selection(
922                &payload_list,
923                self.wafbench_cycle_all,
924            ));
925            additional_code.push('\n');
926            additional_code
927                .push_str(&SecurityTestGenerator::generate_apply_payload(&target_fields));
928            additional_code.push('\n');
929            additional_code.push_str(&SecurityTestGenerator::generate_security_checks());
930            additional_code.push('\n');
931
932            let mode = if self.wafbench_cycle_all {
933                "cycle-all"
934            } else {
935                "random"
936            };
937            TerminalReporter::print_success(&format!(
938                "Security testing enabled ({} payloads, {} mode)",
939                payload_list.len(),
940                mode
941            ));
942        } else if security_requested {
943            // User requested security testing (e.g., --wafbench-dir) but no payloads were loaded.
944            // The template has security_testing_enabled=true so it renders calling code.
945            // We must inject stub definitions to avoid undefined function references.
946            TerminalReporter::print_warning(
947                "Security testing was requested but no payloads were loaded. \
948                 Ensure --wafbench-dir points to valid CRS YAML files or add --security-test.",
949            );
950            additional_code
951                .push_str(&SecurityTestGenerator::generate_payload_selection(&[], false));
952            additional_code.push('\n');
953            additional_code.push_str(&SecurityTestGenerator::generate_apply_payload(&[]));
954            additional_code.push('\n');
955        }
956
957        // Add parallel execution code
958        if let Some(config) = self.build_parallel_config() {
959            TerminalReporter::print_progress("Adding parallel execution support...");
960            additional_code.push_str(&ParallelRequestGenerator::generate_batch_helper(&config));
961            additional_code.push('\n');
962            TerminalReporter::print_success(&format!(
963                "Parallel execution enabled (count: {})",
964                config.count
965            ));
966        }
967
968        // Insert additional code after the imports section
969        if !additional_code.is_empty() {
970            // Find the end of the import section
971            if let Some(import_end) = enhanced_script.find("export const options") {
972                enhanced_script.insert_str(
973                    import_end,
974                    &format!("\n// === Advanced Testing Features ===\n{}\n", additional_code),
975                );
976            }
977        }
978
979        Ok(enhanced_script)
980    }
981
982    /// Execute specs sequentially with dependency ordering and value passing
983    async fn execute_sequential_specs(&self) -> Result<()> {
984        TerminalReporter::print_progress("Sequential spec mode: Loading specs individually...");
985
986        // Load all specs (without merging)
987        let mut all_specs: Vec<(PathBuf, OpenApiSpec)> = Vec::new();
988
989        if !self.spec.is_empty() {
990            let specs = load_specs_from_files(self.spec.clone())
991                .await
992                .map_err(|e| BenchError::Other(format!("Failed to load spec files: {}", e)))?;
993            all_specs.extend(specs);
994        }
995
996        if let Some(spec_dir) = &self.spec_dir {
997            let dir_specs = load_specs_from_directory(spec_dir).await.map_err(|e| {
998                BenchError::Other(format!("Failed to load specs from directory: {}", e))
999            })?;
1000            all_specs.extend(dir_specs);
1001        }
1002
1003        if all_specs.is_empty() {
1004            return Err(BenchError::Other(
1005                "No spec files found for sequential execution".to_string(),
1006            ));
1007        }
1008
1009        TerminalReporter::print_success(&format!("Loaded {} spec(s)", all_specs.len()));
1010
1011        // Load dependency config or auto-detect
1012        let execution_order = if let Some(config_path) = &self.dependency_config {
1013            TerminalReporter::print_progress("Loading dependency configuration...");
1014            let config = SpecDependencyConfig::from_file(config_path)?;
1015
1016            if !config.disable_auto_detect && config.execution_order.is_empty() {
1017                // Auto-detect if config doesn't specify order
1018                self.detect_and_sort_specs(&all_specs)?
1019            } else {
1020                // Use configured order
1021                config.execution_order.iter().flat_map(|g| g.specs.clone()).collect()
1022            }
1023        } else {
1024            // Auto-detect dependencies
1025            self.detect_and_sort_specs(&all_specs)?
1026        };
1027
1028        TerminalReporter::print_success(&format!(
1029            "Execution order: {}",
1030            execution_order
1031                .iter()
1032                .map(|p| p.file_name().unwrap_or_default().to_string_lossy().to_string())
1033                .collect::<Vec<_>>()
1034                .join(" → ")
1035        ));
1036
1037        // Execute each spec in order
1038        let mut extracted_values = ExtractedValues::new();
1039        let total_specs = execution_order.len();
1040
1041        for (index, spec_path) in execution_order.iter().enumerate() {
1042            let spec_name = spec_path.file_name().unwrap_or_default().to_string_lossy().to_string();
1043
1044            TerminalReporter::print_progress(&format!(
1045                "[{}/{}] Executing spec: {}",
1046                index + 1,
1047                total_specs,
1048                spec_name
1049            ));
1050
1051            // Find the spec in our loaded specs (match by full path or filename)
1052            let spec = all_specs
1053                .iter()
1054                .find(|(p, _)| {
1055                    p == spec_path
1056                        || p.file_name() == spec_path.file_name()
1057                        || p.file_name() == Some(spec_path.as_os_str())
1058                })
1059                .map(|(_, s)| s.clone())
1060                .ok_or_else(|| {
1061                    BenchError::Other(format!("Spec not found: {}", spec_path.display()))
1062                })?;
1063
1064            // Execute this spec with any extracted values from previous specs
1065            let new_values = self.execute_single_spec(&spec, &spec_name, &extracted_values).await?;
1066
1067            // Merge extracted values for the next spec
1068            extracted_values.merge(&new_values);
1069
1070            TerminalReporter::print_success(&format!(
1071                "[{}/{}] Completed: {} (extracted {} values)",
1072                index + 1,
1073                total_specs,
1074                spec_name,
1075                new_values.values.len()
1076            ));
1077        }
1078
1079        TerminalReporter::print_success(&format!(
1080            "Sequential execution complete: {} specs executed",
1081            total_specs
1082        ));
1083
1084        Ok(())
1085    }
1086
1087    /// Detect dependencies and return topologically sorted spec paths
1088    fn detect_and_sort_specs(&self, specs: &[(PathBuf, OpenApiSpec)]) -> Result<Vec<PathBuf>> {
1089        TerminalReporter::print_progress("Auto-detecting spec dependencies...");
1090
1091        let mut detector = DependencyDetector::new();
1092        let dependencies = detector.detect_dependencies(specs);
1093
1094        if dependencies.is_empty() {
1095            TerminalReporter::print_progress("No dependencies detected, using file order");
1096            return Ok(specs.iter().map(|(p, _)| p.clone()).collect());
1097        }
1098
1099        TerminalReporter::print_progress(&format!(
1100            "Detected {} cross-spec dependencies",
1101            dependencies.len()
1102        ));
1103
1104        for dep in &dependencies {
1105            TerminalReporter::print_progress(&format!(
1106                "  {} → {} (via field '{}')",
1107                dep.dependency_spec.file_name().unwrap_or_default().to_string_lossy(),
1108                dep.dependent_spec.file_name().unwrap_or_default().to_string_lossy(),
1109                dep.field_name
1110            ));
1111        }
1112
1113        topological_sort(specs, &dependencies)
1114    }
1115
1116    /// Execute a single spec and extract values for dependent specs
1117    async fn execute_single_spec(
1118        &self,
1119        spec: &OpenApiSpec,
1120        spec_name: &str,
1121        _external_values: &ExtractedValues,
1122    ) -> Result<ExtractedValues> {
1123        let parser = SpecParser::from_spec(spec.clone());
1124
1125        // For now, we execute in CRUD flow mode if enabled, otherwise standard mode
1126        if self.crud_flow {
1127            // Execute CRUD flow and extract values
1128            self.execute_crud_flow_with_extraction(&parser, spec_name).await
1129        } else {
1130            // Execute standard benchmark (no value extraction in non-CRUD mode)
1131            self.execute_standard_spec(&parser, spec_name).await?;
1132            Ok(ExtractedValues::new())
1133        }
1134    }
1135
1136    /// Execute CRUD flow with value extraction for sequential mode
1137    async fn execute_crud_flow_with_extraction(
1138        &self,
1139        parser: &SpecParser,
1140        spec_name: &str,
1141    ) -> Result<ExtractedValues> {
1142        let operations = parser.get_operations();
1143        let flows = CrudFlowDetector::detect_flows(&operations);
1144
1145        if flows.is_empty() {
1146            TerminalReporter::print_warning(&format!("No CRUD flows detected in {}", spec_name));
1147            return Ok(ExtractedValues::new());
1148        }
1149
1150        TerminalReporter::print_progress(&format!(
1151            "  {} CRUD flow(s) in {}",
1152            flows.len(),
1153            spec_name
1154        ));
1155
1156        // Generate and execute the CRUD flow script
1157        let mut handlebars = handlebars::Handlebars::new();
1158        // Register json helper for serializing arrays/objects in templates
1159        handlebars.register_helper(
1160            "json",
1161            Box::new(
1162                |h: &handlebars::Helper,
1163                 _: &handlebars::Handlebars,
1164                 _: &handlebars::Context,
1165                 _: &mut handlebars::RenderContext,
1166                 out: &mut dyn handlebars::Output|
1167                 -> handlebars::HelperResult {
1168                    let param = h.param(0).map(|v| v.value()).unwrap_or(&serde_json::Value::Null);
1169                    out.write(&serde_json::to_string(param).unwrap_or_else(|_| "[]".to_string()))?;
1170                    Ok(())
1171                },
1172            ),
1173        );
1174        let template = include_str!("templates/k6_crud_flow.hbs");
1175
1176        let custom_headers = self.parse_headers()?;
1177        let config = self.build_crud_flow_config().unwrap_or_default();
1178
1179        // Load parameter overrides if provided (for body configurations)
1180        let param_overrides = if let Some(params_file) = &self.params_file {
1181            let overrides = ParameterOverrides::from_file(params_file)?;
1182            Some(overrides)
1183        } else {
1184            None
1185        };
1186
1187        // Generate stages from scenario
1188        let duration_secs = Self::parse_duration(&self.duration)?;
1189        let scenario =
1190            LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1191        let stages = scenario.generate_stages(duration_secs, self.vus);
1192
1193        // Resolve base path (CLI option takes priority over spec's servers URL)
1194        let api_base_path = self.resolve_base_path(parser);
1195
1196        // Build headers JSON string for the template
1197        let mut all_headers = custom_headers.clone();
1198        if let Some(auth) = &self.auth {
1199            all_headers.insert("Authorization".to_string(), auth.clone());
1200        }
1201        let headers_json = serde_json::to_string(&all_headers).unwrap_or_else(|_| "{}".to_string());
1202
1203        // Track all dynamic placeholders across all operations
1204        let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
1205
1206        let flows_data: Vec<serde_json::Value> = flows.iter().map(|f| {
1207            let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
1208            serde_json::json!({
1209                "name": sanitized_name.clone(),
1210                "display_name": f.name,
1211                "base_path": f.base_path,
1212                "steps": f.steps.iter().enumerate().map(|(idx, s)| {
1213                    // Parse operation to get method and path
1214                    let parts: Vec<&str> = s.operation.splitn(2, ' ').collect();
1215                    let method_raw = if !parts.is_empty() {
1216                        parts[0].to_uppercase()
1217                    } else {
1218                        "GET".to_string()
1219                    };
1220                    let method = if !parts.is_empty() {
1221                        let m = parts[0].to_lowercase();
1222                        // k6 uses 'del' for DELETE
1223                        if m == "delete" { "del".to_string() } else { m }
1224                    } else {
1225                        "get".to_string()
1226                    };
1227                    let raw_path = if parts.len() >= 2 { parts[1] } else { "/" };
1228                    // Prepend API base path if configured
1229                    let path = if let Some(ref bp) = api_base_path {
1230                        format!("{}{}", bp, raw_path)
1231                    } else {
1232                        raw_path.to_string()
1233                    };
1234                    let is_get_or_head = method == "get" || method == "head";
1235                    // POST, PUT, PATCH typically have bodies
1236                    let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1237
1238                    // Look up body from params file if available
1239                    let body_value = if has_body {
1240                        param_overrides.as_ref()
1241                            .map(|po| po.get_for_operation(None, &method_raw, raw_path))
1242                            .and_then(|oo| oo.body)
1243                            .unwrap_or_else(|| serde_json::json!({}))
1244                    } else {
1245                        serde_json::json!({})
1246                    };
1247
1248                    // Process body for dynamic placeholders like ${__VU}, ${__ITER}, etc.
1249                    let processed_body = DynamicParamProcessor::process_json_body(&body_value);
1250
1251                    // Also check for ${extracted.xxx} placeholders which need runtime substitution
1252                    let body_has_extracted_placeholders = processed_body.value.contains("${extracted.");
1253                    let body_is_dynamic = processed_body.is_dynamic || body_has_extracted_placeholders;
1254
1255                    serde_json::json!({
1256                        "operation": s.operation,
1257                        "method": method,
1258                        "path": path,
1259                        "extract": s.extract,
1260                        "use_values": s.use_values,
1261                        "use_body": s.use_body,
1262                        "merge_body": if s.merge_body.is_empty() { None } else { Some(&s.merge_body) },
1263                        "inject_attacks": s.inject_attacks,
1264                        "attack_types": s.attack_types,
1265                        "description": s.description,
1266                        "display_name": s.description.clone().unwrap_or_else(|| format!("Step {}", idx)),
1267                        "is_get_or_head": is_get_or_head,
1268                        "has_body": has_body,
1269                        "body": processed_body.value,
1270                        "body_is_dynamic": body_is_dynamic,
1271                        "_placeholders": processed_body.placeholders.iter().map(|p| format!("{:?}", p)).collect::<Vec<_>>(),
1272                    })
1273                }).collect::<Vec<_>>(),
1274            })
1275        }).collect();
1276
1277        // Collect all placeholders from all steps
1278        for flow_data in &flows_data {
1279            if let Some(steps) = flow_data.get("steps").and_then(|s| s.as_array()) {
1280                for step in steps {
1281                    if let Some(placeholders_arr) =
1282                        step.get("_placeholders").and_then(|p| p.as_array())
1283                    {
1284                        for p_str in placeholders_arr {
1285                            if let Some(p_name) = p_str.as_str() {
1286                                match p_name {
1287                                    "VU" => {
1288                                        all_placeholders.insert(DynamicPlaceholder::VU);
1289                                    }
1290                                    "Iteration" => {
1291                                        all_placeholders.insert(DynamicPlaceholder::Iteration);
1292                                    }
1293                                    "Timestamp" => {
1294                                        all_placeholders.insert(DynamicPlaceholder::Timestamp);
1295                                    }
1296                                    "UUID" => {
1297                                        all_placeholders.insert(DynamicPlaceholder::UUID);
1298                                    }
1299                                    "Random" => {
1300                                        all_placeholders.insert(DynamicPlaceholder::Random);
1301                                    }
1302                                    "Counter" => {
1303                                        all_placeholders.insert(DynamicPlaceholder::Counter);
1304                                    }
1305                                    "Date" => {
1306                                        all_placeholders.insert(DynamicPlaceholder::Date);
1307                                    }
1308                                    "VuIter" => {
1309                                        all_placeholders.insert(DynamicPlaceholder::VuIter);
1310                                    }
1311                                    _ => {}
1312                                }
1313                            }
1314                        }
1315                    }
1316                }
1317            }
1318        }
1319
1320        // Get required imports and globals based on placeholders used
1321        let required_imports = DynamicParamProcessor::get_required_imports(&all_placeholders);
1322        let required_globals = DynamicParamProcessor::get_required_globals(&all_placeholders);
1323
1324        // Check if security testing is enabled
1325        let security_testing_enabled = self.wafbench_dir.is_some() || self.security_test;
1326
1327        let data = serde_json::json!({
1328            "base_url": self.target,
1329            "flows": flows_data,
1330            "extract_fields": config.default_extract_fields,
1331            "duration_secs": duration_secs,
1332            "max_vus": self.vus,
1333            "auth_header": self.auth,
1334            "custom_headers": custom_headers,
1335            "skip_tls_verify": self.skip_tls_verify,
1336            // Add missing template fields
1337            "stages": stages.iter().map(|s| serde_json::json!({
1338                "duration": s.duration,
1339                "target": s.target,
1340            })).collect::<Vec<_>>(),
1341            "threshold_percentile": self.threshold_percentile,
1342            "threshold_ms": self.threshold_ms,
1343            "max_error_rate": self.max_error_rate,
1344            "headers": headers_json,
1345            "dynamic_imports": required_imports,
1346            "dynamic_globals": required_globals,
1347            // Security testing settings
1348            "security_testing_enabled": security_testing_enabled,
1349            "has_custom_headers": !custom_headers.is_empty(),
1350        });
1351
1352        let mut script = handlebars
1353            .render_template(template, &data)
1354            .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
1355
1356        // Enhance script with security testing support if enabled
1357        if security_testing_enabled {
1358            script = self.generate_enhanced_script(&script)?;
1359        }
1360
1361        // Write and execute script
1362        let script_path =
1363            self.output.join(format!("k6-{}-crud-flow.js", spec_name.replace('.', "_")));
1364
1365        std::fs::create_dir_all(self.output.clone())?;
1366        std::fs::write(&script_path, &script)?;
1367
1368        if !self.generate_only {
1369            let executor = K6Executor::new()?;
1370            let output_dir = self.output.join(format!("{}_results", spec_name.replace('.', "_")));
1371            std::fs::create_dir_all(&output_dir)?;
1372
1373            executor.execute(&script_path, Some(&output_dir), self.verbose).await?;
1374        }
1375
1376        // For now, return empty extracted values
1377        // TODO: Parse k6 output to extract actual values
1378        Ok(ExtractedValues::new())
1379    }
1380
1381    /// Execute standard (non-CRUD) spec benchmark
1382    async fn execute_standard_spec(&self, parser: &SpecParser, spec_name: &str) -> Result<()> {
1383        let mut operations = if let Some(filter) = &self.operations {
1384            parser.filter_operations(filter)?
1385        } else {
1386            parser.get_operations()
1387        };
1388
1389        if let Some(exclude) = &self.exclude_operations {
1390            operations = parser.exclude_operations(operations, exclude)?;
1391        }
1392
1393        if operations.is_empty() {
1394            TerminalReporter::print_warning(&format!("No operations found in {}", spec_name));
1395            return Ok(());
1396        }
1397
1398        TerminalReporter::print_progress(&format!(
1399            "  {} operations in {}",
1400            operations.len(),
1401            spec_name
1402        ));
1403
1404        // Generate request templates
1405        let templates: Vec<_> = operations
1406            .iter()
1407            .map(RequestGenerator::generate_template)
1408            .collect::<Result<Vec<_>>>()?;
1409
1410        // Parse headers
1411        let custom_headers = self.parse_headers()?;
1412
1413        // Resolve base path
1414        let base_path = self.resolve_base_path(parser);
1415
1416        // Generate k6 script
1417        let scenario =
1418            LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1419
1420        let security_testing_enabled = self.security_test || self.wafbench_dir.is_some();
1421
1422        let k6_config = K6Config {
1423            target_url: self.target.clone(),
1424            base_path,
1425            scenario,
1426            duration_secs: Self::parse_duration(&self.duration)?,
1427            max_vus: self.vus,
1428            threshold_percentile: self.threshold_percentile.clone(),
1429            threshold_ms: self.threshold_ms,
1430            max_error_rate: self.max_error_rate,
1431            auth_header: self.auth.clone(),
1432            custom_headers,
1433            skip_tls_verify: self.skip_tls_verify,
1434            security_testing_enabled,
1435        };
1436
1437        let generator = K6ScriptGenerator::new(k6_config, templates);
1438        let mut script = generator.generate()?;
1439
1440        // Enhance script with advanced features (security testing, etc.)
1441        let has_advanced_features = self.data_file.is_some()
1442            || self.error_rate.is_some()
1443            || self.security_test
1444            || self.parallel_create.is_some()
1445            || self.wafbench_dir.is_some();
1446
1447        if has_advanced_features {
1448            script = self.generate_enhanced_script(&script)?;
1449        }
1450
1451        // Write and execute script
1452        let script_path = self.output.join(format!("k6-{}.js", spec_name.replace('.', "_")));
1453
1454        std::fs::create_dir_all(self.output.clone())?;
1455        std::fs::write(&script_path, &script)?;
1456
1457        if !self.generate_only {
1458            let executor = K6Executor::new()?;
1459            let output_dir = self.output.join(format!("{}_results", spec_name.replace('.', "_")));
1460            std::fs::create_dir_all(&output_dir)?;
1461
1462            executor.execute(&script_path, Some(&output_dir), self.verbose).await?;
1463        }
1464
1465        Ok(())
1466    }
1467
1468    /// Execute CRUD flow testing mode
1469    async fn execute_crud_flow(&self, parser: &SpecParser) -> Result<()> {
1470        // Check if a custom flow config is provided
1471        let config = self.build_crud_flow_config().unwrap_or_default();
1472
1473        // Use flows from config if provided, otherwise auto-detect
1474        let flows = if !config.flows.is_empty() {
1475            TerminalReporter::print_progress("Using custom flow configuration...");
1476            config.flows.clone()
1477        } else {
1478            TerminalReporter::print_progress("Detecting CRUD operations...");
1479            let operations = parser.get_operations();
1480            CrudFlowDetector::detect_flows(&operations)
1481        };
1482
1483        if flows.is_empty() {
1484            return Err(BenchError::Other(
1485                "No CRUD flows detected in spec. Ensure spec has POST/GET/PUT/DELETE operations on related paths.".to_string(),
1486            ));
1487        }
1488
1489        if config.flows.is_empty() {
1490            TerminalReporter::print_success(&format!("Detected {} CRUD flow(s)", flows.len()));
1491        } else {
1492            TerminalReporter::print_success(&format!("Loaded {} custom flow(s)", flows.len()));
1493        }
1494
1495        for flow in &flows {
1496            TerminalReporter::print_progress(&format!(
1497                "  - {}: {} steps",
1498                flow.name,
1499                flow.steps.len()
1500            ));
1501        }
1502
1503        // Generate CRUD flow script
1504        let mut handlebars = handlebars::Handlebars::new();
1505        // Register json helper for serializing arrays/objects in templates
1506        handlebars.register_helper(
1507            "json",
1508            Box::new(
1509                |h: &handlebars::Helper,
1510                 _: &handlebars::Handlebars,
1511                 _: &handlebars::Context,
1512                 _: &mut handlebars::RenderContext,
1513                 out: &mut dyn handlebars::Output|
1514                 -> handlebars::HelperResult {
1515                    let param = h.param(0).map(|v| v.value()).unwrap_or(&serde_json::Value::Null);
1516                    out.write(&serde_json::to_string(param).unwrap_or_else(|_| "[]".to_string()))?;
1517                    Ok(())
1518                },
1519            ),
1520        );
1521        let template = include_str!("templates/k6_crud_flow.hbs");
1522
1523        let custom_headers = self.parse_headers()?;
1524
1525        // Load parameter overrides if provided (for body configurations)
1526        let param_overrides = if let Some(params_file) = &self.params_file {
1527            TerminalReporter::print_progress("Loading parameter overrides...");
1528            let overrides = ParameterOverrides::from_file(params_file)?;
1529            TerminalReporter::print_success(&format!(
1530                "Loaded parameter overrides ({} operation-specific, {} defaults)",
1531                overrides.operations.len(),
1532                if overrides.defaults.is_empty() { 0 } else { 1 }
1533            ));
1534            Some(overrides)
1535        } else {
1536            None
1537        };
1538
1539        // Generate stages from scenario
1540        let duration_secs = Self::parse_duration(&self.duration)?;
1541        let scenario =
1542            LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1543        let stages = scenario.generate_stages(duration_secs, self.vus);
1544
1545        // Resolve base path (CLI option takes priority over spec's servers URL)
1546        let api_base_path = self.resolve_base_path(parser);
1547        if let Some(ref bp) = api_base_path {
1548            TerminalReporter::print_progress(&format!("Using base path: {}", bp));
1549        }
1550
1551        // Build headers JSON string for the template
1552        let mut all_headers = custom_headers.clone();
1553        if let Some(auth) = &self.auth {
1554            all_headers.insert("Authorization".to_string(), auth.clone());
1555        }
1556        let headers_json = serde_json::to_string(&all_headers).unwrap_or_else(|_| "{}".to_string());
1557
1558        // Track all dynamic placeholders across all operations
1559        let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
1560
1561        let flows_data: Vec<serde_json::Value> = flows.iter().map(|f| {
1562            // Sanitize flow name for use as JavaScript variable and k6 metric names
1563            let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
1564            serde_json::json!({
1565                "name": sanitized_name.clone(),  // Use sanitized name for variable names
1566                "display_name": f.name,          // Keep original for comments/display
1567                "base_path": f.base_path,
1568                "steps": f.steps.iter().enumerate().map(|(idx, s)| {
1569                    // Parse operation to get method and path
1570                    let parts: Vec<&str> = s.operation.splitn(2, ' ').collect();
1571                    let method_raw = if !parts.is_empty() {
1572                        parts[0].to_uppercase()
1573                    } else {
1574                        "GET".to_string()
1575                    };
1576                    let method = if !parts.is_empty() {
1577                        let m = parts[0].to_lowercase();
1578                        // k6 uses 'del' for DELETE
1579                        if m == "delete" { "del".to_string() } else { m }
1580                    } else {
1581                        "get".to_string()
1582                    };
1583                    let raw_path = if parts.len() >= 2 { parts[1] } else { "/" };
1584                    // Prepend API base path if configured
1585                    let path = if let Some(ref bp) = api_base_path {
1586                        format!("{}{}", bp, raw_path)
1587                    } else {
1588                        raw_path.to_string()
1589                    };
1590                    let is_get_or_head = method == "get" || method == "head";
1591                    // POST, PUT, PATCH typically have bodies
1592                    let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1593
1594                    // Look up body from params file if available (use raw_path for matching)
1595                    let body_value = if has_body {
1596                        param_overrides.as_ref()
1597                            .map(|po| po.get_for_operation(None, &method_raw, raw_path))
1598                            .and_then(|oo| oo.body)
1599                            .unwrap_or_else(|| serde_json::json!({}))
1600                    } else {
1601                        serde_json::json!({})
1602                    };
1603
1604                    // Process body for dynamic placeholders like ${__VU}, ${__ITER}, etc.
1605                    let processed_body = DynamicParamProcessor::process_json_body(&body_value);
1606                    // Note: all_placeholders is captured by the closure but we can't mutate it directly
1607                    // We'll collect placeholders separately below
1608
1609                    // Also check for ${extracted.xxx} placeholders which need runtime substitution
1610                    let body_has_extracted_placeholders = processed_body.value.contains("${extracted.");
1611                    let body_is_dynamic = processed_body.is_dynamic || body_has_extracted_placeholders;
1612
1613                    serde_json::json!({
1614                        "operation": s.operation,
1615                        "method": method,
1616                        "path": path,
1617                        "extract": s.extract,
1618                        "use_values": s.use_values,
1619                        "use_body": s.use_body,
1620                        "merge_body": if s.merge_body.is_empty() { None } else { Some(&s.merge_body) },
1621                        "inject_attacks": s.inject_attacks,
1622                        "attack_types": s.attack_types,
1623                        "description": s.description,
1624                        "display_name": s.description.clone().unwrap_or_else(|| format!("Step {}", idx)),
1625                        "is_get_or_head": is_get_or_head,
1626                        "has_body": has_body,
1627                        "body": processed_body.value,
1628                        "body_is_dynamic": body_is_dynamic,
1629                        "_placeholders": processed_body.placeholders.iter().map(|p| format!("{:?}", p)).collect::<Vec<_>>(),
1630                    })
1631                }).collect::<Vec<_>>(),
1632            })
1633        }).collect();
1634
1635        // Collect all placeholders from all steps
1636        for flow_data in &flows_data {
1637            if let Some(steps) = flow_data.get("steps").and_then(|s| s.as_array()) {
1638                for step in steps {
1639                    if let Some(placeholders_arr) =
1640                        step.get("_placeholders").and_then(|p| p.as_array())
1641                    {
1642                        for p_str in placeholders_arr {
1643                            if let Some(p_name) = p_str.as_str() {
1644                                // Parse placeholder from debug string
1645                                match p_name {
1646                                    "VU" => {
1647                                        all_placeholders.insert(DynamicPlaceholder::VU);
1648                                    }
1649                                    "Iteration" => {
1650                                        all_placeholders.insert(DynamicPlaceholder::Iteration);
1651                                    }
1652                                    "Timestamp" => {
1653                                        all_placeholders.insert(DynamicPlaceholder::Timestamp);
1654                                    }
1655                                    "UUID" => {
1656                                        all_placeholders.insert(DynamicPlaceholder::UUID);
1657                                    }
1658                                    "Random" => {
1659                                        all_placeholders.insert(DynamicPlaceholder::Random);
1660                                    }
1661                                    "Counter" => {
1662                                        all_placeholders.insert(DynamicPlaceholder::Counter);
1663                                    }
1664                                    "Date" => {
1665                                        all_placeholders.insert(DynamicPlaceholder::Date);
1666                                    }
1667                                    "VuIter" => {
1668                                        all_placeholders.insert(DynamicPlaceholder::VuIter);
1669                                    }
1670                                    _ => {}
1671                                }
1672                            }
1673                        }
1674                    }
1675                }
1676            }
1677        }
1678
1679        // Get required imports and globals based on placeholders used
1680        let required_imports = DynamicParamProcessor::get_required_imports(&all_placeholders);
1681        let required_globals = DynamicParamProcessor::get_required_globals(&all_placeholders);
1682
1683        // Build invalid data config if error injection is enabled
1684        let invalid_data_config = self.build_invalid_data_config();
1685        let error_injection_enabled = invalid_data_config.is_some();
1686        let error_rate = self.error_rate.unwrap_or(0.0);
1687        let error_types: Vec<String> = invalid_data_config
1688            .as_ref()
1689            .map(|c| c.error_types.iter().map(|t| format!("{:?}", t)).collect())
1690            .unwrap_or_default();
1691
1692        if error_injection_enabled {
1693            TerminalReporter::print_progress(&format!(
1694                "Error injection enabled ({}% rate)",
1695                (error_rate * 100.0) as u32
1696            ));
1697        }
1698
1699        // Check if security testing is enabled
1700        let security_testing_enabled = self.wafbench_dir.is_some() || self.security_test;
1701
1702        let data = serde_json::json!({
1703            "base_url": self.target,
1704            "flows": flows_data,
1705            "extract_fields": config.default_extract_fields,
1706            "duration_secs": duration_secs,
1707            "max_vus": self.vus,
1708            "auth_header": self.auth,
1709            "custom_headers": custom_headers,
1710            "skip_tls_verify": self.skip_tls_verify,
1711            // Add missing template fields
1712            "stages": stages.iter().map(|s| serde_json::json!({
1713                "duration": s.duration,
1714                "target": s.target,
1715            })).collect::<Vec<_>>(),
1716            "threshold_percentile": self.threshold_percentile,
1717            "threshold_ms": self.threshold_ms,
1718            "max_error_rate": self.max_error_rate,
1719            "headers": headers_json,
1720            "dynamic_imports": required_imports,
1721            "dynamic_globals": required_globals,
1722            // Error injection settings
1723            "error_injection_enabled": error_injection_enabled,
1724            "error_rate": error_rate,
1725            "error_types": error_types,
1726            // Security testing settings
1727            "security_testing_enabled": security_testing_enabled,
1728            "has_custom_headers": !custom_headers.is_empty(),
1729        });
1730
1731        let mut script = handlebars
1732            .render_template(template, &data)
1733            .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
1734
1735        // Enhance script with security testing support if enabled
1736        if security_testing_enabled {
1737            script = self.generate_enhanced_script(&script)?;
1738        }
1739
1740        // Validate the generated CRUD flow script
1741        TerminalReporter::print_progress("Validating CRUD flow script...");
1742        let validation_errors = K6ScriptGenerator::validate_script(&script);
1743        if !validation_errors.is_empty() {
1744            TerminalReporter::print_error("CRUD flow script validation failed");
1745            for error in &validation_errors {
1746                eprintln!("  {}", error);
1747            }
1748            return Err(BenchError::Other(format!(
1749                "CRUD flow script validation failed with {} error(s)",
1750                validation_errors.len()
1751            )));
1752        }
1753
1754        TerminalReporter::print_success("CRUD flow script generated");
1755
1756        // Write and execute script
1757        let script_path = if let Some(output) = &self.script_output {
1758            output.clone()
1759        } else {
1760            self.output.join("k6-crud-flow-script.js")
1761        };
1762
1763        if let Some(parent) = script_path.parent() {
1764            std::fs::create_dir_all(parent)?;
1765        }
1766        std::fs::write(&script_path, &script)?;
1767        TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
1768
1769        if self.generate_only {
1770            println!("\nScript generated successfully. Run it with:");
1771            println!("  k6 run {}", script_path.display());
1772            return Ok(());
1773        }
1774
1775        // Execute k6
1776        TerminalReporter::print_progress("Executing CRUD flow test...");
1777        let executor = K6Executor::new()?;
1778        std::fs::create_dir_all(&self.output)?;
1779
1780        let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
1781
1782        let duration_secs = Self::parse_duration(&self.duration)?;
1783        TerminalReporter::print_summary(&results, duration_secs);
1784
1785        Ok(())
1786    }
1787
1788    /// Execute OpenAPI 3.0.0 conformance testing mode
1789    async fn execute_conformance_test(&self) -> Result<()> {
1790        use crate::conformance::generator::{ConformanceConfig, ConformanceGenerator};
1791        use crate::conformance::report::ConformanceReport;
1792        use crate::conformance::spec::ConformanceFeature;
1793
1794        TerminalReporter::print_progress("OpenAPI 3.0.0 Conformance Testing Mode");
1795
1796        // Parse category filter
1797        let categories = self.conformance_categories.as_ref().map(|cats_str| {
1798            cats_str
1799                .split(',')
1800                .filter_map(|s| {
1801                    let trimmed = s.trim();
1802                    if let Some(canonical) = ConformanceFeature::category_from_cli_name(trimmed) {
1803                        Some(canonical.to_string())
1804                    } else {
1805                        TerminalReporter::print_warning(&format!(
1806                            "Unknown conformance category: '{}'. Valid categories: {}",
1807                            trimmed,
1808                            ConformanceFeature::cli_category_names()
1809                                .iter()
1810                                .map(|(cli, _)| *cli)
1811                                .collect::<Vec<_>>()
1812                                .join(", ")
1813                        ));
1814                        None
1815                    }
1816                })
1817                .collect::<Vec<String>>()
1818        });
1819
1820        let config = ConformanceConfig {
1821            target_url: self.target.clone(),
1822            api_key: self.conformance_api_key.clone(),
1823            basic_auth: self.conformance_basic_auth.clone(),
1824            skip_tls_verify: self.skip_tls_verify,
1825            categories,
1826        };
1827
1828        // Branch: spec-driven mode vs reference mode
1829        let script = if !self.spec.is_empty() {
1830            // Spec-driven mode: analyze user's spec
1831            TerminalReporter::print_progress("Spec-driven conformance mode: analyzing spec...");
1832            let parser = SpecParser::from_file(&self.spec[0]).await?;
1833            let operations = parser.get_operations();
1834
1835            let annotated =
1836                crate::conformance::spec_driven::SpecDrivenConformanceGenerator::annotate_operations(
1837                    &operations,
1838                    parser.spec(),
1839                );
1840            TerminalReporter::print_success(&format!(
1841                "Analyzed {} operations, found {} feature annotations",
1842                operations.len(),
1843                annotated.iter().map(|a| a.features.len()).sum::<usize>()
1844            ));
1845
1846            let gen = crate::conformance::spec_driven::SpecDrivenConformanceGenerator::new(
1847                config, annotated,
1848            );
1849            gen.generate()?
1850        } else {
1851            // Reference mode: uses /conformance/ endpoints
1852            let generator = ConformanceGenerator::new(config);
1853            generator.generate()?
1854        };
1855
1856        // Write script
1857        std::fs::create_dir_all(&self.output)?;
1858        let script_path = self.output.join("k6-conformance.js");
1859        std::fs::write(&script_path, &script)
1860            .map_err(|e| BenchError::Other(format!("Failed to write conformance script: {}", e)))?;
1861        TerminalReporter::print_success(&format!(
1862            "Conformance script generated: {}",
1863            script_path.display()
1864        ));
1865
1866        // If generate-only, stop here
1867        if self.generate_only {
1868            println!("\nScript generated. Run with:");
1869            println!("  k6 run {}", script_path.display());
1870            return Ok(());
1871        }
1872
1873        // Validate k6
1874        if !K6Executor::is_k6_installed() {
1875            TerminalReporter::print_error("k6 is not installed");
1876            TerminalReporter::print_warning(
1877                "Install k6 from: https://k6.io/docs/get-started/installation/",
1878            );
1879            return Err(BenchError::K6NotFound);
1880        }
1881
1882        // Execute
1883        TerminalReporter::print_progress("Running conformance tests...");
1884        let executor = K6Executor::new()?;
1885        executor.execute(&script_path, Some(&self.output), self.verbose).await?;
1886
1887        // Parse and display report
1888        let report_path = self.output.join("conformance-report.json");
1889        if report_path.exists() {
1890            let report = ConformanceReport::from_file(&report_path)?;
1891            report.print_report();
1892
1893            // Output in requested format
1894            if self.conformance_report_format == "sarif" {
1895                use crate::conformance::sarif::ConformanceSarifReport;
1896                ConformanceSarifReport::write(&report, &self.target, &self.conformance_report)?;
1897                TerminalReporter::print_success(&format!(
1898                    "SARIF report saved to: {}",
1899                    self.conformance_report.display()
1900                ));
1901            } else if self.conformance_report != report_path {
1902                // Copy JSON report to user-specified location
1903                std::fs::copy(&report_path, &self.conformance_report)?;
1904                TerminalReporter::print_success(&format!(
1905                    "Report saved to: {}",
1906                    self.conformance_report.display()
1907                ));
1908            }
1909        } else {
1910            TerminalReporter::print_warning(
1911                "Conformance report not generated (k6 handleSummary may not have run)",
1912            );
1913        }
1914
1915        Ok(())
1916    }
1917
1918    /// Execute OWASP API Security Top 10 testing mode
1919    async fn execute_owasp_test(&self, parser: &SpecParser) -> Result<()> {
1920        TerminalReporter::print_progress("OWASP API Security Top 10 Testing Mode");
1921
1922        // Parse custom headers from CLI
1923        let custom_headers = self.parse_headers()?;
1924
1925        // Build OWASP configuration from CLI options
1926        let mut config = OwaspApiConfig::new()
1927            .with_auth_header(&self.owasp_auth_header)
1928            .with_verbose(self.verbose)
1929            .with_insecure(self.skip_tls_verify)
1930            .with_concurrency(self.vus as usize)
1931            .with_iterations(self.owasp_iterations as usize)
1932            .with_base_path(self.base_path.clone())
1933            .with_custom_headers(custom_headers);
1934
1935        // Set valid auth token if provided
1936        if let Some(ref token) = self.owasp_auth_token {
1937            config = config.with_valid_auth_token(token);
1938        }
1939
1940        // Parse categories if provided
1941        if let Some(ref cats_str) = self.owasp_categories {
1942            let categories: Vec<OwaspCategory> = cats_str
1943                .split(',')
1944                .filter_map(|s| {
1945                    let trimmed = s.trim();
1946                    match trimmed.parse::<OwaspCategory>() {
1947                        Ok(cat) => Some(cat),
1948                        Err(e) => {
1949                            TerminalReporter::print_warning(&e);
1950                            None
1951                        }
1952                    }
1953                })
1954                .collect();
1955
1956            if !categories.is_empty() {
1957                config = config.with_categories(categories);
1958            }
1959        }
1960
1961        // Load admin paths from file if provided
1962        if let Some(ref admin_paths_file) = self.owasp_admin_paths {
1963            config.admin_paths_file = Some(admin_paths_file.clone());
1964            if let Err(e) = config.load_admin_paths() {
1965                TerminalReporter::print_warning(&format!("Failed to load admin paths file: {}", e));
1966            }
1967        }
1968
1969        // Set ID fields if provided
1970        if let Some(ref id_fields_str) = self.owasp_id_fields {
1971            let id_fields: Vec<String> = id_fields_str
1972                .split(',')
1973                .map(|s| s.trim().to_string())
1974                .filter(|s| !s.is_empty())
1975                .collect();
1976            if !id_fields.is_empty() {
1977                config = config.with_id_fields(id_fields);
1978            }
1979        }
1980
1981        // Set report path and format
1982        if let Some(ref report_path) = self.owasp_report {
1983            config = config.with_report_path(report_path);
1984        }
1985        if let Ok(format) = self.owasp_report_format.parse::<ReportFormat>() {
1986            config = config.with_report_format(format);
1987        }
1988
1989        // Print configuration summary
1990        let categories = config.categories_to_test();
1991        TerminalReporter::print_success(&format!(
1992            "Testing {} OWASP categories: {}",
1993            categories.len(),
1994            categories.iter().map(|c| c.cli_name()).collect::<Vec<_>>().join(", ")
1995        ));
1996
1997        if config.valid_auth_token.is_some() {
1998            TerminalReporter::print_progress("Using provided auth token for baseline requests");
1999        }
2000
2001        // Create the OWASP generator
2002        TerminalReporter::print_progress("Generating OWASP security test script...");
2003        let generator = OwaspApiGenerator::new(config, self.target.clone(), parser);
2004
2005        // Generate the script
2006        let script = generator.generate()?;
2007        TerminalReporter::print_success("OWASP security test script generated");
2008
2009        // Write script to file
2010        let script_path = if let Some(output) = &self.script_output {
2011            output.clone()
2012        } else {
2013            self.output.join("k6-owasp-security-test.js")
2014        };
2015
2016        if let Some(parent) = script_path.parent() {
2017            std::fs::create_dir_all(parent)?;
2018        }
2019        std::fs::write(&script_path, &script)?;
2020        TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
2021
2022        // If generate-only mode, exit here
2023        if self.generate_only {
2024            println!("\nOWASP security test script generated. Run it with:");
2025            println!("  k6 run {}", script_path.display());
2026            return Ok(());
2027        }
2028
2029        // Execute k6
2030        TerminalReporter::print_progress("Executing OWASP security tests...");
2031        let executor = K6Executor::new()?;
2032        std::fs::create_dir_all(&self.output)?;
2033
2034        let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
2035
2036        let duration_secs = Self::parse_duration(&self.duration)?;
2037        TerminalReporter::print_summary(&results, duration_secs);
2038
2039        println!("\nOWASP security test results saved to: {}", self.output.display());
2040
2041        Ok(())
2042    }
2043}
2044
2045#[cfg(test)]
2046mod tests {
2047    use super::*;
2048
2049    #[test]
2050    fn test_parse_duration() {
2051        assert_eq!(BenchCommand::parse_duration("30s").unwrap(), 30);
2052        assert_eq!(BenchCommand::parse_duration("5m").unwrap(), 300);
2053        assert_eq!(BenchCommand::parse_duration("1h").unwrap(), 3600);
2054        assert_eq!(BenchCommand::parse_duration("60").unwrap(), 60);
2055    }
2056
2057    #[test]
2058    fn test_parse_duration_invalid() {
2059        assert!(BenchCommand::parse_duration("invalid").is_err());
2060        assert!(BenchCommand::parse_duration("30x").is_err());
2061    }
2062
2063    #[test]
2064    fn test_parse_headers() {
2065        let cmd = BenchCommand {
2066            spec: vec![PathBuf::from("test.yaml")],
2067            spec_dir: None,
2068            merge_conflicts: "error".to_string(),
2069            spec_mode: "merge".to_string(),
2070            dependency_config: None,
2071            target: "http://localhost".to_string(),
2072            base_path: None,
2073            duration: "1m".to_string(),
2074            vus: 10,
2075            scenario: "ramp-up".to_string(),
2076            operations: None,
2077            exclude_operations: None,
2078            auth: None,
2079            headers: Some("X-API-Key:test123,X-Client-ID:client456".to_string()),
2080            output: PathBuf::from("output"),
2081            generate_only: false,
2082            script_output: None,
2083            threshold_percentile: "p(95)".to_string(),
2084            threshold_ms: 500,
2085            max_error_rate: 0.05,
2086            verbose: false,
2087            skip_tls_verify: false,
2088            targets_file: None,
2089            max_concurrency: None,
2090            results_format: "both".to_string(),
2091            params_file: None,
2092            crud_flow: false,
2093            flow_config: None,
2094            extract_fields: None,
2095            parallel_create: None,
2096            data_file: None,
2097            data_distribution: "unique-per-vu".to_string(),
2098            data_mappings: None,
2099            per_uri_control: false,
2100            error_rate: None,
2101            error_types: None,
2102            security_test: false,
2103            security_payloads: None,
2104            security_categories: None,
2105            security_target_fields: None,
2106            wafbench_dir: None,
2107            wafbench_cycle_all: false,
2108            owasp_api_top10: false,
2109            owasp_categories: None,
2110            owasp_auth_header: "Authorization".to_string(),
2111            owasp_auth_token: None,
2112            owasp_admin_paths: None,
2113            owasp_id_fields: None,
2114            owasp_report: None,
2115            owasp_report_format: "json".to_string(),
2116            owasp_iterations: 1,
2117            conformance: false,
2118            conformance_api_key: None,
2119            conformance_basic_auth: None,
2120            conformance_report: PathBuf::from("conformance-report.json"),
2121            conformance_categories: None,
2122            conformance_report_format: "json".to_string(),
2123        };
2124
2125        let headers = cmd.parse_headers().unwrap();
2126        assert_eq!(headers.get("X-API-Key"), Some(&"test123".to_string()));
2127        assert_eq!(headers.get("X-Client-ID"), Some(&"client456".to_string()));
2128    }
2129
2130    #[test]
2131    fn test_get_spec_display_name() {
2132        let cmd = BenchCommand {
2133            spec: vec![PathBuf::from("test.yaml")],
2134            spec_dir: None,
2135            merge_conflicts: "error".to_string(),
2136            spec_mode: "merge".to_string(),
2137            dependency_config: None,
2138            target: "http://localhost".to_string(),
2139            base_path: None,
2140            duration: "1m".to_string(),
2141            vus: 10,
2142            scenario: "ramp-up".to_string(),
2143            operations: None,
2144            exclude_operations: None,
2145            auth: None,
2146            headers: None,
2147            output: PathBuf::from("output"),
2148            generate_only: false,
2149            script_output: None,
2150            threshold_percentile: "p(95)".to_string(),
2151            threshold_ms: 500,
2152            max_error_rate: 0.05,
2153            verbose: false,
2154            skip_tls_verify: false,
2155            targets_file: None,
2156            max_concurrency: None,
2157            results_format: "both".to_string(),
2158            params_file: None,
2159            crud_flow: false,
2160            flow_config: None,
2161            extract_fields: None,
2162            parallel_create: None,
2163            data_file: None,
2164            data_distribution: "unique-per-vu".to_string(),
2165            data_mappings: None,
2166            per_uri_control: false,
2167            error_rate: None,
2168            error_types: None,
2169            security_test: false,
2170            security_payloads: None,
2171            security_categories: None,
2172            security_target_fields: None,
2173            wafbench_dir: None,
2174            wafbench_cycle_all: false,
2175            owasp_api_top10: false,
2176            owasp_categories: None,
2177            owasp_auth_header: "Authorization".to_string(),
2178            owasp_auth_token: None,
2179            owasp_admin_paths: None,
2180            owasp_id_fields: None,
2181            owasp_report: None,
2182            owasp_report_format: "json".to_string(),
2183            owasp_iterations: 1,
2184            conformance: false,
2185            conformance_api_key: None,
2186            conformance_basic_auth: None,
2187            conformance_report: PathBuf::from("conformance-report.json"),
2188            conformance_categories: None,
2189            conformance_report_format: "json".to_string(),
2190        };
2191
2192        assert_eq!(cmd.get_spec_display_name(), "test.yaml");
2193
2194        // Test multiple specs
2195        let cmd_multi = BenchCommand {
2196            spec: vec![PathBuf::from("a.yaml"), PathBuf::from("b.yaml")],
2197            spec_dir: None,
2198            merge_conflicts: "error".to_string(),
2199            spec_mode: "merge".to_string(),
2200            dependency_config: None,
2201            target: "http://localhost".to_string(),
2202            base_path: None,
2203            duration: "1m".to_string(),
2204            vus: 10,
2205            scenario: "ramp-up".to_string(),
2206            operations: None,
2207            exclude_operations: None,
2208            auth: None,
2209            headers: None,
2210            output: PathBuf::from("output"),
2211            generate_only: false,
2212            script_output: None,
2213            threshold_percentile: "p(95)".to_string(),
2214            threshold_ms: 500,
2215            max_error_rate: 0.05,
2216            verbose: false,
2217            skip_tls_verify: false,
2218            targets_file: None,
2219            max_concurrency: None,
2220            results_format: "both".to_string(),
2221            params_file: None,
2222            crud_flow: false,
2223            flow_config: None,
2224            extract_fields: None,
2225            parallel_create: None,
2226            data_file: None,
2227            data_distribution: "unique-per-vu".to_string(),
2228            data_mappings: None,
2229            per_uri_control: false,
2230            error_rate: None,
2231            error_types: None,
2232            security_test: false,
2233            security_payloads: None,
2234            security_categories: None,
2235            security_target_fields: None,
2236            wafbench_dir: None,
2237            wafbench_cycle_all: false,
2238            owasp_api_top10: false,
2239            owasp_categories: None,
2240            owasp_auth_header: "Authorization".to_string(),
2241            owasp_auth_token: None,
2242            owasp_admin_paths: None,
2243            owasp_id_fields: None,
2244            owasp_report: None,
2245            owasp_report_format: "json".to_string(),
2246            owasp_iterations: 1,
2247            conformance: false,
2248            conformance_api_key: None,
2249            conformance_basic_auth: None,
2250            conformance_report: PathBuf::from("conformance-report.json"),
2251            conformance_categories: None,
2252            conformance_report_format: "json".to_string(),
2253        };
2254
2255        assert_eq!(cmd_multi.get_spec_display_name(), "2 spec files");
2256    }
2257}