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