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