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::{Path, PathBuf};
35use std::str::FromStr;
36
37/// Parse a comma-separated header string into a `HashMap`.
38///
39/// Format: `Key:Value,Key2:Value2`
40///
41/// **Known limitation**: Header values containing commas will be incorrectly
42/// split. Cookie headers with semicolons work fine, but Cookie values with
43/// commas (e.g. `expires=Thu, 01 Jan 2099`) will break.
44pub fn parse_header_string(input: &str) -> Result<HashMap<String, String>> {
45    let mut headers = HashMap::new();
46
47    for pair in input.split(',') {
48        let parts: Vec<&str> = pair.splitn(2, ':').collect();
49        if parts.len() != 2 {
50            return Err(BenchError::Other(format!(
51                "Invalid header format: '{}'. Expected 'Key:Value'",
52                pair
53            )));
54        }
55        headers.insert(parts[0].trim().to_string(), parts[1].trim().to_string());
56    }
57
58    Ok(headers)
59}
60
61/// Bench command configuration
62pub struct BenchCommand {
63    /// OpenAPI spec file(s) - can specify multiple
64    pub spec: Vec<PathBuf>,
65    /// Directory containing OpenAPI spec files (discovers .json, .yaml, .yml files)
66    pub spec_dir: Option<PathBuf>,
67    /// Conflict resolution strategy when merging multiple specs: "error" (default), "first", "last"
68    pub merge_conflicts: String,
69    /// Spec mode: "merge" (default) combines all specs, "sequential" runs them in order
70    pub spec_mode: String,
71    /// Dependency configuration file for cross-spec value passing (used with sequential mode)
72    pub dependency_config: Option<PathBuf>,
73    pub target: String,
74    /// API base path prefix (e.g., "/api" or "/v2/api")
75    /// If None, extracts from OpenAPI spec's servers URL
76    pub base_path: Option<String>,
77    pub duration: String,
78    pub vus: u32,
79    pub scenario: String,
80    pub operations: Option<String>,
81    /// Exclude operations from testing (comma-separated)
82    ///
83    /// Supports "METHOD /path" or just "METHOD" to exclude all operations of that type.
84    pub exclude_operations: Option<String>,
85    pub auth: Option<String>,
86    pub headers: Option<String>,
87    pub output: PathBuf,
88    pub generate_only: bool,
89    pub script_output: Option<PathBuf>,
90    pub threshold_percentile: String,
91    pub threshold_ms: u64,
92    pub max_error_rate: f64,
93    pub verbose: bool,
94    pub skip_tls_verify: bool,
95    /// Optional file containing multiple targets
96    pub targets_file: Option<PathBuf>,
97    /// Maximum number of parallel executions (for multi-target mode)
98    pub max_concurrency: Option<u32>,
99    /// Results format: "per-target", "aggregated", or "both"
100    pub results_format: String,
101    /// Optional file containing parameter value overrides (JSON or YAML)
102    ///
103    /// Allows users to provide custom values for path parameters, query parameters,
104    /// headers, and request bodies instead of auto-generated placeholder values.
105    pub params_file: Option<PathBuf>,
106
107    // === CRUD Flow Options ===
108    /// Enable CRUD flow mode
109    pub crud_flow: bool,
110    /// Custom CRUD flow configuration file
111    pub flow_config: Option<PathBuf>,
112    /// Fields to extract from responses
113    pub extract_fields: Option<String>,
114
115    // === Parallel Execution Options ===
116    /// Number of resources to create in parallel
117    pub parallel_create: Option<u32>,
118
119    // === Data-Driven Testing Options ===
120    /// Test data file (CSV or JSON)
121    pub data_file: Option<PathBuf>,
122    /// Data distribution strategy
123    pub data_distribution: String,
124    /// Data column to field mappings
125    pub data_mappings: Option<String>,
126    /// Enable per-URI control mode (each row specifies method, uri, body, etc.)
127    pub per_uri_control: bool,
128
129    // === Invalid Data Testing Options ===
130    /// Percentage of requests with invalid data
131    pub error_rate: Option<f64>,
132    /// Types of invalid data to generate
133    pub error_types: Option<String>,
134
135    // === Security Testing Options ===
136    /// Enable security testing
137    pub security_test: bool,
138    /// Custom security payloads file
139    pub security_payloads: Option<PathBuf>,
140    /// Security test categories
141    pub security_categories: Option<String>,
142    /// Fields to target for security injection
143    pub security_target_fields: Option<String>,
144
145    // === WAFBench Integration ===
146    /// WAFBench test directory or glob pattern for loading CRS attack patterns
147    pub wafbench_dir: Option<String>,
148    /// Cycle through ALL WAFBench payloads instead of random sampling
149    pub wafbench_cycle_all: bool,
150
151    // === OpenAPI 3.0.0 Conformance Testing ===
152    /// Enable conformance testing mode
153    pub conformance: bool,
154    /// API key for conformance security tests
155    pub conformance_api_key: Option<String>,
156    /// Basic auth credentials for conformance security tests (user:pass)
157    pub conformance_basic_auth: Option<String>,
158    /// Conformance report output file
159    pub conformance_report: PathBuf,
160    /// Conformance categories to test (comma-separated, e.g. "parameters,security")
161    pub conformance_categories: Option<String>,
162    /// Conformance report format: "json" or "sarif"
163    pub conformance_report_format: String,
164    /// Custom headers to inject into every conformance request (for authentication).
165    /// Each entry is "Header-Name: value" format.
166    pub conformance_headers: Vec<String>,
167    /// When true, test ALL operations for method/response/body categories
168    /// instead of just one representative per feature check.
169    pub conformance_all_operations: bool,
170    /// Optional YAML file with custom conformance checks
171    pub conformance_custom: Option<PathBuf>,
172    /// Delay in milliseconds between consecutive conformance requests.
173    /// Useful when testing against rate-limited APIs.
174    pub conformance_delay_ms: u64,
175    /// Use k6 for conformance test execution instead of the native Rust executor
176    pub use_k6: bool,
177    /// Regex filter for custom conformance checks — only checks whose name or
178    /// path matches the pattern are included. Example: "wafcrs|ssl" to test
179    /// only checks with "wafcrs" or "ssl" in the name/path.
180    pub conformance_custom_filter: Option<String>,
181    /// When true, export all request/response pairs to
182    /// `conformance-requests.json` in the output directory.
183    pub export_requests: bool,
184    /// When true, validate each request against the OpenAPI spec and report
185    /// violations to `conformance-request-violations.json`.
186    pub validate_requests: bool,
187
188    // === OWASP API Security Top 10 Testing ===
189    /// Enable OWASP API Security Top 10 testing mode
190    pub owasp_api_top10: bool,
191    /// OWASP API categories to test (comma-separated)
192    pub owasp_categories: Option<String>,
193    /// Authorization header name for OWASP auth tests
194    pub owasp_auth_header: String,
195    /// Valid authorization token for OWASP baseline requests
196    pub owasp_auth_token: Option<String>,
197    /// File containing admin/privileged paths to test
198    pub owasp_admin_paths: Option<PathBuf>,
199    /// Fields containing resource IDs for BOLA testing
200    pub owasp_id_fields: Option<String>,
201    /// OWASP report output file
202    pub owasp_report: Option<PathBuf>,
203    /// OWASP report format (json, sarif)
204    pub owasp_report_format: String,
205    /// Number of iterations per VU for OWASP tests (default: 1)
206    pub owasp_iterations: u32,
207}
208
209impl BenchCommand {
210    /// Load and merge specs from --spec files and --spec-dir
211    pub async fn load_and_merge_specs(&self) -> Result<OpenApiSpec> {
212        let mut all_specs: Vec<(PathBuf, OpenApiSpec)> = Vec::new();
213
214        // Load specs from --spec flags
215        if !self.spec.is_empty() {
216            let specs = load_specs_from_files(self.spec.clone())
217                .await
218                .map_err(|e| BenchError::Other(format!("Failed to load spec files: {}", e)))?;
219            all_specs.extend(specs);
220        }
221
222        // Load specs from --spec-dir if provided
223        if let Some(spec_dir) = &self.spec_dir {
224            let dir_specs = load_specs_from_directory(spec_dir).await.map_err(|e| {
225                BenchError::Other(format!("Failed to load specs from directory: {}", e))
226            })?;
227            all_specs.extend(dir_specs);
228        }
229
230        if all_specs.is_empty() {
231            return Err(BenchError::Other(
232                "No spec files provided. Use --spec or --spec-dir.".to_string(),
233            ));
234        }
235
236        // If only one spec, return it directly (extract just the OpenApiSpec)
237        if all_specs.len() == 1 {
238            // Safe to unwrap because we just checked len() == 1
239            return Ok(all_specs.into_iter().next().expect("checked len() == 1 above").1);
240        }
241
242        // Merge multiple specs
243        let conflict_strategy = match self.merge_conflicts.as_str() {
244            "first" => ConflictStrategy::First,
245            "last" => ConflictStrategy::Last,
246            _ => ConflictStrategy::Error,
247        };
248
249        merge_specs(all_specs, conflict_strategy)
250            .map_err(|e| BenchError::Other(format!("Failed to merge specs: {}", e)))
251    }
252
253    /// Get a display name for the spec(s)
254    fn get_spec_display_name(&self) -> String {
255        if self.spec.len() == 1 {
256            self.spec[0].to_string_lossy().to_string()
257        } else if !self.spec.is_empty() {
258            format!("{} spec files", self.spec.len())
259        } else if let Some(dir) = &self.spec_dir {
260            format!("specs from {}", dir.display())
261        } else {
262            "no specs".to_string()
263        }
264    }
265
266    /// Execute the bench command
267    pub async fn execute(&self) -> Result<()> {
268        // Check if we're in multi-target mode
269        if let Some(targets_file) = &self.targets_file {
270            if self.conformance {
271                return self.execute_multi_target_conformance(targets_file).await;
272            }
273            return self.execute_multi_target(targets_file).await;
274        }
275
276        // Check if we're in sequential spec mode (for dependency handling)
277        if self.spec_mode == "sequential" && (self.spec.len() > 1 || self.spec_dir.is_some()) {
278            return self.execute_sequential_specs().await;
279        }
280
281        // Single target mode (existing behavior)
282        // Print header
283        TerminalReporter::print_header(
284            &self.get_spec_display_name(),
285            &self.target,
286            0, // Will be updated later
287            &self.scenario,
288            Self::parse_duration(&self.duration)?,
289        );
290
291        // Validate k6 installation
292        if !K6Executor::is_k6_installed() {
293            TerminalReporter::print_error("k6 is not installed");
294            TerminalReporter::print_warning(
295                "Install k6 from: https://k6.io/docs/get-started/installation/",
296            );
297            return Err(BenchError::K6NotFound);
298        }
299
300        // Check for conformance testing mode (before spec loading — conformance doesn't need a user spec)
301        if self.conformance {
302            return self.execute_conformance_test().await;
303        }
304
305        // Load and parse spec(s)
306        TerminalReporter::print_progress("Loading OpenAPI specification(s)...");
307        let merged_spec = self.load_and_merge_specs().await?;
308        let parser = SpecParser::from_spec(merged_spec);
309        if self.spec.len() > 1 || self.spec_dir.is_some() {
310            TerminalReporter::print_success(&format!(
311                "Loaded and merged {} specification(s)",
312                self.spec.len() + self.spec_dir.as_ref().map(|_| 1).unwrap_or(0)
313            ));
314        } else {
315            TerminalReporter::print_success("Specification loaded");
316        }
317
318        // Check for mock server integration
319        let mock_config = self.build_mock_config().await;
320        if mock_config.is_mock_server {
321            TerminalReporter::print_progress("Mock server integration enabled");
322        }
323
324        // Check for CRUD flow mode
325        if self.crud_flow {
326            return self.execute_crud_flow(&parser).await;
327        }
328
329        // Check for OWASP API Top 10 testing mode
330        if self.owasp_api_top10 {
331            return self.execute_owasp_test(&parser).await;
332        }
333
334        // Get operations
335        TerminalReporter::print_progress("Extracting API operations...");
336        let mut operations = if let Some(filter) = &self.operations {
337            parser.filter_operations(filter)?
338        } else {
339            parser.get_operations()
340        };
341
342        // Apply exclusions if provided
343        if let Some(exclude) = &self.exclude_operations {
344            let before_count = operations.len();
345            operations = parser.exclude_operations(operations, exclude)?;
346            let excluded_count = before_count - operations.len();
347            if excluded_count > 0 {
348                TerminalReporter::print_progress(&format!(
349                    "Excluded {} operations matching '{}'",
350                    excluded_count, exclude
351                ));
352            }
353        }
354
355        if operations.is_empty() {
356            return Err(BenchError::Other("No operations found in spec".to_string()));
357        }
358
359        TerminalReporter::print_success(&format!("Found {} operations", operations.len()));
360
361        // Load parameter overrides if provided
362        let param_overrides = if let Some(params_file) = &self.params_file {
363            TerminalReporter::print_progress("Loading parameter overrides...");
364            let overrides = ParameterOverrides::from_file(params_file)?;
365            TerminalReporter::print_success(&format!(
366                "Loaded parameter overrides ({} operation-specific, {} defaults)",
367                overrides.operations.len(),
368                if overrides.defaults.is_empty() { 0 } else { 1 }
369            ));
370            Some(overrides)
371        } else {
372            None
373        };
374
375        // Generate request templates
376        TerminalReporter::print_progress("Generating request templates...");
377        let templates: Vec<_> = operations
378            .iter()
379            .map(|op| {
380                let op_overrides = param_overrides.as_ref().map(|po| {
381                    po.get_for_operation(op.operation_id.as_deref(), &op.method, &op.path)
382                });
383                RequestGenerator::generate_template_with_overrides(op, op_overrides.as_ref())
384            })
385            .collect::<Result<Vec<_>>>()?;
386        TerminalReporter::print_success("Request templates generated");
387
388        // Parse headers
389        let custom_headers = self.parse_headers()?;
390
391        // Resolve base path (CLI option takes priority over spec's servers URL)
392        let base_path = self.resolve_base_path(&parser);
393        if let Some(ref bp) = base_path {
394            TerminalReporter::print_progress(&format!("Using base path: {}", bp));
395        }
396
397        // Generate k6 script
398        TerminalReporter::print_progress("Generating k6 load test script...");
399        let scenario =
400            LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
401
402        let security_testing_enabled = self.security_test || self.wafbench_dir.is_some();
403
404        let k6_config = K6Config {
405            target_url: self.target.clone(),
406            base_path,
407            scenario,
408            duration_secs: Self::parse_duration(&self.duration)?,
409            max_vus: self.vus,
410            threshold_percentile: self.threshold_percentile.clone(),
411            threshold_ms: self.threshold_ms,
412            max_error_rate: self.max_error_rate,
413            auth_header: self.auth.clone(),
414            custom_headers,
415            skip_tls_verify: self.skip_tls_verify,
416            security_testing_enabled,
417        };
418
419        let generator = K6ScriptGenerator::new(k6_config, templates);
420        let mut script = generator.generate()?;
421        TerminalReporter::print_success("k6 script generated");
422
423        // Check if any advanced features are enabled
424        let has_advanced_features = self.data_file.is_some()
425            || self.error_rate.is_some()
426            || self.security_test
427            || self.parallel_create.is_some()
428            || self.wafbench_dir.is_some();
429
430        // Enhance script with advanced features
431        if has_advanced_features {
432            script = self.generate_enhanced_script(&script)?;
433        }
434
435        // Add mock server integration code
436        if mock_config.is_mock_server {
437            let setup_code = MockIntegrationGenerator::generate_setup(&mock_config);
438            let teardown_code = MockIntegrationGenerator::generate_teardown(&mock_config);
439            let helper_code = MockIntegrationGenerator::generate_vu_id_helper();
440
441            // Insert mock server code after imports
442            if let Some(import_end) = script.find("export const options") {
443                script.insert_str(
444                    import_end,
445                    &format!(
446                        "\n// === Mock Server Integration ===\n{}\n{}\n{}\n",
447                        helper_code, setup_code, teardown_code
448                    ),
449                );
450            }
451        }
452
453        // Validate the generated script
454        TerminalReporter::print_progress("Validating k6 script...");
455        let validation_errors = K6ScriptGenerator::validate_script(&script);
456        if !validation_errors.is_empty() {
457            TerminalReporter::print_error("Script validation failed");
458            for error in &validation_errors {
459                eprintln!("  {}", error);
460            }
461            return Err(BenchError::Other(format!(
462                "Generated k6 script has {} validation error(s). Please check the output above.",
463                validation_errors.len()
464            )));
465        }
466        TerminalReporter::print_success("Script validation passed");
467
468        // Write script to file
469        let script_path = if let Some(output) = &self.script_output {
470            output.clone()
471        } else {
472            self.output.join("k6-script.js")
473        };
474
475        if let Some(parent) = script_path.parent() {
476            std::fs::create_dir_all(parent)?;
477        }
478        std::fs::write(&script_path, &script)?;
479        TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
480
481        // If generate-only mode, exit here
482        if self.generate_only {
483            println!("\nScript generated successfully. Run it with:");
484            println!("  k6 run {}", script_path.display());
485            return Ok(());
486        }
487
488        // Execute k6
489        TerminalReporter::print_progress("Executing load test...");
490        let executor = K6Executor::new()?;
491
492        std::fs::create_dir_all(&self.output)?;
493
494        let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
495
496        // Print results
497        let duration_secs = Self::parse_duration(&self.duration)?;
498        TerminalReporter::print_summary(&results, duration_secs);
499
500        println!("\nResults saved to: {}", self.output.display());
501
502        Ok(())
503    }
504
505    /// Execute multi-target bench testing
506    async fn execute_multi_target(&self, targets_file: &Path) -> Result<()> {
507        TerminalReporter::print_progress("Parsing targets file...");
508        let targets = parse_targets_file(targets_file)?;
509        let num_targets = targets.len();
510        TerminalReporter::print_success(&format!("Loaded {} targets", num_targets));
511
512        if targets.is_empty() {
513            return Err(BenchError::Other("No targets found in file".to_string()));
514        }
515
516        // Determine max concurrency
517        let max_concurrency = self.max_concurrency.unwrap_or(10) as usize;
518        let max_concurrency = max_concurrency.min(num_targets); // Don't exceed number of targets
519
520        // Print header for multi-target mode
521        TerminalReporter::print_header(
522            &self.get_spec_display_name(),
523            &format!("{} targets", num_targets),
524            0,
525            &self.scenario,
526            Self::parse_duration(&self.duration)?,
527        );
528
529        // Create parallel executor
530        let executor = ParallelExecutor::new(
531            BenchCommand {
532                // Clone all fields except targets_file (we don't need it in the executor)
533                spec: self.spec.clone(),
534                spec_dir: self.spec_dir.clone(),
535                merge_conflicts: self.merge_conflicts.clone(),
536                spec_mode: self.spec_mode.clone(),
537                dependency_config: self.dependency_config.clone(),
538                target: self.target.clone(), // Not used in multi-target mode, but kept for compatibility
539                base_path: self.base_path.clone(),
540                duration: self.duration.clone(),
541                vus: self.vus,
542                scenario: self.scenario.clone(),
543                operations: self.operations.clone(),
544                exclude_operations: self.exclude_operations.clone(),
545                auth: self.auth.clone(),
546                headers: self.headers.clone(),
547                output: self.output.clone(),
548                generate_only: self.generate_only,
549                script_output: self.script_output.clone(),
550                threshold_percentile: self.threshold_percentile.clone(),
551                threshold_ms: self.threshold_ms,
552                max_error_rate: self.max_error_rate,
553                verbose: self.verbose,
554                skip_tls_verify: self.skip_tls_verify,
555                targets_file: None,
556                max_concurrency: None,
557                results_format: self.results_format.clone(),
558                params_file: self.params_file.clone(),
559                crud_flow: self.crud_flow,
560                flow_config: self.flow_config.clone(),
561                extract_fields: self.extract_fields.clone(),
562                parallel_create: self.parallel_create,
563                data_file: self.data_file.clone(),
564                data_distribution: self.data_distribution.clone(),
565                data_mappings: self.data_mappings.clone(),
566                per_uri_control: self.per_uri_control,
567                error_rate: self.error_rate,
568                error_types: self.error_types.clone(),
569                security_test: self.security_test,
570                security_payloads: self.security_payloads.clone(),
571                security_categories: self.security_categories.clone(),
572                security_target_fields: self.security_target_fields.clone(),
573                wafbench_dir: self.wafbench_dir.clone(),
574                wafbench_cycle_all: self.wafbench_cycle_all,
575                owasp_api_top10: self.owasp_api_top10,
576                owasp_categories: self.owasp_categories.clone(),
577                owasp_auth_header: self.owasp_auth_header.clone(),
578                owasp_auth_token: self.owasp_auth_token.clone(),
579                owasp_admin_paths: self.owasp_admin_paths.clone(),
580                owasp_id_fields: self.owasp_id_fields.clone(),
581                owasp_report: self.owasp_report.clone(),
582                owasp_report_format: self.owasp_report_format.clone(),
583                owasp_iterations: self.owasp_iterations,
584                conformance: false,
585                conformance_api_key: None,
586                conformance_basic_auth: None,
587                conformance_report: PathBuf::from("conformance-report.json"),
588                conformance_categories: None,
589                conformance_report_format: "json".to_string(),
590                conformance_headers: vec![],
591                conformance_all_operations: false,
592                conformance_custom: None,
593                conformance_delay_ms: 0,
594                use_k6: false,
595                conformance_custom_filter: None,
596                export_requests: false,
597                validate_requests: false,
598            },
599            targets,
600            max_concurrency,
601        );
602
603        // Execute all targets
604        let start_time = std::time::Instant::now();
605        let aggregated_results = executor.execute_all().await?;
606        let elapsed = start_time.elapsed();
607
608        // Organize and report results
609        self.report_multi_target_results(&aggregated_results, elapsed)?;
610
611        Ok(())
612    }
613
614    /// Report results for multi-target execution
615    fn report_multi_target_results(
616        &self,
617        results: &AggregatedResults,
618        elapsed: std::time::Duration,
619    ) -> Result<()> {
620        // Print summary
621        TerminalReporter::print_multi_target_summary(results);
622
623        // Print elapsed time
624        let total_secs = elapsed.as_secs();
625        let hours = total_secs / 3600;
626        let minutes = (total_secs % 3600) / 60;
627        let seconds = total_secs % 60;
628        if hours > 0 {
629            println!("\n  Total Elapsed Time:   {}h {}m {}s", hours, minutes, seconds);
630        } else if minutes > 0 {
631            println!("\n  Total Elapsed Time:   {}m {}s", minutes, seconds);
632        } else {
633            println!("\n  Total Elapsed Time:   {}s", seconds);
634        }
635
636        // Save aggregated summary if requested
637        if self.results_format == "aggregated" || self.results_format == "both" {
638            let summary_path = self.output.join("aggregated_summary.json");
639            let summary_json = serde_json::json!({
640                "total_elapsed_seconds": elapsed.as_secs(),
641                "total_targets": results.total_targets,
642                "successful_targets": results.successful_targets,
643                "failed_targets": results.failed_targets,
644                "aggregated_metrics": {
645                    "total_requests": results.aggregated_metrics.total_requests,
646                    "total_failed_requests": results.aggregated_metrics.total_failed_requests,
647                    "avg_duration_ms": results.aggregated_metrics.avg_duration_ms,
648                    "p95_duration_ms": results.aggregated_metrics.p95_duration_ms,
649                    "p99_duration_ms": results.aggregated_metrics.p99_duration_ms,
650                    "error_rate": results.aggregated_metrics.error_rate,
651                    "total_rps": results.aggregated_metrics.total_rps,
652                    "avg_rps": results.aggregated_metrics.avg_rps,
653                    "total_vus_max": results.aggregated_metrics.total_vus_max,
654                },
655                "target_results": results.target_results.iter().map(|r| {
656                    serde_json::json!({
657                        "target_url": r.target_url,
658                        "target_index": r.target_index,
659                        "success": r.success,
660                        "error": r.error,
661                        "total_requests": r.results.total_requests,
662                        "failed_requests": r.results.failed_requests,
663                        "avg_duration_ms": r.results.avg_duration_ms,
664                        "min_duration_ms": r.results.min_duration_ms,
665                        "med_duration_ms": r.results.med_duration_ms,
666                        "p90_duration_ms": r.results.p90_duration_ms,
667                        "p95_duration_ms": r.results.p95_duration_ms,
668                        "p99_duration_ms": r.results.p99_duration_ms,
669                        "max_duration_ms": r.results.max_duration_ms,
670                        "rps": r.results.rps,
671                        "vus_max": r.results.vus_max,
672                        "output_dir": r.output_dir.to_string_lossy(),
673                    })
674                }).collect::<Vec<_>>(),
675            });
676
677            std::fs::write(&summary_path, serde_json::to_string_pretty(&summary_json)?)?;
678            TerminalReporter::print_success(&format!(
679                "Aggregated summary saved to: {}",
680                summary_path.display()
681            ));
682        }
683
684        // Write CSV with all per-target results for easy parsing
685        let csv_path = self.output.join("all_targets.csv");
686        let mut csv = String::from(
687            "target_url,success,requests,failed,rps,vus,min_ms,avg_ms,med_ms,p90_ms,p95_ms,p99_ms,max_ms,error\n",
688        );
689        for r in &results.target_results {
690            csv.push_str(&format!(
691                "{},{},{},{},{:.1},{},{:.1},{:.1},{:.1},{:.1},{:.1},{:.1},{:.1},{}\n",
692                r.target_url,
693                r.success,
694                r.results.total_requests,
695                r.results.failed_requests,
696                r.results.rps,
697                r.results.vus_max,
698                r.results.min_duration_ms,
699                r.results.avg_duration_ms,
700                r.results.med_duration_ms,
701                r.results.p90_duration_ms,
702                r.results.p95_duration_ms,
703                r.results.p99_duration_ms,
704                r.results.max_duration_ms,
705                r.error.as_deref().unwrap_or(""),
706            ));
707        }
708        let _ = std::fs::write(&csv_path, &csv);
709
710        println!("\nResults saved to: {}", self.output.display());
711        println!("  - Per-target results: {}", self.output.join("target_*").display());
712        println!("  - All targets CSV:    {}", csv_path.display());
713        if self.results_format == "aggregated" || self.results_format == "both" {
714            println!(
715                "  - Aggregated summary: {}",
716                self.output.join("aggregated_summary.json").display()
717            );
718        }
719
720        Ok(())
721    }
722
723    /// Parse duration string (e.g., "30s", "5m", "1h") to seconds
724    pub fn parse_duration(duration: &str) -> Result<u64> {
725        let duration = duration.trim();
726
727        if let Some(secs) = duration.strip_suffix('s') {
728            secs.parse::<u64>()
729                .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
730        } else if let Some(mins) = duration.strip_suffix('m') {
731            mins.parse::<u64>()
732                .map(|m| m * 60)
733                .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
734        } else if let Some(hours) = duration.strip_suffix('h') {
735            hours
736                .parse::<u64>()
737                .map(|h| h * 3600)
738                .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
739        } else {
740            // Try parsing as seconds without suffix
741            duration
742                .parse::<u64>()
743                .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
744        }
745    }
746
747    /// Parse headers from command line format (Key:Value,Key2:Value2)
748    pub fn parse_headers(&self) -> Result<HashMap<String, String>> {
749        match &self.headers {
750            Some(s) => parse_header_string(s),
751            None => Ok(HashMap::new()),
752        }
753    }
754
755    fn parse_extracted_values(output_dir: &Path) -> Result<ExtractedValues> {
756        let extracted_path = output_dir.join("extracted_values.json");
757        if !extracted_path.exists() {
758            return Ok(ExtractedValues::new());
759        }
760
761        let content = std::fs::read_to_string(&extracted_path)
762            .map_err(|e| BenchError::ResultsParseError(e.to_string()))?;
763        let parsed: serde_json::Value = serde_json::from_str(&content)
764            .map_err(|e| BenchError::ResultsParseError(e.to_string()))?;
765
766        let mut extracted = ExtractedValues::new();
767        if let Some(values) = parsed.as_object() {
768            for (key, value) in values {
769                extracted.set(key.clone(), value.clone());
770            }
771        }
772
773        Ok(extracted)
774    }
775
776    /// Resolve the effective base path for API endpoints
777    ///
778    /// Priority:
779    /// 1. CLI --base-path option (if provided, even if empty string)
780    /// 2. Base path extracted from OpenAPI spec's servers URL
781    /// 3. None (no base path)
782    ///
783    /// An empty string from CLI explicitly disables base path.
784    fn resolve_base_path(&self, parser: &SpecParser) -> Option<String> {
785        // CLI option takes priority (including empty string to disable)
786        if let Some(cli_base_path) = &self.base_path {
787            if cli_base_path.is_empty() {
788                // Empty string explicitly means "no base path"
789                return None;
790            }
791            return Some(cli_base_path.clone());
792        }
793
794        // Fall back to spec's base path
795        parser.get_base_path()
796    }
797
798    /// Build mock server integration configuration
799    async fn build_mock_config(&self) -> MockIntegrationConfig {
800        // Check if target looks like a mock server
801        if MockServerDetector::looks_like_mock_server(&self.target) {
802            // Try to detect if it's actually a MockForge server
803            if let Ok(info) = MockServerDetector::detect(&self.target).await {
804                if info.is_mockforge {
805                    TerminalReporter::print_success(&format!(
806                        "Detected MockForge server (version: {})",
807                        info.version.as_deref().unwrap_or("unknown")
808                    ));
809                    return MockIntegrationConfig::mock_server();
810                }
811            }
812        }
813        MockIntegrationConfig::real_api()
814    }
815
816    /// Build CRUD flow configuration
817    fn build_crud_flow_config(&self) -> Option<CrudFlowConfig> {
818        if !self.crud_flow {
819            return None;
820        }
821
822        // If flow_config file is provided, load it
823        if let Some(config_path) = &self.flow_config {
824            match CrudFlowConfig::from_file(config_path) {
825                Ok(config) => return Some(config),
826                Err(e) => {
827                    TerminalReporter::print_warning(&format!(
828                        "Failed to load flow config: {}. Using auto-detection.",
829                        e
830                    ));
831                }
832            }
833        }
834
835        // Parse extract fields
836        let extract_fields = self
837            .extract_fields
838            .as_ref()
839            .map(|f| f.split(',').map(|s| s.trim().to_string()).collect())
840            .unwrap_or_else(|| vec!["id".to_string(), "uuid".to_string()]);
841
842        Some(CrudFlowConfig {
843            flows: Vec::new(), // Will be auto-detected
844            default_extract_fields: extract_fields,
845        })
846    }
847
848    /// Build data-driven testing configuration
849    fn build_data_driven_config(&self) -> Option<DataDrivenConfig> {
850        let data_file = self.data_file.as_ref()?;
851
852        let distribution = DataDistribution::from_str(&self.data_distribution)
853            .unwrap_or(DataDistribution::UniquePerVu);
854
855        let mappings = self
856            .data_mappings
857            .as_ref()
858            .map(|m| DataMapping::parse_mappings(m).unwrap_or_default())
859            .unwrap_or_default();
860
861        Some(DataDrivenConfig {
862            file_path: data_file.to_string_lossy().to_string(),
863            distribution,
864            mappings,
865            csv_has_header: true,
866            per_uri_control: self.per_uri_control,
867            per_uri_columns: crate::data_driven::PerUriColumns::default(),
868        })
869    }
870
871    /// Build invalid data testing configuration
872    fn build_invalid_data_config(&self) -> Option<InvalidDataConfig> {
873        let error_rate = self.error_rate?;
874
875        let error_types = self
876            .error_types
877            .as_ref()
878            .map(|types| InvalidDataConfig::parse_error_types(types).unwrap_or_default())
879            .unwrap_or_default();
880
881        Some(InvalidDataConfig {
882            error_rate,
883            error_types,
884            target_fields: Vec::new(),
885        })
886    }
887
888    /// Build security testing configuration
889    fn build_security_config(&self) -> Option<SecurityTestConfig> {
890        if !self.security_test {
891            return None;
892        }
893
894        let categories = self
895            .security_categories
896            .as_ref()
897            .map(|cats| SecurityTestConfig::parse_categories(cats).unwrap_or_default())
898            .unwrap_or_else(|| {
899                let mut default = HashSet::new();
900                default.insert(SecurityCategory::SqlInjection);
901                default.insert(SecurityCategory::Xss);
902                default
903            });
904
905        let target_fields = self
906            .security_target_fields
907            .as_ref()
908            .map(|fields| fields.split(',').map(|f| f.trim().to_string()).collect())
909            .unwrap_or_default();
910
911        let custom_payloads_file =
912            self.security_payloads.as_ref().map(|p| p.to_string_lossy().to_string());
913
914        Some(SecurityTestConfig {
915            enabled: true,
916            categories,
917            target_fields,
918            custom_payloads_file,
919            include_high_risk: false,
920        })
921    }
922
923    /// Build parallel execution configuration
924    fn build_parallel_config(&self) -> Option<ParallelConfig> {
925        let count = self.parallel_create?;
926
927        Some(ParallelConfig::new(count))
928    }
929
930    /// Load WAFBench payloads from the specified directory or pattern
931    fn load_wafbench_payloads(&self) -> Vec<SecurityPayload> {
932        let Some(ref wafbench_dir) = self.wafbench_dir else {
933            return Vec::new();
934        };
935
936        let mut loader = WafBenchLoader::new();
937
938        if let Err(e) = loader.load_from_pattern(wafbench_dir) {
939            TerminalReporter::print_warning(&format!("Failed to load WAFBench tests: {}", e));
940            return Vec::new();
941        }
942
943        let stats = loader.stats();
944
945        if stats.files_processed == 0 {
946            TerminalReporter::print_warning(&format!(
947                "No WAFBench YAML files found matching '{}'",
948                wafbench_dir
949            ));
950            // Also report any parse errors that may explain why no files were processed
951            if !stats.parse_errors.is_empty() {
952                TerminalReporter::print_warning("Some files were found but failed to parse:");
953                for error in &stats.parse_errors {
954                    TerminalReporter::print_warning(&format!("  - {}", error));
955                }
956            }
957            return Vec::new();
958        }
959
960        TerminalReporter::print_progress(&format!(
961            "Loaded {} WAFBench files, {} test cases, {} payloads",
962            stats.files_processed, stats.test_cases_loaded, stats.payloads_extracted
963        ));
964
965        // Print category breakdown
966        for (category, count) in &stats.by_category {
967            TerminalReporter::print_progress(&format!("  - {}: {} tests", category, count));
968        }
969
970        // Report any parse errors
971        for error in &stats.parse_errors {
972            TerminalReporter::print_warning(&format!("  Parse error: {}", error));
973        }
974
975        loader.to_security_payloads()
976    }
977
978    /// Generate enhanced k6 script with advanced features
979    pub(crate) fn generate_enhanced_script(&self, base_script: &str) -> Result<String> {
980        let mut enhanced_script = base_script.to_string();
981        let mut additional_code = String::new();
982
983        // Add data-driven testing code
984        if let Some(config) = self.build_data_driven_config() {
985            TerminalReporter::print_progress("Adding data-driven testing support...");
986            additional_code.push_str(&DataDrivenGenerator::generate_setup(&config));
987            additional_code.push('\n');
988            TerminalReporter::print_success("Data-driven testing enabled");
989        }
990
991        // Add invalid data generation code
992        if let Some(config) = self.build_invalid_data_config() {
993            TerminalReporter::print_progress("Adding invalid data testing support...");
994            additional_code.push_str(&InvalidDataGenerator::generate_invalidation_logic());
995            additional_code.push('\n');
996            additional_code
997                .push_str(&InvalidDataGenerator::generate_should_invalidate(config.error_rate));
998            additional_code.push('\n');
999            additional_code
1000                .push_str(&InvalidDataGenerator::generate_type_selection(&config.error_types));
1001            additional_code.push('\n');
1002            TerminalReporter::print_success(&format!(
1003                "Invalid data testing enabled ({}% error rate)",
1004                (self.error_rate.unwrap_or(0.0) * 100.0) as u32
1005            ));
1006        }
1007
1008        // Add security testing code
1009        let security_config = self.build_security_config();
1010        let wafbench_payloads = self.load_wafbench_payloads();
1011        let security_requested = security_config.is_some() || self.wafbench_dir.is_some();
1012
1013        if security_config.is_some() || !wafbench_payloads.is_empty() {
1014            TerminalReporter::print_progress("Adding security testing support...");
1015
1016            // Combine built-in payloads with WAFBench payloads
1017            let mut payload_list: Vec<SecurityPayload> = Vec::new();
1018
1019            if let Some(ref config) = security_config {
1020                payload_list.extend(SecurityPayloads::get_payloads(config));
1021            }
1022
1023            // Add WAFBench payloads
1024            if !wafbench_payloads.is_empty() {
1025                TerminalReporter::print_progress(&format!(
1026                    "Loading {} WAFBench attack patterns...",
1027                    wafbench_payloads.len()
1028                ));
1029                payload_list.extend(wafbench_payloads);
1030            }
1031
1032            let target_fields =
1033                security_config.as_ref().map(|c| c.target_fields.clone()).unwrap_or_default();
1034
1035            additional_code.push_str(&SecurityTestGenerator::generate_payload_selection(
1036                &payload_list,
1037                self.wafbench_cycle_all,
1038            ));
1039            additional_code.push('\n');
1040            additional_code
1041                .push_str(&SecurityTestGenerator::generate_apply_payload(&target_fields));
1042            additional_code.push('\n');
1043            additional_code.push_str(&SecurityTestGenerator::generate_security_checks());
1044            additional_code.push('\n');
1045
1046            let mode = if self.wafbench_cycle_all {
1047                "cycle-all"
1048            } else {
1049                "random"
1050            };
1051            TerminalReporter::print_success(&format!(
1052                "Security testing enabled ({} payloads, {} mode)",
1053                payload_list.len(),
1054                mode
1055            ));
1056        } else if security_requested {
1057            // User requested security testing (e.g., --wafbench-dir) but no payloads were loaded.
1058            // The template has security_testing_enabled=true so it renders calling code.
1059            // We must inject stub definitions to avoid undefined function references.
1060            TerminalReporter::print_warning(
1061                "Security testing was requested but no payloads were loaded. \
1062                 Ensure --wafbench-dir points to valid CRS YAML files or add --security-test.",
1063            );
1064            additional_code
1065                .push_str(&SecurityTestGenerator::generate_payload_selection(&[], false));
1066            additional_code.push('\n');
1067            additional_code.push_str(&SecurityTestGenerator::generate_apply_payload(&[]));
1068            additional_code.push('\n');
1069        }
1070
1071        // Add parallel execution code
1072        if let Some(config) = self.build_parallel_config() {
1073            TerminalReporter::print_progress("Adding parallel execution support...");
1074            additional_code.push_str(&ParallelRequestGenerator::generate_batch_helper(&config));
1075            additional_code.push('\n');
1076            TerminalReporter::print_success(&format!(
1077                "Parallel execution enabled (count: {})",
1078                config.count
1079            ));
1080        }
1081
1082        // Insert additional code after the imports section
1083        if !additional_code.is_empty() {
1084            // Find the end of the import section
1085            if let Some(import_end) = enhanced_script.find("export const options") {
1086                enhanced_script.insert_str(
1087                    import_end,
1088                    &format!("\n// === Advanced Testing Features ===\n{}\n", additional_code),
1089                );
1090            }
1091        }
1092
1093        Ok(enhanced_script)
1094    }
1095
1096    /// Execute specs sequentially with dependency ordering and value passing
1097    async fn execute_sequential_specs(&self) -> Result<()> {
1098        TerminalReporter::print_progress("Sequential spec mode: Loading specs individually...");
1099
1100        // Load all specs (without merging)
1101        let mut all_specs: Vec<(PathBuf, OpenApiSpec)> = Vec::new();
1102
1103        if !self.spec.is_empty() {
1104            let specs = load_specs_from_files(self.spec.clone())
1105                .await
1106                .map_err(|e| BenchError::Other(format!("Failed to load spec files: {}", e)))?;
1107            all_specs.extend(specs);
1108        }
1109
1110        if let Some(spec_dir) = &self.spec_dir {
1111            let dir_specs = load_specs_from_directory(spec_dir).await.map_err(|e| {
1112                BenchError::Other(format!("Failed to load specs from directory: {}", e))
1113            })?;
1114            all_specs.extend(dir_specs);
1115        }
1116
1117        if all_specs.is_empty() {
1118            return Err(BenchError::Other(
1119                "No spec files found for sequential execution".to_string(),
1120            ));
1121        }
1122
1123        TerminalReporter::print_success(&format!("Loaded {} spec(s)", all_specs.len()));
1124
1125        // Load dependency config or auto-detect
1126        let execution_order = if let Some(config_path) = &self.dependency_config {
1127            TerminalReporter::print_progress("Loading dependency configuration...");
1128            let config = SpecDependencyConfig::from_file(config_path)?;
1129
1130            if !config.disable_auto_detect && config.execution_order.is_empty() {
1131                // Auto-detect if config doesn't specify order
1132                self.detect_and_sort_specs(&all_specs)?
1133            } else {
1134                // Use configured order
1135                config.execution_order.iter().flat_map(|g| g.specs.clone()).collect()
1136            }
1137        } else {
1138            // Auto-detect dependencies
1139            self.detect_and_sort_specs(&all_specs)?
1140        };
1141
1142        TerminalReporter::print_success(&format!(
1143            "Execution order: {}",
1144            execution_order
1145                .iter()
1146                .map(|p| p.file_name().unwrap_or_default().to_string_lossy().to_string())
1147                .collect::<Vec<_>>()
1148                .join(" → ")
1149        ));
1150
1151        // Execute each spec in order
1152        let mut extracted_values = ExtractedValues::new();
1153        let total_specs = execution_order.len();
1154
1155        for (index, spec_path) in execution_order.iter().enumerate() {
1156            let spec_name = spec_path.file_name().unwrap_or_default().to_string_lossy().to_string();
1157
1158            TerminalReporter::print_progress(&format!(
1159                "[{}/{}] Executing spec: {}",
1160                index + 1,
1161                total_specs,
1162                spec_name
1163            ));
1164
1165            // Find the spec in our loaded specs (match by full path or filename)
1166            let spec = all_specs
1167                .iter()
1168                .find(|(p, _)| {
1169                    p == spec_path
1170                        || p.file_name() == spec_path.file_name()
1171                        || p.file_name() == Some(spec_path.as_os_str())
1172                })
1173                .map(|(_, s)| s.clone())
1174                .ok_or_else(|| {
1175                    BenchError::Other(format!("Spec not found: {}", spec_path.display()))
1176                })?;
1177
1178            // Execute this spec with any extracted values from previous specs
1179            let new_values = self.execute_single_spec(&spec, &spec_name, &extracted_values).await?;
1180
1181            // Merge extracted values for the next spec
1182            extracted_values.merge(&new_values);
1183
1184            TerminalReporter::print_success(&format!(
1185                "[{}/{}] Completed: {} (extracted {} values)",
1186                index + 1,
1187                total_specs,
1188                spec_name,
1189                new_values.values.len()
1190            ));
1191        }
1192
1193        TerminalReporter::print_success(&format!(
1194            "Sequential execution complete: {} specs executed",
1195            total_specs
1196        ));
1197
1198        Ok(())
1199    }
1200
1201    /// Detect dependencies and return topologically sorted spec paths
1202    fn detect_and_sort_specs(&self, specs: &[(PathBuf, OpenApiSpec)]) -> Result<Vec<PathBuf>> {
1203        TerminalReporter::print_progress("Auto-detecting spec dependencies...");
1204
1205        let mut detector = DependencyDetector::new();
1206        let dependencies = detector.detect_dependencies(specs);
1207
1208        if dependencies.is_empty() {
1209            TerminalReporter::print_progress("No dependencies detected, using file order");
1210            return Ok(specs.iter().map(|(p, _)| p.clone()).collect());
1211        }
1212
1213        TerminalReporter::print_progress(&format!(
1214            "Detected {} cross-spec dependencies",
1215            dependencies.len()
1216        ));
1217
1218        for dep in &dependencies {
1219            TerminalReporter::print_progress(&format!(
1220                "  {} → {} (via field '{}')",
1221                dep.dependency_spec.file_name().unwrap_or_default().to_string_lossy(),
1222                dep.dependent_spec.file_name().unwrap_or_default().to_string_lossy(),
1223                dep.field_name
1224            ));
1225        }
1226
1227        topological_sort(specs, &dependencies)
1228    }
1229
1230    /// Execute a single spec and extract values for dependent specs
1231    async fn execute_single_spec(
1232        &self,
1233        spec: &OpenApiSpec,
1234        spec_name: &str,
1235        _external_values: &ExtractedValues,
1236    ) -> Result<ExtractedValues> {
1237        let parser = SpecParser::from_spec(spec.clone());
1238
1239        // For now, we execute in CRUD flow mode if enabled, otherwise standard mode
1240        if self.crud_flow {
1241            // Execute CRUD flow and extract values
1242            self.execute_crud_flow_with_extraction(&parser, spec_name).await
1243        } else {
1244            // Execute standard benchmark (no value extraction in non-CRUD mode)
1245            self.execute_standard_spec(&parser, spec_name).await?;
1246            Ok(ExtractedValues::new())
1247        }
1248    }
1249
1250    /// Execute CRUD flow with value extraction for sequential mode
1251    async fn execute_crud_flow_with_extraction(
1252        &self,
1253        parser: &SpecParser,
1254        spec_name: &str,
1255    ) -> Result<ExtractedValues> {
1256        let operations = parser.get_operations();
1257        let flows = CrudFlowDetector::detect_flows(&operations);
1258
1259        if flows.is_empty() {
1260            TerminalReporter::print_warning(&format!("No CRUD flows detected in {}", spec_name));
1261            return Ok(ExtractedValues::new());
1262        }
1263
1264        TerminalReporter::print_progress(&format!(
1265            "  {} CRUD flow(s) in {}",
1266            flows.len(),
1267            spec_name
1268        ));
1269
1270        // Generate and execute the CRUD flow script
1271        let mut handlebars = handlebars::Handlebars::new();
1272        // Register json helper for serializing arrays/objects in templates
1273        handlebars.register_helper(
1274            "json",
1275            Box::new(
1276                |h: &handlebars::Helper,
1277                 _: &handlebars::Handlebars,
1278                 _: &handlebars::Context,
1279                 _: &mut handlebars::RenderContext,
1280                 out: &mut dyn handlebars::Output|
1281                 -> handlebars::HelperResult {
1282                    let param = h.param(0).map(|v| v.value()).unwrap_or(&serde_json::Value::Null);
1283                    out.write(&serde_json::to_string(param).unwrap_or_else(|_| "[]".to_string()))?;
1284                    Ok(())
1285                },
1286            ),
1287        );
1288        let template = include_str!("templates/k6_crud_flow.hbs");
1289        let output_dir = self.output.join(format!("{}_results", spec_name.replace('.', "_")));
1290
1291        let custom_headers = self.parse_headers()?;
1292        let config = self.build_crud_flow_config().unwrap_or_default();
1293
1294        // Load parameter overrides if provided (for body configurations)
1295        let param_overrides = if let Some(params_file) = &self.params_file {
1296            let overrides = ParameterOverrides::from_file(params_file)?;
1297            Some(overrides)
1298        } else {
1299            None
1300        };
1301
1302        // Generate stages from scenario
1303        let duration_secs = Self::parse_duration(&self.duration)?;
1304        let scenario =
1305            LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1306        let stages = scenario.generate_stages(duration_secs, self.vus);
1307
1308        // Resolve base path (CLI option takes priority over spec's servers URL)
1309        let api_base_path = self.resolve_base_path(parser);
1310
1311        // Build headers JSON string for the template
1312        let mut all_headers = custom_headers.clone();
1313        if let Some(auth) = &self.auth {
1314            all_headers.insert("Authorization".to_string(), auth.clone());
1315        }
1316        let headers_json = serde_json::to_string(&all_headers).unwrap_or_else(|_| "{}".to_string());
1317
1318        // Track all dynamic placeholders across all operations
1319        let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
1320
1321        let flows_data: Vec<serde_json::Value> = flows.iter().map(|f| {
1322            let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
1323            serde_json::json!({
1324                "name": sanitized_name.clone(),
1325                "display_name": f.name,
1326                "base_path": f.base_path,
1327                "steps": f.steps.iter().enumerate().map(|(idx, s)| {
1328                    // Parse operation to get method and path
1329                    let parts: Vec<&str> = s.operation.splitn(2, ' ').collect();
1330                    let method_raw = if !parts.is_empty() {
1331                        parts[0].to_uppercase()
1332                    } else {
1333                        "GET".to_string()
1334                    };
1335                    let method = if !parts.is_empty() {
1336                        let m = parts[0].to_lowercase();
1337                        // k6 uses 'del' for DELETE
1338                        if m == "delete" { "del".to_string() } else { m }
1339                    } else {
1340                        "get".to_string()
1341                    };
1342                    let raw_path = if parts.len() >= 2 { parts[1] } else { "/" };
1343                    // Prepend API base path if configured
1344                    let path = if let Some(ref bp) = api_base_path {
1345                        format!("{}{}", bp, raw_path)
1346                    } else {
1347                        raw_path.to_string()
1348                    };
1349                    let is_get_or_head = method == "get" || method == "head";
1350                    // POST, PUT, PATCH typically have bodies
1351                    let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1352
1353                    // Look up body from params file if available
1354                    let body_value = if has_body {
1355                        param_overrides.as_ref()
1356                            .map(|po| po.get_for_operation(None, &method_raw, raw_path))
1357                            .and_then(|oo| oo.body)
1358                            .unwrap_or_else(|| serde_json::json!({}))
1359                    } else {
1360                        serde_json::json!({})
1361                    };
1362
1363                    // Process body for dynamic placeholders like ${__VU}, ${__ITER}, etc.
1364                    let processed_body = DynamicParamProcessor::process_json_body(&body_value);
1365
1366                    // Also check for ${extracted.xxx} placeholders which need runtime substitution
1367                    let body_has_extracted_placeholders = processed_body.value.contains("${extracted.");
1368                    let body_is_dynamic = processed_body.is_dynamic || body_has_extracted_placeholders;
1369
1370                    serde_json::json!({
1371                        "operation": s.operation,
1372                        "method": method,
1373                        "path": path,
1374                        "extract": s.extract,
1375                        "use_values": s.use_values,
1376                        "use_body": s.use_body,
1377                        "merge_body": if s.merge_body.is_empty() { None } else { Some(&s.merge_body) },
1378                        "inject_attacks": s.inject_attacks,
1379                        "attack_types": s.attack_types,
1380                        "description": s.description,
1381                        "display_name": s.description.clone().unwrap_or_else(|| format!("Step {}", idx)),
1382                        "is_get_or_head": is_get_or_head,
1383                        "has_body": has_body,
1384                        "body": processed_body.value,
1385                        "body_is_dynamic": body_is_dynamic,
1386                        "_placeholders": processed_body.placeholders.iter().map(|p| format!("{:?}", p)).collect::<Vec<_>>(),
1387                    })
1388                }).collect::<Vec<_>>(),
1389            })
1390        }).collect();
1391
1392        // Collect all placeholders from all steps
1393        for flow_data in &flows_data {
1394            if let Some(steps) = flow_data.get("steps").and_then(|s| s.as_array()) {
1395                for step in steps {
1396                    if let Some(placeholders_arr) =
1397                        step.get("_placeholders").and_then(|p| p.as_array())
1398                    {
1399                        for p_str in placeholders_arr {
1400                            if let Some(p_name) = p_str.as_str() {
1401                                match p_name {
1402                                    "VU" => {
1403                                        all_placeholders.insert(DynamicPlaceholder::VU);
1404                                    }
1405                                    "Iteration" => {
1406                                        all_placeholders.insert(DynamicPlaceholder::Iteration);
1407                                    }
1408                                    "Timestamp" => {
1409                                        all_placeholders.insert(DynamicPlaceholder::Timestamp);
1410                                    }
1411                                    "UUID" => {
1412                                        all_placeholders.insert(DynamicPlaceholder::UUID);
1413                                    }
1414                                    "Random" => {
1415                                        all_placeholders.insert(DynamicPlaceholder::Random);
1416                                    }
1417                                    "Counter" => {
1418                                        all_placeholders.insert(DynamicPlaceholder::Counter);
1419                                    }
1420                                    "Date" => {
1421                                        all_placeholders.insert(DynamicPlaceholder::Date);
1422                                    }
1423                                    "VuIter" => {
1424                                        all_placeholders.insert(DynamicPlaceholder::VuIter);
1425                                    }
1426                                    _ => {}
1427                                }
1428                            }
1429                        }
1430                    }
1431                }
1432            }
1433        }
1434
1435        // Get required imports and globals based on placeholders used
1436        let required_imports = DynamicParamProcessor::get_required_imports(&all_placeholders);
1437        let required_globals = DynamicParamProcessor::get_required_globals(&all_placeholders);
1438
1439        // Check if security testing is enabled
1440        let security_testing_enabled = self.wafbench_dir.is_some() || self.security_test;
1441
1442        let data = serde_json::json!({
1443            "base_url": self.target,
1444            "flows": flows_data,
1445            "extract_fields": config.default_extract_fields,
1446            "duration_secs": duration_secs,
1447            "max_vus": self.vus,
1448            "auth_header": self.auth,
1449            "custom_headers": custom_headers,
1450            "skip_tls_verify": self.skip_tls_verify,
1451            // Add missing template fields
1452            "stages": stages.iter().map(|s| serde_json::json!({
1453                "duration": s.duration,
1454                "target": s.target,
1455            })).collect::<Vec<_>>(),
1456            "threshold_percentile": self.threshold_percentile,
1457            "threshold_ms": self.threshold_ms,
1458            "max_error_rate": self.max_error_rate,
1459            "headers": headers_json,
1460            "dynamic_imports": required_imports,
1461            "dynamic_globals": required_globals,
1462            "extracted_values_output_path": output_dir.join("extracted_values.json").to_string_lossy(),
1463            // Security testing settings
1464            "security_testing_enabled": security_testing_enabled,
1465            "has_custom_headers": !custom_headers.is_empty(),
1466        });
1467
1468        let mut script = handlebars
1469            .render_template(template, &data)
1470            .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
1471
1472        // Enhance script with security testing support if enabled
1473        if security_testing_enabled {
1474            script = self.generate_enhanced_script(&script)?;
1475        }
1476
1477        // Write and execute script
1478        let script_path =
1479            self.output.join(format!("k6-{}-crud-flow.js", spec_name.replace('.', "_")));
1480
1481        std::fs::create_dir_all(self.output.clone())?;
1482        std::fs::write(&script_path, &script)?;
1483
1484        if !self.generate_only {
1485            let executor = K6Executor::new()?;
1486            std::fs::create_dir_all(&output_dir)?;
1487
1488            executor.execute(&script_path, Some(&output_dir), self.verbose).await?;
1489
1490            let extracted = Self::parse_extracted_values(&output_dir)?;
1491            TerminalReporter::print_progress(&format!(
1492                "  Extracted {} value(s) from {}",
1493                extracted.values.len(),
1494                spec_name
1495            ));
1496            return Ok(extracted);
1497        }
1498
1499        Ok(ExtractedValues::new())
1500    }
1501
1502    /// Execute standard (non-CRUD) spec benchmark
1503    async fn execute_standard_spec(&self, parser: &SpecParser, spec_name: &str) -> Result<()> {
1504        let mut operations = if let Some(filter) = &self.operations {
1505            parser.filter_operations(filter)?
1506        } else {
1507            parser.get_operations()
1508        };
1509
1510        if let Some(exclude) = &self.exclude_operations {
1511            operations = parser.exclude_operations(operations, exclude)?;
1512        }
1513
1514        if operations.is_empty() {
1515            TerminalReporter::print_warning(&format!("No operations found in {}", spec_name));
1516            return Ok(());
1517        }
1518
1519        TerminalReporter::print_progress(&format!(
1520            "  {} operations in {}",
1521            operations.len(),
1522            spec_name
1523        ));
1524
1525        // Generate request templates
1526        let templates: Vec<_> = operations
1527            .iter()
1528            .map(RequestGenerator::generate_template)
1529            .collect::<Result<Vec<_>>>()?;
1530
1531        // Parse headers
1532        let custom_headers = self.parse_headers()?;
1533
1534        // Resolve base path
1535        let base_path = self.resolve_base_path(parser);
1536
1537        // Generate k6 script
1538        let scenario =
1539            LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1540
1541        let security_testing_enabled = self.security_test || self.wafbench_dir.is_some();
1542
1543        let k6_config = K6Config {
1544            target_url: self.target.clone(),
1545            base_path,
1546            scenario,
1547            duration_secs: Self::parse_duration(&self.duration)?,
1548            max_vus: self.vus,
1549            threshold_percentile: self.threshold_percentile.clone(),
1550            threshold_ms: self.threshold_ms,
1551            max_error_rate: self.max_error_rate,
1552            auth_header: self.auth.clone(),
1553            custom_headers,
1554            skip_tls_verify: self.skip_tls_verify,
1555            security_testing_enabled,
1556        };
1557
1558        let generator = K6ScriptGenerator::new(k6_config, templates);
1559        let mut script = generator.generate()?;
1560
1561        // Enhance script with advanced features (security testing, etc.)
1562        let has_advanced_features = self.data_file.is_some()
1563            || self.error_rate.is_some()
1564            || self.security_test
1565            || self.parallel_create.is_some()
1566            || self.wafbench_dir.is_some();
1567
1568        if has_advanced_features {
1569            script = self.generate_enhanced_script(&script)?;
1570        }
1571
1572        // Write and execute script
1573        let script_path = self.output.join(format!("k6-{}.js", spec_name.replace('.', "_")));
1574
1575        std::fs::create_dir_all(self.output.clone())?;
1576        std::fs::write(&script_path, &script)?;
1577
1578        if !self.generate_only {
1579            let executor = K6Executor::new()?;
1580            let output_dir = self.output.join(format!("{}_results", spec_name.replace('.', "_")));
1581            std::fs::create_dir_all(&output_dir)?;
1582
1583            executor.execute(&script_path, Some(&output_dir), self.verbose).await?;
1584        }
1585
1586        Ok(())
1587    }
1588
1589    /// Execute CRUD flow testing mode
1590    async fn execute_crud_flow(&self, parser: &SpecParser) -> Result<()> {
1591        // Check if a custom flow config is provided
1592        let config = self.build_crud_flow_config().unwrap_or_default();
1593
1594        // Use flows from config if provided, otherwise auto-detect
1595        let flows = if !config.flows.is_empty() {
1596            TerminalReporter::print_progress("Using custom flow configuration...");
1597            config.flows.clone()
1598        } else {
1599            TerminalReporter::print_progress("Detecting CRUD operations...");
1600            let operations = parser.get_operations();
1601            CrudFlowDetector::detect_flows(&operations)
1602        };
1603
1604        if flows.is_empty() {
1605            return Err(BenchError::Other(
1606                "No CRUD flows detected in spec. Ensure spec has POST/GET/PUT/DELETE operations on related paths.".to_string(),
1607            ));
1608        }
1609
1610        if config.flows.is_empty() {
1611            TerminalReporter::print_success(&format!("Detected {} CRUD flow(s)", flows.len()));
1612        } else {
1613            TerminalReporter::print_success(&format!("Loaded {} custom flow(s)", flows.len()));
1614        }
1615
1616        for flow in &flows {
1617            TerminalReporter::print_progress(&format!(
1618                "  - {}: {} steps",
1619                flow.name,
1620                flow.steps.len()
1621            ));
1622        }
1623
1624        // Generate CRUD flow script
1625        let mut handlebars = handlebars::Handlebars::new();
1626        // Register json helper for serializing arrays/objects in templates
1627        handlebars.register_helper(
1628            "json",
1629            Box::new(
1630                |h: &handlebars::Helper,
1631                 _: &handlebars::Handlebars,
1632                 _: &handlebars::Context,
1633                 _: &mut handlebars::RenderContext,
1634                 out: &mut dyn handlebars::Output|
1635                 -> handlebars::HelperResult {
1636                    let param = h.param(0).map(|v| v.value()).unwrap_or(&serde_json::Value::Null);
1637                    out.write(&serde_json::to_string(param).unwrap_or_else(|_| "[]".to_string()))?;
1638                    Ok(())
1639                },
1640            ),
1641        );
1642        let template = include_str!("templates/k6_crud_flow.hbs");
1643
1644        let custom_headers = self.parse_headers()?;
1645
1646        // Load parameter overrides if provided (for body configurations)
1647        let param_overrides = if let Some(params_file) = &self.params_file {
1648            TerminalReporter::print_progress("Loading parameter overrides...");
1649            let overrides = ParameterOverrides::from_file(params_file)?;
1650            TerminalReporter::print_success(&format!(
1651                "Loaded parameter overrides ({} operation-specific, {} defaults)",
1652                overrides.operations.len(),
1653                if overrides.defaults.is_empty() { 0 } else { 1 }
1654            ));
1655            Some(overrides)
1656        } else {
1657            None
1658        };
1659
1660        // Generate stages from scenario
1661        let duration_secs = Self::parse_duration(&self.duration)?;
1662        let scenario =
1663            LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1664        let stages = scenario.generate_stages(duration_secs, self.vus);
1665
1666        // Resolve base path (CLI option takes priority over spec's servers URL)
1667        let api_base_path = self.resolve_base_path(parser);
1668        if let Some(ref bp) = api_base_path {
1669            TerminalReporter::print_progress(&format!("Using base path: {}", bp));
1670        }
1671
1672        // Build headers JSON string for the template
1673        let mut all_headers = custom_headers.clone();
1674        if let Some(auth) = &self.auth {
1675            all_headers.insert("Authorization".to_string(), auth.clone());
1676        }
1677        let headers_json = serde_json::to_string(&all_headers).unwrap_or_else(|_| "{}".to_string());
1678
1679        // Track all dynamic placeholders across all operations
1680        let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
1681
1682        let flows_data: Vec<serde_json::Value> = flows.iter().map(|f| {
1683            // Sanitize flow name for use as JavaScript variable and k6 metric names
1684            let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
1685            serde_json::json!({
1686                "name": sanitized_name.clone(),  // Use sanitized name for variable names
1687                "display_name": f.name,          // Keep original for comments/display
1688                "base_path": f.base_path,
1689                "steps": f.steps.iter().enumerate().map(|(idx, s)| {
1690                    // Parse operation to get method and path
1691                    let parts: Vec<&str> = s.operation.splitn(2, ' ').collect();
1692                    let method_raw = if !parts.is_empty() {
1693                        parts[0].to_uppercase()
1694                    } else {
1695                        "GET".to_string()
1696                    };
1697                    let method = if !parts.is_empty() {
1698                        let m = parts[0].to_lowercase();
1699                        // k6 uses 'del' for DELETE
1700                        if m == "delete" { "del".to_string() } else { m }
1701                    } else {
1702                        "get".to_string()
1703                    };
1704                    let raw_path = if parts.len() >= 2 { parts[1] } else { "/" };
1705                    // Prepend API base path if configured
1706                    let path = if let Some(ref bp) = api_base_path {
1707                        format!("{}{}", bp, raw_path)
1708                    } else {
1709                        raw_path.to_string()
1710                    };
1711                    let is_get_or_head = method == "get" || method == "head";
1712                    // POST, PUT, PATCH typically have bodies
1713                    let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1714
1715                    // Look up body from params file if available (use raw_path for matching)
1716                    let body_value = if has_body {
1717                        param_overrides.as_ref()
1718                            .map(|po| po.get_for_operation(None, &method_raw, raw_path))
1719                            .and_then(|oo| oo.body)
1720                            .unwrap_or_else(|| serde_json::json!({}))
1721                    } else {
1722                        serde_json::json!({})
1723                    };
1724
1725                    // Process body for dynamic placeholders like ${__VU}, ${__ITER}, etc.
1726                    let processed_body = DynamicParamProcessor::process_json_body(&body_value);
1727                    // Note: all_placeholders is captured by the closure but we can't mutate it directly
1728                    // We'll collect placeholders separately below
1729
1730                    // Also check for ${extracted.xxx} placeholders which need runtime substitution
1731                    let body_has_extracted_placeholders = processed_body.value.contains("${extracted.");
1732                    let body_is_dynamic = processed_body.is_dynamic || body_has_extracted_placeholders;
1733
1734                    serde_json::json!({
1735                        "operation": s.operation,
1736                        "method": method,
1737                        "path": path,
1738                        "extract": s.extract,
1739                        "use_values": s.use_values,
1740                        "use_body": s.use_body,
1741                        "merge_body": if s.merge_body.is_empty() { None } else { Some(&s.merge_body) },
1742                        "inject_attacks": s.inject_attacks,
1743                        "attack_types": s.attack_types,
1744                        "description": s.description,
1745                        "display_name": s.description.clone().unwrap_or_else(|| format!("Step {}", idx)),
1746                        "is_get_or_head": is_get_or_head,
1747                        "has_body": has_body,
1748                        "body": processed_body.value,
1749                        "body_is_dynamic": body_is_dynamic,
1750                        "_placeholders": processed_body.placeholders.iter().map(|p| format!("{:?}", p)).collect::<Vec<_>>(),
1751                    })
1752                }).collect::<Vec<_>>(),
1753            })
1754        }).collect();
1755
1756        // Collect all placeholders from all steps
1757        for flow_data in &flows_data {
1758            if let Some(steps) = flow_data.get("steps").and_then(|s| s.as_array()) {
1759                for step in steps {
1760                    if let Some(placeholders_arr) =
1761                        step.get("_placeholders").and_then(|p| p.as_array())
1762                    {
1763                        for p_str in placeholders_arr {
1764                            if let Some(p_name) = p_str.as_str() {
1765                                // Parse placeholder from debug string
1766                                match p_name {
1767                                    "VU" => {
1768                                        all_placeholders.insert(DynamicPlaceholder::VU);
1769                                    }
1770                                    "Iteration" => {
1771                                        all_placeholders.insert(DynamicPlaceholder::Iteration);
1772                                    }
1773                                    "Timestamp" => {
1774                                        all_placeholders.insert(DynamicPlaceholder::Timestamp);
1775                                    }
1776                                    "UUID" => {
1777                                        all_placeholders.insert(DynamicPlaceholder::UUID);
1778                                    }
1779                                    "Random" => {
1780                                        all_placeholders.insert(DynamicPlaceholder::Random);
1781                                    }
1782                                    "Counter" => {
1783                                        all_placeholders.insert(DynamicPlaceholder::Counter);
1784                                    }
1785                                    "Date" => {
1786                                        all_placeholders.insert(DynamicPlaceholder::Date);
1787                                    }
1788                                    "VuIter" => {
1789                                        all_placeholders.insert(DynamicPlaceholder::VuIter);
1790                                    }
1791                                    _ => {}
1792                                }
1793                            }
1794                        }
1795                    }
1796                }
1797            }
1798        }
1799
1800        // Get required imports and globals based on placeholders used
1801        let required_imports = DynamicParamProcessor::get_required_imports(&all_placeholders);
1802        let required_globals = DynamicParamProcessor::get_required_globals(&all_placeholders);
1803
1804        // Build invalid data config if error injection is enabled
1805        let invalid_data_config = self.build_invalid_data_config();
1806        let error_injection_enabled = invalid_data_config.is_some();
1807        let error_rate = self.error_rate.unwrap_or(0.0);
1808        let error_types: Vec<String> = invalid_data_config
1809            .as_ref()
1810            .map(|c| c.error_types.iter().map(|t| format!("{:?}", t)).collect())
1811            .unwrap_or_default();
1812
1813        if error_injection_enabled {
1814            TerminalReporter::print_progress(&format!(
1815                "Error injection enabled ({}% rate)",
1816                (error_rate * 100.0) as u32
1817            ));
1818        }
1819
1820        // Check if security testing is enabled
1821        let security_testing_enabled = self.wafbench_dir.is_some() || self.security_test;
1822
1823        let data = serde_json::json!({
1824            "base_url": self.target,
1825            "flows": flows_data,
1826            "extract_fields": config.default_extract_fields,
1827            "duration_secs": duration_secs,
1828            "max_vus": self.vus,
1829            "auth_header": self.auth,
1830            "custom_headers": custom_headers,
1831            "skip_tls_verify": self.skip_tls_verify,
1832            // Add missing template fields
1833            "stages": stages.iter().map(|s| serde_json::json!({
1834                "duration": s.duration,
1835                "target": s.target,
1836            })).collect::<Vec<_>>(),
1837            "threshold_percentile": self.threshold_percentile,
1838            "threshold_ms": self.threshold_ms,
1839            "max_error_rate": self.max_error_rate,
1840            "headers": headers_json,
1841            "dynamic_imports": required_imports,
1842            "dynamic_globals": required_globals,
1843            "extracted_values_output_path": self
1844                .output
1845                .join("crud_flow_extracted_values.json")
1846                .to_string_lossy(),
1847            // Error injection settings
1848            "error_injection_enabled": error_injection_enabled,
1849            "error_rate": error_rate,
1850            "error_types": error_types,
1851            // Security testing settings
1852            "security_testing_enabled": security_testing_enabled,
1853            "has_custom_headers": !custom_headers.is_empty(),
1854        });
1855
1856        let mut script = handlebars
1857            .render_template(template, &data)
1858            .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
1859
1860        // Enhance script with security testing support if enabled
1861        if security_testing_enabled {
1862            script = self.generate_enhanced_script(&script)?;
1863        }
1864
1865        // Validate the generated CRUD flow script
1866        TerminalReporter::print_progress("Validating CRUD flow script...");
1867        let validation_errors = K6ScriptGenerator::validate_script(&script);
1868        if !validation_errors.is_empty() {
1869            TerminalReporter::print_error("CRUD flow script validation failed");
1870            for error in &validation_errors {
1871                eprintln!("  {}", error);
1872            }
1873            return Err(BenchError::Other(format!(
1874                "CRUD flow script validation failed with {} error(s)",
1875                validation_errors.len()
1876            )));
1877        }
1878
1879        TerminalReporter::print_success("CRUD flow script generated");
1880
1881        // Write and execute script
1882        let script_path = if let Some(output) = &self.script_output {
1883            output.clone()
1884        } else {
1885            self.output.join("k6-crud-flow-script.js")
1886        };
1887
1888        if let Some(parent) = script_path.parent() {
1889            std::fs::create_dir_all(parent)?;
1890        }
1891        std::fs::write(&script_path, &script)?;
1892        TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
1893
1894        if self.generate_only {
1895            println!("\nScript generated successfully. Run it with:");
1896            println!("  k6 run {}", script_path.display());
1897            return Ok(());
1898        }
1899
1900        // Execute k6
1901        TerminalReporter::print_progress("Executing CRUD flow test...");
1902        let executor = K6Executor::new()?;
1903        std::fs::create_dir_all(&self.output)?;
1904
1905        let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
1906
1907        let duration_secs = Self::parse_duration(&self.duration)?;
1908        TerminalReporter::print_summary(&results, duration_secs);
1909
1910        Ok(())
1911    }
1912
1913    /// Execute OpenAPI 3.0.0 conformance testing mode
1914    async fn execute_conformance_test(&self) -> Result<()> {
1915        use crate::conformance::generator::{ConformanceConfig, ConformanceGenerator};
1916        use crate::conformance::report::ConformanceReport;
1917        use crate::conformance::spec::ConformanceFeature;
1918
1919        TerminalReporter::print_progress("OpenAPI 3.0.0 Conformance Testing Mode");
1920
1921        // Conformance testing is a functional correctness check (1 VU, 1 iteration).
1922        // --vus and -d flags are always ignored in this mode.
1923        TerminalReporter::print_progress(
1924            "Conformance mode runs 1 VU, 1 iteration per endpoint (--vus and -d are ignored)",
1925        );
1926
1927        // Parse category filter
1928        let categories = self.conformance_categories.as_ref().map(|cats_str| {
1929            cats_str
1930                .split(',')
1931                .filter_map(|s| {
1932                    let trimmed = s.trim();
1933                    if let Some(canonical) = ConformanceFeature::category_from_cli_name(trimmed) {
1934                        Some(canonical.to_string())
1935                    } else {
1936                        TerminalReporter::print_warning(&format!(
1937                            "Unknown conformance category: '{}'. Valid categories: {}",
1938                            trimmed,
1939                            ConformanceFeature::cli_category_names()
1940                                .iter()
1941                                .map(|(cli, _)| *cli)
1942                                .collect::<Vec<_>>()
1943                                .join(", ")
1944                        ));
1945                        None
1946                    }
1947                })
1948                .collect::<Vec<String>>()
1949        });
1950
1951        // Parse custom headers from "Key: Value" format
1952        let custom_headers: Vec<(String, String)> = self
1953            .conformance_headers
1954            .iter()
1955            .filter_map(|h| {
1956                let (name, value) = h.split_once(':')?;
1957                Some((name.trim().to_string(), value.trim().to_string()))
1958            })
1959            .collect();
1960
1961        if !custom_headers.is_empty() {
1962            TerminalReporter::print_progress(&format!(
1963                "Using {} custom header(s) for authentication",
1964                custom_headers.len()
1965            ));
1966        }
1967
1968        if self.conformance_delay_ms > 0 {
1969            TerminalReporter::print_progress(&format!(
1970                "Using {}ms delay between conformance requests",
1971                self.conformance_delay_ms
1972            ));
1973        }
1974
1975        // Ensure output dir exists so canonicalize works for the report path
1976        std::fs::create_dir_all(&self.output)?;
1977
1978        let config = ConformanceConfig {
1979            target_url: self.target.clone(),
1980            api_key: self.conformance_api_key.clone(),
1981            basic_auth: self.conformance_basic_auth.clone(),
1982            skip_tls_verify: self.skip_tls_verify,
1983            categories,
1984            base_path: self.base_path.clone(),
1985            custom_headers,
1986            output_dir: Some(self.output.clone()),
1987            all_operations: self.conformance_all_operations,
1988            custom_checks_file: self.conformance_custom.clone(),
1989            request_delay_ms: self.conformance_delay_ms,
1990            custom_filter: self.conformance_custom_filter.clone(),
1991            export_requests: self.export_requests,
1992            validate_requests: self.validate_requests,
1993        };
1994
1995        // Branch: spec-driven mode vs reference mode
1996        // Annotate operations if spec is provided (used by both native and k6 paths)
1997        let annotated_ops = if !self.spec.is_empty() {
1998            TerminalReporter::print_progress("Spec-driven conformance mode: analyzing spec...");
1999            let parser = SpecParser::from_file(&self.spec[0]).await?;
2000            let operations = parser.get_operations();
2001
2002            let annotated =
2003                crate::conformance::spec_driven::SpecDrivenConformanceGenerator::annotate_operations(
2004                    &operations,
2005                    parser.spec(),
2006                );
2007            TerminalReporter::print_success(&format!(
2008                "Analyzed {} operations, found {} feature annotations",
2009                operations.len(),
2010                annotated.iter().map(|a| a.features.len()).sum::<usize>()
2011            ));
2012            Some(annotated)
2013        } else {
2014            None
2015        };
2016
2017        // Request validation against OpenAPI spec (if --validate-requests is set)
2018        if self.validate_requests && !self.spec.is_empty() {
2019            TerminalReporter::print_progress("Validating requests against OpenAPI spec...");
2020            let violation_count = crate::conformance::request_validator::run_request_validation(
2021                &self.spec,
2022                self.conformance_custom.as_deref(),
2023                self.base_path.as_deref(),
2024                &self.output,
2025            )
2026            .await?;
2027            if violation_count > 0 {
2028                TerminalReporter::print_warning(&format!(
2029                    "{} request validation violation(s) found — see conformance-request-violations.json",
2030                    violation_count
2031                ));
2032            } else {
2033                TerminalReporter::print_success("All requests conform to the OpenAPI spec");
2034            }
2035        }
2036
2037        // If generate-only OR --use-k6, use the k6 script generation path
2038        if self.generate_only || self.use_k6 {
2039            let script = if let Some(annotated) = &annotated_ops {
2040                let gen = crate::conformance::spec_driven::SpecDrivenConformanceGenerator::new(
2041                    config,
2042                    annotated.clone(),
2043                );
2044                let op_count = gen.operation_count();
2045                let (script, check_count) = gen.generate()?;
2046                TerminalReporter::print_success(&format!(
2047                    "Conformance: {} operations analyzed, {} unique checks generated",
2048                    op_count, check_count
2049                ));
2050                script
2051            } else {
2052                let generator = ConformanceGenerator::new(config);
2053                generator.generate()?
2054            };
2055
2056            let script_path = self.output.join("k6-conformance.js");
2057            std::fs::write(&script_path, &script).map_err(|e| {
2058                BenchError::Other(format!("Failed to write conformance script: {}", e))
2059            })?;
2060            TerminalReporter::print_success(&format!(
2061                "Conformance script generated: {}",
2062                script_path.display()
2063            ));
2064
2065            if self.generate_only {
2066                println!("\nScript generated. Run with:");
2067                println!("  k6 run {}", script_path.display());
2068                return Ok(());
2069            }
2070
2071            // --use-k6: execute via k6
2072            if !K6Executor::is_k6_installed() {
2073                TerminalReporter::print_error("k6 is not installed");
2074                TerminalReporter::print_warning(
2075                    "Install k6 from: https://k6.io/docs/get-started/installation/",
2076                );
2077                return Err(BenchError::K6NotFound);
2078            }
2079
2080            TerminalReporter::print_progress("Running conformance tests via k6...");
2081            let executor = K6Executor::new()?;
2082            executor.execute(&script_path, Some(&self.output), self.verbose).await?;
2083
2084            let report_path = self.output.join("conformance-report.json");
2085            if report_path.exists() {
2086                let report = ConformanceReport::from_file(&report_path)?;
2087                report.print_report_with_options(self.conformance_all_operations);
2088                self.save_conformance_report(&report, &report_path)?;
2089            } else {
2090                TerminalReporter::print_warning(
2091                    "Conformance report not generated (k6 handleSummary may not have run)",
2092                );
2093            }
2094
2095            return Ok(());
2096        }
2097
2098        // Default: Native Rust executor (no k6 dependency)
2099        TerminalReporter::print_progress("Running conformance tests (native executor)...");
2100
2101        let mut executor = crate::conformance::executor::NativeConformanceExecutor::new(config)?;
2102
2103        executor = if let Some(annotated) = &annotated_ops {
2104            executor.with_spec_driven_checks(annotated)
2105        } else {
2106            executor.with_reference_checks()
2107        };
2108        executor = executor.with_custom_checks()?;
2109
2110        TerminalReporter::print_success(&format!(
2111            "Executing {} conformance checks...",
2112            executor.check_count()
2113        ));
2114
2115        let report = executor.execute().await?;
2116        report.print_report_with_options(self.conformance_all_operations);
2117
2118        // Save failure details to a separate file for easy debugging
2119        let failure_details = report.failure_details();
2120        if !failure_details.is_empty() {
2121            let details_path = self.output.join("conformance-failure-details.json");
2122            if let Ok(json) = serde_json::to_string_pretty(&failure_details) {
2123                let _ = std::fs::write(&details_path, json);
2124                TerminalReporter::print_success(&format!(
2125                    "Failure details saved to: {}",
2126                    details_path.display()
2127                ));
2128            }
2129        }
2130
2131        // Save report
2132        let report_path = self.output.join("conformance-report.json");
2133        let report_json = serde_json::to_string_pretty(&report.to_json())
2134            .map_err(|e| BenchError::Other(format!("Failed to serialize report: {}", e)))?;
2135        std::fs::write(&report_path, &report_json)
2136            .map_err(|e| BenchError::Other(format!("Failed to write report: {}", e)))?;
2137        TerminalReporter::print_success(&format!("Report saved to: {}", report_path.display()));
2138
2139        self.save_conformance_report(&report, &report_path)?;
2140
2141        Ok(())
2142    }
2143
2144    /// Save conformance report in the requested format (SARIF or JSON copy)
2145    fn save_conformance_report(
2146        &self,
2147        report: &crate::conformance::report::ConformanceReport,
2148        report_path: &Path,
2149    ) -> Result<()> {
2150        if self.conformance_report_format == "sarif" {
2151            use crate::conformance::sarif::ConformanceSarifReport;
2152            ConformanceSarifReport::write(report, &self.target, &self.conformance_report)?;
2153            TerminalReporter::print_success(&format!(
2154                "SARIF report saved to: {}",
2155                self.conformance_report.display()
2156            ));
2157        } else if self.conformance_report != *report_path {
2158            std::fs::copy(report_path, &self.conformance_report)?;
2159            TerminalReporter::print_success(&format!(
2160                "Report saved to: {}",
2161                self.conformance_report.display()
2162            ));
2163        }
2164        Ok(())
2165    }
2166
2167    /// Execute conformance tests against multiple targets from a targets file.
2168    ///
2169    /// Uses the native `NativeConformanceExecutor` (no k6 dependency). Targets are
2170    /// tested sequentially to avoid overwhelming them, and per-target headers from
2171    /// the targets file are merged with the base `--conformance-header` headers.
2172    async fn execute_multi_target_conformance(&self, targets_file: &Path) -> Result<()> {
2173        use crate::conformance::generator::{ConformanceConfig, ConformanceGenerator};
2174        use crate::conformance::report::ConformanceReport;
2175        use crate::conformance::spec::ConformanceFeature;
2176
2177        TerminalReporter::print_progress("Multi-target OpenAPI 3.0.0 Conformance Testing Mode");
2178
2179        // Parse targets file
2180        TerminalReporter::print_progress("Parsing targets file...");
2181        let targets = parse_targets_file(targets_file)?;
2182        let num_targets = targets.len();
2183        TerminalReporter::print_success(&format!("Loaded {} targets", num_targets));
2184
2185        if targets.is_empty() {
2186            return Err(BenchError::Other("No targets found in file".to_string()));
2187        }
2188
2189        TerminalReporter::print_progress(
2190            "Conformance mode runs 1 VU, 1 iteration per endpoint (--vus and -d are ignored)",
2191        );
2192
2193        // Parse category filter (shared across all targets)
2194        let categories = self.conformance_categories.as_ref().map(|cats_str| {
2195            cats_str
2196                .split(',')
2197                .filter_map(|s| {
2198                    let trimmed = s.trim();
2199                    if let Some(canonical) = ConformanceFeature::category_from_cli_name(trimmed) {
2200                        Some(canonical.to_string())
2201                    } else {
2202                        TerminalReporter::print_warning(&format!(
2203                            "Unknown conformance category: '{}'. Valid categories: {}",
2204                            trimmed,
2205                            ConformanceFeature::cli_category_names()
2206                                .iter()
2207                                .map(|(cli, _)| *cli)
2208                                .collect::<Vec<_>>()
2209                                .join(", ")
2210                        ));
2211                        None
2212                    }
2213                })
2214                .collect::<Vec<String>>()
2215        });
2216
2217        // Parse base custom headers from --conformance-header flags
2218        let base_custom_headers: Vec<(String, String)> = self
2219            .conformance_headers
2220            .iter()
2221            .filter_map(|h| {
2222                let (name, value) = h.split_once(':')?;
2223                Some((name.trim().to_string(), value.trim().to_string()))
2224            })
2225            .collect();
2226
2227        if !base_custom_headers.is_empty() {
2228            TerminalReporter::print_progress(&format!(
2229                "Using {} base custom header(s) for authentication",
2230                base_custom_headers.len()
2231            ));
2232        }
2233
2234        // Load spec once if provided (shared across all targets)
2235        let annotated_ops = if !self.spec.is_empty() {
2236            TerminalReporter::print_progress("Spec-driven conformance mode: analyzing spec...");
2237            let parser = SpecParser::from_file(&self.spec[0]).await?;
2238            let operations = parser.get_operations();
2239            let annotated =
2240                crate::conformance::spec_driven::SpecDrivenConformanceGenerator::annotate_operations(
2241                    &operations,
2242                    parser.spec(),
2243                );
2244            TerminalReporter::print_success(&format!(
2245                "Analyzed {} operations, found {} feature annotations",
2246                operations.len(),
2247                annotated.iter().map(|a| a.features.len()).sum::<usize>()
2248            ));
2249            Some(annotated)
2250        } else {
2251            None
2252        };
2253
2254        // Ensure output dir exists
2255        std::fs::create_dir_all(&self.output)?;
2256
2257        // Collect per-target results for the summary
2258        struct TargetResult {
2259            url: String,
2260            passed: usize,
2261            failed: usize,
2262            elapsed: std::time::Duration,
2263            report_json: serde_json::Value,
2264            owasp_coverage: Vec<crate::conformance::report::OwaspCoverageEntry>,
2265        }
2266
2267        let mut target_results: Vec<TargetResult> = Vec::with_capacity(num_targets);
2268        let total_start = std::time::Instant::now();
2269
2270        for (idx, target) in targets.iter().enumerate() {
2271            tracing::info!(
2272                "Running conformance tests against target {}/{}: {}",
2273                idx + 1,
2274                num_targets,
2275                target.url
2276            );
2277            TerminalReporter::print_progress(&format!(
2278                "\n--- Target {}/{}: {} ---",
2279                idx + 1,
2280                num_targets,
2281                target.url
2282            ));
2283
2284            // Merge base headers with per-target headers
2285            let mut merged_headers = base_custom_headers.clone();
2286            if let Some(ref target_headers) = target.headers {
2287                for (name, value) in target_headers {
2288                    // Per-target headers override base headers with the same name
2289                    if let Some(existing) = merged_headers.iter_mut().find(|(n, _)| n == name) {
2290                        existing.1 = value.clone();
2291                    } else {
2292                        merged_headers.push((name.clone(), value.clone()));
2293                    }
2294                }
2295            }
2296            // Add auth header if present on target
2297            if let Some(ref auth) = target.auth {
2298                if let Some(existing) =
2299                    merged_headers.iter_mut().find(|(n, _)| n.eq_ignore_ascii_case("Authorization"))
2300                {
2301                    existing.1 = auth.clone();
2302                } else {
2303                    merged_headers.push(("Authorization".to_string(), auth.clone()));
2304                }
2305            }
2306
2307            // Per-target output dir (used by both native and k6 paths).
2308            // Created before the config so we can point the k6 script's
2309            // handleSummary at the per-target directory rather than the shared
2310            // parent output dir (otherwise every target would overwrite the
2311            // same conformance-report.json).
2312            let target_dir = self.output.join(format!("target_{}", idx));
2313            std::fs::create_dir_all(&target_dir)?;
2314
2315            let config = ConformanceConfig {
2316                target_url: target.url.clone(),
2317                api_key: self.conformance_api_key.clone(),
2318                basic_auth: self.conformance_basic_auth.clone(),
2319                skip_tls_verify: self.skip_tls_verify,
2320                categories: categories.clone(),
2321                base_path: self.base_path.clone(),
2322                custom_headers: merged_headers,
2323                output_dir: Some(target_dir.clone()),
2324                all_operations: self.conformance_all_operations,
2325                custom_checks_file: self.conformance_custom.clone(),
2326                request_delay_ms: self.conformance_delay_ms,
2327                custom_filter: self.conformance_custom_filter.clone(),
2328                export_requests: self.export_requests,
2329                validate_requests: self.validate_requests,
2330            };
2331
2332            let target_start = std::time::Instant::now();
2333            let report = if self.use_k6 {
2334                if !K6Executor::is_k6_installed() {
2335                    TerminalReporter::print_error("k6 is not installed");
2336                    TerminalReporter::print_warning(
2337                        "Install k6 from: https://k6.io/docs/get-started/installation/",
2338                    );
2339                    return Err(BenchError::K6NotFound);
2340                }
2341
2342                let script = if let Some(ref annotated) = annotated_ops {
2343                    let gen = crate::conformance::spec_driven::SpecDrivenConformanceGenerator::new(
2344                        config.clone(),
2345                        annotated.clone(),
2346                    );
2347                    let (script, _check_count) = gen.generate()?;
2348                    script
2349                } else {
2350                    let generator = ConformanceGenerator::new(config.clone());
2351                    generator.generate()?
2352                };
2353
2354                let script_path = target_dir.join("k6-conformance.js");
2355                std::fs::write(&script_path, &script).map_err(|e| {
2356                    BenchError::Other(format!("Failed to write conformance script: {}", e))
2357                })?;
2358                TerminalReporter::print_success(&format!(
2359                    "Conformance script generated: {}",
2360                    script_path.display()
2361                ));
2362
2363                TerminalReporter::print_progress(&format!(
2364                    "Running conformance tests via k6 against {}...",
2365                    target.url
2366                ));
2367                let k6 = K6Executor::new()?;
2368                // Unique k6 API port per target to avoid collisions.
2369                let api_port = 6565u16.saturating_add(idx as u16);
2370                k6.execute_with_port(&script_path, Some(&target_dir), self.verbose, Some(api_port))
2371                    .await?;
2372
2373                let report_path = target_dir.join("conformance-report.json");
2374                if report_path.exists() {
2375                    ConformanceReport::from_file(&report_path)?
2376                } else {
2377                    TerminalReporter::print_warning(&format!(
2378                        "Conformance report not generated for target {} (k6 handleSummary may not have run)",
2379                        target.url
2380                    ));
2381                    continue;
2382                }
2383            } else {
2384                let mut executor =
2385                    crate::conformance::executor::NativeConformanceExecutor::new(config)?;
2386
2387                executor = if let Some(ref annotated) = annotated_ops {
2388                    executor.with_spec_driven_checks(annotated)
2389                } else {
2390                    executor.with_reference_checks()
2391                };
2392                executor = executor.with_custom_checks()?;
2393
2394                TerminalReporter::print_success(&format!(
2395                    "Executing {} conformance checks against {}...",
2396                    executor.check_count(),
2397                    target.url
2398                ));
2399
2400                executor.execute().await?
2401            };
2402            let target_elapsed = target_start.elapsed();
2403
2404            let report_json = report.to_json();
2405
2406            // Extract pass/fail from the summary in the JSON
2407            let passed = report_json["summary"]["passed"].as_u64().unwrap_or(0) as usize;
2408            let failed = report_json["summary"]["failed"].as_u64().unwrap_or(0) as usize;
2409            let total_checks = passed + failed;
2410            let rate = if total_checks == 0 {
2411                0.0
2412            } else {
2413                (passed as f64 / total_checks as f64) * 100.0
2414            };
2415
2416            TerminalReporter::print_success(&format!(
2417                "Target {}: {}/{} passed ({:.1}%) in {:.1}s",
2418                target.url,
2419                passed,
2420                total_checks,
2421                rate,
2422                target_elapsed.as_secs_f64()
2423            ));
2424
2425            // Save per-target report (target_dir created above)
2426            let target_report_path = target_dir.join("conformance-report.json");
2427            let report_str = serde_json::to_string_pretty(&report_json)
2428                .map_err(|e| BenchError::Other(format!("Failed to serialize report: {}", e)))?;
2429            std::fs::write(&target_report_path, &report_str)
2430                .map_err(|e| BenchError::Other(format!("Failed to write report: {}", e)))?;
2431
2432            // Save failure details if any
2433            let failure_details = report.failure_details();
2434            if !failure_details.is_empty() {
2435                let details_path = target_dir.join("conformance-failure-details.json");
2436                if let Ok(json) = serde_json::to_string_pretty(&failure_details) {
2437                    let _ = std::fs::write(&details_path, json);
2438                }
2439            }
2440
2441            // Compute OWASP coverage for this target
2442            let owasp_coverage = report.owasp_coverage_data();
2443
2444            target_results.push(TargetResult {
2445                url: target.url.clone(),
2446                passed,
2447                failed,
2448                elapsed: target_elapsed,
2449                report_json,
2450                owasp_coverage,
2451            });
2452        }
2453
2454        let total_elapsed = total_start.elapsed();
2455
2456        // Print summary table
2457        println!("\n{}", "=".repeat(80));
2458        println!("  Multi-Target Conformance Summary");
2459        println!("{}", "=".repeat(80));
2460        println!(
2461            "  {:<40} {:>8} {:>8} {:>8} {:>8}",
2462            "Target URL", "Passed", "Failed", "Rate", "Time"
2463        );
2464        println!("  {}", "-".repeat(76));
2465
2466        let mut total_passed = 0usize;
2467        let mut total_failed = 0usize;
2468
2469        for result in &target_results {
2470            let total_checks = result.passed + result.failed;
2471            let rate = if total_checks == 0 {
2472                0.0
2473            } else {
2474                (result.passed as f64 / total_checks as f64) * 100.0
2475            };
2476
2477            // Truncate long URLs for display
2478            let display_url = if result.url.len() > 38 {
2479                format!("{}...", &result.url[..35])
2480            } else {
2481                result.url.clone()
2482            };
2483
2484            println!(
2485                "  {:<40} {:>8} {:>8} {:>7.1}% {:>6.1}s",
2486                display_url,
2487                result.passed,
2488                result.failed,
2489                rate,
2490                result.elapsed.as_secs_f64()
2491            );
2492
2493            total_passed += result.passed;
2494            total_failed += result.failed;
2495        }
2496
2497        let grand_total = total_passed + total_failed;
2498        let overall_rate = if grand_total == 0 {
2499            0.0
2500        } else {
2501            (total_passed as f64 / grand_total as f64) * 100.0
2502        };
2503
2504        println!("  {}", "-".repeat(76));
2505        println!(
2506            "  {:<40} {:>8} {:>8} {:>7.1}% {:>6.1}s",
2507            format!("TOTAL ({} targets)", num_targets),
2508            total_passed,
2509            total_failed,
2510            overall_rate,
2511            total_elapsed.as_secs_f64()
2512        );
2513        println!("{}", "=".repeat(80));
2514
2515        // Print per-target OWASP coverage
2516        for result in &target_results {
2517            println!("\n  OWASP API Security Top 10 Coverage for {}:", result.url);
2518            for entry in &result.owasp_coverage {
2519                let status = if !entry.tested {
2520                    "-"
2521                } else if entry.all_passed {
2522                    "pass"
2523                } else {
2524                    "FAIL"
2525                };
2526                let via = if entry.via_categories.is_empty() {
2527                    String::new()
2528                } else {
2529                    format!(" (via {})", entry.via_categories.join(", "))
2530                };
2531                println!("    {:<12} {:<40} {}{}", entry.id, entry.name, status, via);
2532            }
2533        }
2534
2535        // Save combined summary
2536        let per_target_summaries: Vec<serde_json::Value> = target_results
2537            .iter()
2538            .enumerate()
2539            .map(|(idx, r)| {
2540                let total_checks = r.passed + r.failed;
2541                let rate = if total_checks == 0 {
2542                    0.0
2543                } else {
2544                    (r.passed as f64 / total_checks as f64) * 100.0
2545                };
2546                let owasp_json: Vec<serde_json::Value> = r
2547                    .owasp_coverage
2548                    .iter()
2549                    .map(|e| {
2550                        serde_json::json!({
2551                            "id": e.id,
2552                            "name": e.name,
2553                            "tested": e.tested,
2554                            "all_passed": e.all_passed,
2555                            "via_categories": e.via_categories,
2556                        })
2557                    })
2558                    .collect();
2559                serde_json::json!({
2560                    "target_url": r.url,
2561                    "target_index": idx,
2562                    "checks_passed": r.passed,
2563                    "checks_failed": r.failed,
2564                    "total_checks": total_checks,
2565                    "pass_rate": rate,
2566                    "elapsed_seconds": r.elapsed.as_secs_f64(),
2567                    "report": r.report_json,
2568                    "owasp_coverage": owasp_json,
2569                })
2570            })
2571            .collect();
2572
2573        let combined_summary = serde_json::json!({
2574            "total_targets": num_targets,
2575            "total_checks_passed": total_passed,
2576            "total_checks_failed": total_failed,
2577            "overall_pass_rate": overall_rate,
2578            "total_elapsed_seconds": total_elapsed.as_secs_f64(),
2579            "targets": per_target_summaries,
2580        });
2581
2582        let summary_path = self.output.join("multi-target-conformance-summary.json");
2583        let summary_str = serde_json::to_string_pretty(&combined_summary)
2584            .map_err(|e| BenchError::Other(format!("Failed to serialize summary: {}", e)))?;
2585        std::fs::write(&summary_path, &summary_str)
2586            .map_err(|e| BenchError::Other(format!("Failed to write summary: {}", e)))?;
2587        TerminalReporter::print_success(&format!(
2588            "Combined summary saved to: {}",
2589            summary_path.display()
2590        ));
2591
2592        Ok(())
2593    }
2594
2595    /// Execute OWASP API Security Top 10 testing mode
2596    async fn execute_owasp_test(&self, parser: &SpecParser) -> Result<()> {
2597        TerminalReporter::print_progress("OWASP API Security Top 10 Testing Mode");
2598
2599        // Parse custom headers from CLI
2600        let custom_headers = self.parse_headers()?;
2601
2602        // Build OWASP configuration from CLI options
2603        let mut config = OwaspApiConfig::new()
2604            .with_auth_header(&self.owasp_auth_header)
2605            .with_verbose(self.verbose)
2606            .with_insecure(self.skip_tls_verify)
2607            .with_concurrency(self.vus as usize)
2608            .with_iterations(self.owasp_iterations as usize)
2609            .with_base_path(self.base_path.clone())
2610            .with_custom_headers(custom_headers);
2611
2612        // Set valid auth token if provided
2613        if let Some(ref token) = self.owasp_auth_token {
2614            config = config.with_valid_auth_token(token);
2615        }
2616
2617        // Parse categories if provided
2618        if let Some(ref cats_str) = self.owasp_categories {
2619            let categories: Vec<OwaspCategory> = cats_str
2620                .split(',')
2621                .filter_map(|s| {
2622                    let trimmed = s.trim();
2623                    match trimmed.parse::<OwaspCategory>() {
2624                        Ok(cat) => Some(cat),
2625                        Err(e) => {
2626                            TerminalReporter::print_warning(&e);
2627                            None
2628                        }
2629                    }
2630                })
2631                .collect();
2632
2633            if !categories.is_empty() {
2634                config = config.with_categories(categories);
2635            }
2636        }
2637
2638        // Load admin paths from file if provided
2639        if let Some(ref admin_paths_file) = self.owasp_admin_paths {
2640            config.admin_paths_file = Some(admin_paths_file.clone());
2641            if let Err(e) = config.load_admin_paths() {
2642                TerminalReporter::print_warning(&format!("Failed to load admin paths file: {}", e));
2643            }
2644        }
2645
2646        // Set ID fields if provided
2647        if let Some(ref id_fields_str) = self.owasp_id_fields {
2648            let id_fields: Vec<String> = id_fields_str
2649                .split(',')
2650                .map(|s| s.trim().to_string())
2651                .filter(|s| !s.is_empty())
2652                .collect();
2653            if !id_fields.is_empty() {
2654                config = config.with_id_fields(id_fields);
2655            }
2656        }
2657
2658        // Set report path and format
2659        if let Some(ref report_path) = self.owasp_report {
2660            config = config.with_report_path(report_path);
2661        }
2662        if let Ok(format) = self.owasp_report_format.parse::<ReportFormat>() {
2663            config = config.with_report_format(format);
2664        }
2665
2666        // Print configuration summary
2667        let categories = config.categories_to_test();
2668        TerminalReporter::print_success(&format!(
2669            "Testing {} OWASP categories: {}",
2670            categories.len(),
2671            categories.iter().map(|c| c.cli_name()).collect::<Vec<_>>().join(", ")
2672        ));
2673
2674        if config.valid_auth_token.is_some() {
2675            TerminalReporter::print_progress("Using provided auth token for baseline requests");
2676        }
2677
2678        // Create the OWASP generator
2679        TerminalReporter::print_progress("Generating OWASP security test script...");
2680        let generator = OwaspApiGenerator::new(config, self.target.clone(), parser);
2681
2682        // Generate the script
2683        let script = generator.generate()?;
2684        TerminalReporter::print_success("OWASP security test script generated");
2685
2686        // Write script to file
2687        let script_path = if let Some(output) = &self.script_output {
2688            output.clone()
2689        } else {
2690            self.output.join("k6-owasp-security-test.js")
2691        };
2692
2693        if let Some(parent) = script_path.parent() {
2694            std::fs::create_dir_all(parent)?;
2695        }
2696        std::fs::write(&script_path, &script)?;
2697        TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
2698
2699        // If generate-only mode, exit here
2700        if self.generate_only {
2701            println!("\nOWASP security test script generated. Run it with:");
2702            println!("  k6 run {}", script_path.display());
2703            return Ok(());
2704        }
2705
2706        // Execute k6
2707        TerminalReporter::print_progress("Executing OWASP security tests...");
2708        let executor = K6Executor::new()?;
2709        std::fs::create_dir_all(&self.output)?;
2710
2711        let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
2712
2713        let duration_secs = Self::parse_duration(&self.duration)?;
2714        TerminalReporter::print_summary(&results, duration_secs);
2715
2716        println!("\nOWASP security test results saved to: {}", self.output.display());
2717
2718        Ok(())
2719    }
2720}
2721
2722#[cfg(test)]
2723mod tests {
2724    use super::*;
2725    use tempfile::tempdir;
2726
2727    #[test]
2728    fn test_parse_duration() {
2729        assert_eq!(BenchCommand::parse_duration("30s").unwrap(), 30);
2730        assert_eq!(BenchCommand::parse_duration("5m").unwrap(), 300);
2731        assert_eq!(BenchCommand::parse_duration("1h").unwrap(), 3600);
2732        assert_eq!(BenchCommand::parse_duration("60").unwrap(), 60);
2733    }
2734
2735    #[test]
2736    fn test_parse_duration_invalid() {
2737        assert!(BenchCommand::parse_duration("invalid").is_err());
2738        assert!(BenchCommand::parse_duration("30x").is_err());
2739    }
2740
2741    #[test]
2742    fn test_parse_headers() {
2743        let cmd = BenchCommand {
2744            spec: vec![PathBuf::from("test.yaml")],
2745            spec_dir: None,
2746            merge_conflicts: "error".to_string(),
2747            spec_mode: "merge".to_string(),
2748            dependency_config: None,
2749            target: "http://localhost".to_string(),
2750            base_path: None,
2751            duration: "1m".to_string(),
2752            vus: 10,
2753            scenario: "ramp-up".to_string(),
2754            operations: None,
2755            exclude_operations: None,
2756            auth: None,
2757            headers: Some("X-API-Key:test123,X-Client-ID:client456".to_string()),
2758            output: PathBuf::from("output"),
2759            generate_only: false,
2760            script_output: None,
2761            threshold_percentile: "p(95)".to_string(),
2762            threshold_ms: 500,
2763            max_error_rate: 0.05,
2764            verbose: false,
2765            skip_tls_verify: false,
2766            targets_file: None,
2767            max_concurrency: None,
2768            results_format: "both".to_string(),
2769            params_file: None,
2770            crud_flow: false,
2771            flow_config: None,
2772            extract_fields: None,
2773            parallel_create: None,
2774            data_file: None,
2775            data_distribution: "unique-per-vu".to_string(),
2776            data_mappings: None,
2777            per_uri_control: false,
2778            error_rate: None,
2779            error_types: None,
2780            security_test: false,
2781            security_payloads: None,
2782            security_categories: None,
2783            security_target_fields: None,
2784            wafbench_dir: None,
2785            wafbench_cycle_all: false,
2786            owasp_api_top10: false,
2787            owasp_categories: None,
2788            owasp_auth_header: "Authorization".to_string(),
2789            owasp_auth_token: None,
2790            owasp_admin_paths: None,
2791            owasp_id_fields: None,
2792            owasp_report: None,
2793            owasp_report_format: "json".to_string(),
2794            owasp_iterations: 1,
2795            conformance: false,
2796            conformance_api_key: None,
2797            conformance_basic_auth: None,
2798            conformance_report: PathBuf::from("conformance-report.json"),
2799            conformance_categories: None,
2800            conformance_report_format: "json".to_string(),
2801            conformance_headers: vec![],
2802            conformance_all_operations: false,
2803            conformance_custom: None,
2804            conformance_delay_ms: 0,
2805            use_k6: false,
2806            conformance_custom_filter: None,
2807            export_requests: false,
2808            validate_requests: false,
2809        };
2810
2811        let headers = cmd.parse_headers().unwrap();
2812        assert_eq!(headers.get("X-API-Key"), Some(&"test123".to_string()));
2813        assert_eq!(headers.get("X-Client-ID"), Some(&"client456".to_string()));
2814    }
2815
2816    #[test]
2817    fn test_get_spec_display_name() {
2818        let cmd = BenchCommand {
2819            spec: vec![PathBuf::from("test.yaml")],
2820            spec_dir: None,
2821            merge_conflicts: "error".to_string(),
2822            spec_mode: "merge".to_string(),
2823            dependency_config: None,
2824            target: "http://localhost".to_string(),
2825            base_path: None,
2826            duration: "1m".to_string(),
2827            vus: 10,
2828            scenario: "ramp-up".to_string(),
2829            operations: None,
2830            exclude_operations: None,
2831            auth: None,
2832            headers: None,
2833            output: PathBuf::from("output"),
2834            generate_only: false,
2835            script_output: None,
2836            threshold_percentile: "p(95)".to_string(),
2837            threshold_ms: 500,
2838            max_error_rate: 0.05,
2839            verbose: false,
2840            skip_tls_verify: false,
2841            targets_file: None,
2842            max_concurrency: None,
2843            results_format: "both".to_string(),
2844            params_file: None,
2845            crud_flow: false,
2846            flow_config: None,
2847            extract_fields: None,
2848            parallel_create: None,
2849            data_file: None,
2850            data_distribution: "unique-per-vu".to_string(),
2851            data_mappings: None,
2852            per_uri_control: false,
2853            error_rate: None,
2854            error_types: None,
2855            security_test: false,
2856            security_payloads: None,
2857            security_categories: None,
2858            security_target_fields: None,
2859            wafbench_dir: None,
2860            wafbench_cycle_all: false,
2861            owasp_api_top10: false,
2862            owasp_categories: None,
2863            owasp_auth_header: "Authorization".to_string(),
2864            owasp_auth_token: None,
2865            owasp_admin_paths: None,
2866            owasp_id_fields: None,
2867            owasp_report: None,
2868            owasp_report_format: "json".to_string(),
2869            owasp_iterations: 1,
2870            conformance: false,
2871            conformance_api_key: None,
2872            conformance_basic_auth: None,
2873            conformance_report: PathBuf::from("conformance-report.json"),
2874            conformance_categories: None,
2875            conformance_report_format: "json".to_string(),
2876            conformance_headers: vec![],
2877            conformance_all_operations: false,
2878            conformance_custom: None,
2879            conformance_delay_ms: 0,
2880            use_k6: false,
2881            conformance_custom_filter: None,
2882            export_requests: false,
2883            validate_requests: false,
2884        };
2885
2886        assert_eq!(cmd.get_spec_display_name(), "test.yaml");
2887
2888        // Test multiple specs
2889        let cmd_multi = BenchCommand {
2890            spec: vec![PathBuf::from("a.yaml"), PathBuf::from("b.yaml")],
2891            spec_dir: None,
2892            merge_conflicts: "error".to_string(),
2893            spec_mode: "merge".to_string(),
2894            dependency_config: None,
2895            target: "http://localhost".to_string(),
2896            base_path: None,
2897            duration: "1m".to_string(),
2898            vus: 10,
2899            scenario: "ramp-up".to_string(),
2900            operations: None,
2901            exclude_operations: None,
2902            auth: None,
2903            headers: None,
2904            output: PathBuf::from("output"),
2905            generate_only: false,
2906            script_output: None,
2907            threshold_percentile: "p(95)".to_string(),
2908            threshold_ms: 500,
2909            max_error_rate: 0.05,
2910            verbose: false,
2911            skip_tls_verify: false,
2912            targets_file: None,
2913            max_concurrency: None,
2914            results_format: "both".to_string(),
2915            params_file: None,
2916            crud_flow: false,
2917            flow_config: None,
2918            extract_fields: None,
2919            parallel_create: None,
2920            data_file: None,
2921            data_distribution: "unique-per-vu".to_string(),
2922            data_mappings: None,
2923            per_uri_control: false,
2924            error_rate: None,
2925            error_types: None,
2926            security_test: false,
2927            security_payloads: None,
2928            security_categories: None,
2929            security_target_fields: None,
2930            wafbench_dir: None,
2931            wafbench_cycle_all: false,
2932            owasp_api_top10: false,
2933            owasp_categories: None,
2934            owasp_auth_header: "Authorization".to_string(),
2935            owasp_auth_token: None,
2936            owasp_admin_paths: None,
2937            owasp_id_fields: None,
2938            owasp_report: None,
2939            owasp_report_format: "json".to_string(),
2940            owasp_iterations: 1,
2941            conformance: false,
2942            conformance_api_key: None,
2943            conformance_basic_auth: None,
2944            conformance_report: PathBuf::from("conformance-report.json"),
2945            conformance_categories: None,
2946            conformance_report_format: "json".to_string(),
2947            conformance_headers: vec![],
2948            conformance_all_operations: false,
2949            conformance_custom: None,
2950            conformance_delay_ms: 0,
2951            use_k6: false,
2952            conformance_custom_filter: None,
2953            export_requests: false,
2954            validate_requests: false,
2955        };
2956
2957        assert_eq!(cmd_multi.get_spec_display_name(), "2 spec files");
2958    }
2959
2960    #[test]
2961    fn test_parse_extracted_values_from_output_dir() {
2962        let dir = tempdir().unwrap();
2963        let path = dir.path().join("extracted_values.json");
2964        std::fs::write(
2965            &path,
2966            r#"{
2967  "pool_id": "abc123",
2968  "count": 0,
2969  "enabled": false,
2970  "metadata": { "owner": "team-a" }
2971}"#,
2972        )
2973        .unwrap();
2974
2975        let extracted = BenchCommand::parse_extracted_values(dir.path()).unwrap();
2976        assert_eq!(extracted.get("pool_id"), Some(&serde_json::json!("abc123")));
2977        assert_eq!(extracted.get("count"), Some(&serde_json::json!(0)));
2978        assert_eq!(extracted.get("enabled"), Some(&serde_json::json!(false)));
2979        assert_eq!(extracted.get("metadata"), Some(&serde_json::json!({"owner": "team-a"})));
2980    }
2981
2982    #[test]
2983    fn test_parse_extracted_values_missing_file() {
2984        let dir = tempdir().unwrap();
2985        let extracted = BenchCommand::parse_extracted_values(dir.path()).unwrap();
2986        assert!(extracted.values.is_empty());
2987    }
2988}