1use 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, InvalidDataType};
9use crate::k6_gen::{K6Config, K6ScriptGenerator};
10use crate::mock_integration::{
11 MockIntegrationConfig, MockIntegrationGenerator, MockServerDetector,
12};
13use crate::owasp_api::{OwaspApiConfig, OwaspApiGenerator, OwaspCategory, ReportFormat};
14use crate::parallel_executor::{AggregatedResults, ParallelExecutor};
15use crate::parallel_requests::{ParallelConfig, ParallelRequestGenerator};
16use crate::param_overrides::ParameterOverrides;
17use crate::reporter::TerminalReporter;
18use crate::request_gen::RequestGenerator;
19use crate::scenarios::LoadScenario;
20use crate::security_payloads::{
21 SecurityCategory, SecurityPayload, SecurityPayloads, SecurityTestConfig, SecurityTestGenerator,
22};
23use crate::spec_dependencies::{
24 topological_sort, DependencyDetector, ExtractedValues, SpecDependencyConfig,
25};
26use crate::spec_parser::SpecParser;
27use crate::target_parser::parse_targets_file;
28use crate::wafbench::WafBenchLoader;
29use mockforge_core::openapi::multi_spec::{
30 load_specs_from_directory, load_specs_from_files, merge_specs, ConflictStrategy,
31};
32use mockforge_core::openapi::spec::OpenApiSpec;
33use std::collections::{HashMap, HashSet};
34use std::path::PathBuf;
35use std::str::FromStr;
36
37pub struct BenchCommand {
39 pub spec: Vec<PathBuf>,
41 pub spec_dir: Option<PathBuf>,
43 pub merge_conflicts: String,
45 pub spec_mode: String,
47 pub dependency_config: Option<PathBuf>,
49 pub target: String,
50 pub base_path: Option<String>,
53 pub duration: String,
54 pub vus: u32,
55 pub scenario: String,
56 pub operations: Option<String>,
57 pub exclude_operations: Option<String>,
61 pub auth: Option<String>,
62 pub headers: Option<String>,
63 pub output: PathBuf,
64 pub generate_only: bool,
65 pub script_output: Option<PathBuf>,
66 pub threshold_percentile: String,
67 pub threshold_ms: u64,
68 pub max_error_rate: f64,
69 pub verbose: bool,
70 pub skip_tls_verify: bool,
71 pub targets_file: Option<PathBuf>,
73 pub max_concurrency: Option<u32>,
75 pub results_format: String,
77 pub params_file: Option<PathBuf>,
82
83 pub crud_flow: bool,
86 pub flow_config: Option<PathBuf>,
88 pub extract_fields: Option<String>,
90
91 pub parallel_create: Option<u32>,
94
95 pub data_file: Option<PathBuf>,
98 pub data_distribution: String,
100 pub data_mappings: Option<String>,
102 pub per_uri_control: bool,
104
105 pub error_rate: Option<f64>,
108 pub error_types: Option<String>,
110
111 pub security_test: bool,
114 pub security_payloads: Option<PathBuf>,
116 pub security_categories: Option<String>,
118 pub security_target_fields: Option<String>,
120
121 pub wafbench_dir: Option<String>,
124 pub wafbench_cycle_all: bool,
126
127 pub owasp_api_top10: bool,
130 pub owasp_categories: Option<String>,
132 pub owasp_auth_header: String,
134 pub owasp_auth_token: Option<String>,
136 pub owasp_admin_paths: Option<PathBuf>,
138 pub owasp_id_fields: Option<String>,
140 pub owasp_report: Option<PathBuf>,
142 pub owasp_report_format: String,
144 pub owasp_iterations: u32,
146}
147
148impl BenchCommand {
149 pub async fn load_and_merge_specs(&self) -> Result<OpenApiSpec> {
151 let mut all_specs: Vec<(PathBuf, OpenApiSpec)> = Vec::new();
152
153 if !self.spec.is_empty() {
155 let specs = load_specs_from_files(self.spec.clone())
156 .await
157 .map_err(|e| BenchError::Other(format!("Failed to load spec files: {}", e)))?;
158 all_specs.extend(specs);
159 }
160
161 if let Some(spec_dir) = &self.spec_dir {
163 let dir_specs = load_specs_from_directory(spec_dir).await.map_err(|e| {
164 BenchError::Other(format!("Failed to load specs from directory: {}", e))
165 })?;
166 all_specs.extend(dir_specs);
167 }
168
169 if all_specs.is_empty() {
170 return Err(BenchError::Other(
171 "No spec files provided. Use --spec or --spec-dir.".to_string(),
172 ));
173 }
174
175 if all_specs.len() == 1 {
177 return Ok(all_specs.into_iter().next().expect("checked len() == 1 above").1);
179 }
180
181 let conflict_strategy = match self.merge_conflicts.as_str() {
183 "first" => ConflictStrategy::First,
184 "last" => ConflictStrategy::Last,
185 _ => ConflictStrategy::Error,
186 };
187
188 merge_specs(all_specs, conflict_strategy)
189 .map_err(|e| BenchError::Other(format!("Failed to merge specs: {}", e)))
190 }
191
192 fn get_spec_display_name(&self) -> String {
194 if self.spec.len() == 1 {
195 self.spec[0].to_string_lossy().to_string()
196 } else if !self.spec.is_empty() {
197 format!("{} spec files", self.spec.len())
198 } else if let Some(dir) = &self.spec_dir {
199 format!("specs from {}", dir.display())
200 } else {
201 "no specs".to_string()
202 }
203 }
204
205 pub async fn execute(&self) -> Result<()> {
207 if let Some(targets_file) = &self.targets_file {
209 return self.execute_multi_target(targets_file).await;
210 }
211
212 if self.spec_mode == "sequential" && (self.spec.len() > 1 || self.spec_dir.is_some()) {
214 return self.execute_sequential_specs().await;
215 }
216
217 TerminalReporter::print_header(
220 &self.get_spec_display_name(),
221 &self.target,
222 0, &self.scenario,
224 Self::parse_duration(&self.duration)?,
225 );
226
227 if !K6Executor::is_k6_installed() {
229 TerminalReporter::print_error("k6 is not installed");
230 TerminalReporter::print_warning(
231 "Install k6 from: https://k6.io/docs/get-started/installation/",
232 );
233 return Err(BenchError::K6NotFound);
234 }
235
236 TerminalReporter::print_progress("Loading OpenAPI specification(s)...");
238 let merged_spec = self.load_and_merge_specs().await?;
239 let parser = SpecParser::from_spec(merged_spec);
240 if self.spec.len() > 1 || self.spec_dir.is_some() {
241 TerminalReporter::print_success(&format!(
242 "Loaded and merged {} specification(s)",
243 self.spec.len() + self.spec_dir.as_ref().map(|_| 1).unwrap_or(0)
244 ));
245 } else {
246 TerminalReporter::print_success("Specification loaded");
247 }
248
249 let mock_config = self.build_mock_config().await;
251 if mock_config.is_mock_server {
252 TerminalReporter::print_progress("Mock server integration enabled");
253 }
254
255 if self.crud_flow {
257 return self.execute_crud_flow(&parser).await;
258 }
259
260 if self.owasp_api_top10 {
262 return self.execute_owasp_test(&parser).await;
263 }
264
265 TerminalReporter::print_progress("Extracting API operations...");
267 let mut operations = if let Some(filter) = &self.operations {
268 parser.filter_operations(filter)?
269 } else {
270 parser.get_operations()
271 };
272
273 if let Some(exclude) = &self.exclude_operations {
275 let before_count = operations.len();
276 operations = parser.exclude_operations(operations, exclude)?;
277 let excluded_count = before_count - operations.len();
278 if excluded_count > 0 {
279 TerminalReporter::print_progress(&format!(
280 "Excluded {} operations matching '{}'",
281 excluded_count, exclude
282 ));
283 }
284 }
285
286 if operations.is_empty() {
287 return Err(BenchError::Other("No operations found in spec".to_string()));
288 }
289
290 TerminalReporter::print_success(&format!("Found {} operations", operations.len()));
291
292 let param_overrides = if let Some(params_file) = &self.params_file {
294 TerminalReporter::print_progress("Loading parameter overrides...");
295 let overrides = ParameterOverrides::from_file(params_file)?;
296 TerminalReporter::print_success(&format!(
297 "Loaded parameter overrides ({} operation-specific, {} defaults)",
298 overrides.operations.len(),
299 if overrides.defaults.is_empty() { 0 } else { 1 }
300 ));
301 Some(overrides)
302 } else {
303 None
304 };
305
306 TerminalReporter::print_progress("Generating request templates...");
308 let templates: Vec<_> = operations
309 .iter()
310 .map(|op| {
311 let op_overrides = param_overrides.as_ref().map(|po| {
312 po.get_for_operation(op.operation_id.as_deref(), &op.method, &op.path)
313 });
314 RequestGenerator::generate_template_with_overrides(op, op_overrides.as_ref())
315 })
316 .collect::<Result<Vec<_>>>()?;
317 TerminalReporter::print_success("Request templates generated");
318
319 let custom_headers = self.parse_headers()?;
321
322 let base_path = self.resolve_base_path(&parser);
324 if let Some(ref bp) = base_path {
325 TerminalReporter::print_progress(&format!("Using base path: {}", bp));
326 }
327
328 TerminalReporter::print_progress("Generating k6 load test script...");
330 let scenario =
331 LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
332
333 let security_testing_enabled = self.security_test || self.wafbench_dir.is_some();
335
336 let k6_config = K6Config {
337 target_url: self.target.clone(),
338 base_path,
339 scenario,
340 duration_secs: Self::parse_duration(&self.duration)?,
341 max_vus: self.vus,
342 threshold_percentile: self.threshold_percentile.clone(),
343 threshold_ms: self.threshold_ms,
344 max_error_rate: self.max_error_rate,
345 auth_header: self.auth.clone(),
346 custom_headers,
347 skip_tls_verify: self.skip_tls_verify,
348 security_testing_enabled,
349 };
350
351 let generator = K6ScriptGenerator::new(k6_config, templates);
352 let mut script = generator.generate()?;
353 TerminalReporter::print_success("k6 script generated");
354
355 let has_advanced_features = self.data_file.is_some()
357 || self.error_rate.is_some()
358 || self.security_test
359 || self.parallel_create.is_some()
360 || self.wafbench_dir.is_some();
361
362 if has_advanced_features {
364 script = self.generate_enhanced_script(&script)?;
365 }
366
367 if mock_config.is_mock_server {
369 let setup_code = MockIntegrationGenerator::generate_setup(&mock_config);
370 let teardown_code = MockIntegrationGenerator::generate_teardown(&mock_config);
371 let helper_code = MockIntegrationGenerator::generate_vu_id_helper();
372
373 if let Some(import_end) = script.find("export const options") {
375 script.insert_str(
376 import_end,
377 &format!(
378 "\n// === Mock Server Integration ===\n{}\n{}\n{}\n",
379 helper_code, setup_code, teardown_code
380 ),
381 );
382 }
383 }
384
385 TerminalReporter::print_progress("Validating k6 script...");
387 let validation_errors = K6ScriptGenerator::validate_script(&script);
388 if !validation_errors.is_empty() {
389 TerminalReporter::print_error("Script validation failed");
390 for error in &validation_errors {
391 eprintln!(" {}", error);
392 }
393 return Err(BenchError::Other(format!(
394 "Generated k6 script has {} validation error(s). Please check the output above.",
395 validation_errors.len()
396 )));
397 }
398 TerminalReporter::print_success("Script validation passed");
399
400 let script_path = if let Some(output) = &self.script_output {
402 output.clone()
403 } else {
404 self.output.join("k6-script.js")
405 };
406
407 if let Some(parent) = script_path.parent() {
408 std::fs::create_dir_all(parent)?;
409 }
410 std::fs::write(&script_path, &script)?;
411 TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
412
413 if self.generate_only {
415 println!("\nScript generated successfully. Run it with:");
416 println!(" k6 run {}", script_path.display());
417 return Ok(());
418 }
419
420 TerminalReporter::print_progress("Executing load test...");
422 let executor = K6Executor::new()?;
423
424 std::fs::create_dir_all(&self.output)?;
425
426 let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
427
428 let duration_secs = Self::parse_duration(&self.duration)?;
430 TerminalReporter::print_summary(&results, duration_secs);
431
432 println!("\nResults saved to: {}", self.output.display());
433
434 Ok(())
435 }
436
437 async fn execute_multi_target(&self, targets_file: &PathBuf) -> Result<()> {
439 TerminalReporter::print_progress("Parsing targets file...");
440 let targets = parse_targets_file(targets_file)?;
441 let num_targets = targets.len();
442 TerminalReporter::print_success(&format!("Loaded {} targets", num_targets));
443
444 if targets.is_empty() {
445 return Err(BenchError::Other("No targets found in file".to_string()));
446 }
447
448 let max_concurrency = self.max_concurrency.unwrap_or(10) as usize;
450 let max_concurrency = max_concurrency.min(num_targets); TerminalReporter::print_header(
454 &self.get_spec_display_name(),
455 &format!("{} targets", num_targets),
456 0,
457 &self.scenario,
458 Self::parse_duration(&self.duration)?,
459 );
460
461 let executor = ParallelExecutor::new(
463 BenchCommand {
464 spec: self.spec.clone(),
466 spec_dir: self.spec_dir.clone(),
467 merge_conflicts: self.merge_conflicts.clone(),
468 spec_mode: self.spec_mode.clone(),
469 dependency_config: self.dependency_config.clone(),
470 target: self.target.clone(), base_path: self.base_path.clone(),
472 duration: self.duration.clone(),
473 vus: self.vus,
474 scenario: self.scenario.clone(),
475 operations: self.operations.clone(),
476 exclude_operations: self.exclude_operations.clone(),
477 auth: self.auth.clone(),
478 headers: self.headers.clone(),
479 output: self.output.clone(),
480 generate_only: self.generate_only,
481 script_output: self.script_output.clone(),
482 threshold_percentile: self.threshold_percentile.clone(),
483 threshold_ms: self.threshold_ms,
484 max_error_rate: self.max_error_rate,
485 verbose: self.verbose,
486 skip_tls_verify: self.skip_tls_verify,
487 targets_file: None,
488 max_concurrency: None,
489 results_format: self.results_format.clone(),
490 params_file: self.params_file.clone(),
491 crud_flow: self.crud_flow,
492 flow_config: self.flow_config.clone(),
493 extract_fields: self.extract_fields.clone(),
494 parallel_create: self.parallel_create,
495 data_file: self.data_file.clone(),
496 data_distribution: self.data_distribution.clone(),
497 data_mappings: self.data_mappings.clone(),
498 per_uri_control: self.per_uri_control,
499 error_rate: self.error_rate,
500 error_types: self.error_types.clone(),
501 security_test: self.security_test,
502 security_payloads: self.security_payloads.clone(),
503 security_categories: self.security_categories.clone(),
504 security_target_fields: self.security_target_fields.clone(),
505 wafbench_dir: self.wafbench_dir.clone(),
506 wafbench_cycle_all: self.wafbench_cycle_all,
507 owasp_api_top10: self.owasp_api_top10,
508 owasp_categories: self.owasp_categories.clone(),
509 owasp_auth_header: self.owasp_auth_header.clone(),
510 owasp_auth_token: self.owasp_auth_token.clone(),
511 owasp_admin_paths: self.owasp_admin_paths.clone(),
512 owasp_id_fields: self.owasp_id_fields.clone(),
513 owasp_report: self.owasp_report.clone(),
514 owasp_report_format: self.owasp_report_format.clone(),
515 owasp_iterations: self.owasp_iterations,
516 },
517 targets,
518 max_concurrency,
519 );
520
521 let aggregated_results = executor.execute_all().await?;
523
524 self.report_multi_target_results(&aggregated_results)?;
526
527 Ok(())
528 }
529
530 fn report_multi_target_results(&self, results: &AggregatedResults) -> Result<()> {
532 TerminalReporter::print_multi_target_summary(results);
534
535 if self.results_format == "aggregated" || self.results_format == "both" {
537 let summary_path = self.output.join("aggregated_summary.json");
538 let summary_json = serde_json::json!({
539 "total_targets": results.total_targets,
540 "successful_targets": results.successful_targets,
541 "failed_targets": results.failed_targets,
542 "aggregated_metrics": {
543 "total_requests": results.aggregated_metrics.total_requests,
544 "total_failed_requests": results.aggregated_metrics.total_failed_requests,
545 "avg_duration_ms": results.aggregated_metrics.avg_duration_ms,
546 "p95_duration_ms": results.aggregated_metrics.p95_duration_ms,
547 "p99_duration_ms": results.aggregated_metrics.p99_duration_ms,
548 "error_rate": results.aggregated_metrics.error_rate,
549 },
550 "target_results": results.target_results.iter().map(|r| {
551 serde_json::json!({
552 "target_url": r.target_url,
553 "target_index": r.target_index,
554 "success": r.success,
555 "error": r.error,
556 "total_requests": r.results.total_requests,
557 "failed_requests": r.results.failed_requests,
558 "avg_duration_ms": r.results.avg_duration_ms,
559 "p95_duration_ms": r.results.p95_duration_ms,
560 "p99_duration_ms": r.results.p99_duration_ms,
561 "output_dir": r.output_dir.to_string_lossy(),
562 })
563 }).collect::<Vec<_>>(),
564 });
565
566 std::fs::write(&summary_path, serde_json::to_string_pretty(&summary_json)?)?;
567 TerminalReporter::print_success(&format!(
568 "Aggregated summary saved to: {}",
569 summary_path.display()
570 ));
571 }
572
573 println!("\nResults saved to: {}", self.output.display());
574 println!(" - Per-target results: {}", self.output.join("target_*").display());
575 if self.results_format == "aggregated" || self.results_format == "both" {
576 println!(
577 " - Aggregated summary: {}",
578 self.output.join("aggregated_summary.json").display()
579 );
580 }
581
582 Ok(())
583 }
584
585 pub fn parse_duration(duration: &str) -> Result<u64> {
587 let duration = duration.trim();
588
589 if let Some(secs) = duration.strip_suffix('s') {
590 secs.parse::<u64>()
591 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
592 } else if let Some(mins) = duration.strip_suffix('m') {
593 mins.parse::<u64>()
594 .map(|m| m * 60)
595 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
596 } else if let Some(hours) = duration.strip_suffix('h') {
597 hours
598 .parse::<u64>()
599 .map(|h| h * 3600)
600 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
601 } else {
602 duration
604 .parse::<u64>()
605 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
606 }
607 }
608
609 pub fn parse_headers(&self) -> Result<HashMap<String, String>> {
611 let mut headers = HashMap::new();
612
613 if let Some(header_str) = &self.headers {
614 for pair in header_str.split(',') {
615 let parts: Vec<&str> = pair.splitn(2, ':').collect();
616 if parts.len() != 2 {
617 return Err(BenchError::Other(format!(
618 "Invalid header format: '{}'. Expected 'Key:Value'",
619 pair
620 )));
621 }
622 headers.insert(parts[0].trim().to_string(), parts[1].trim().to_string());
623 }
624 }
625
626 Ok(headers)
627 }
628
629 fn resolve_base_path(&self, parser: &SpecParser) -> Option<String> {
638 if let Some(cli_base_path) = &self.base_path {
640 if cli_base_path.is_empty() {
641 return None;
643 }
644 return Some(cli_base_path.clone());
645 }
646
647 parser.get_base_path()
649 }
650
651 async fn build_mock_config(&self) -> MockIntegrationConfig {
653 if MockServerDetector::looks_like_mock_server(&self.target) {
655 if let Ok(info) = MockServerDetector::detect(&self.target).await {
657 if info.is_mockforge {
658 TerminalReporter::print_success(&format!(
659 "Detected MockForge server (version: {})",
660 info.version.as_deref().unwrap_or("unknown")
661 ));
662 return MockIntegrationConfig::mock_server();
663 }
664 }
665 }
666 MockIntegrationConfig::real_api()
667 }
668
669 fn build_crud_flow_config(&self) -> Option<CrudFlowConfig> {
671 if !self.crud_flow {
672 return None;
673 }
674
675 if let Some(config_path) = &self.flow_config {
677 match CrudFlowConfig::from_file(config_path) {
678 Ok(config) => return Some(config),
679 Err(e) => {
680 TerminalReporter::print_warning(&format!(
681 "Failed to load flow config: {}. Using auto-detection.",
682 e
683 ));
684 }
685 }
686 }
687
688 let extract_fields = self
690 .extract_fields
691 .as_ref()
692 .map(|f| f.split(',').map(|s| s.trim().to_string()).collect())
693 .unwrap_or_else(|| vec!["id".to_string(), "uuid".to_string()]);
694
695 Some(CrudFlowConfig {
696 flows: Vec::new(), default_extract_fields: extract_fields,
698 })
699 }
700
701 fn build_data_driven_config(&self) -> Option<DataDrivenConfig> {
703 let data_file = self.data_file.as_ref()?;
704
705 let distribution = DataDistribution::from_str(&self.data_distribution)
706 .unwrap_or(DataDistribution::UniquePerVu);
707
708 let mappings = self
709 .data_mappings
710 .as_ref()
711 .map(|m| DataMapping::parse_mappings(m).unwrap_or_default())
712 .unwrap_or_default();
713
714 Some(DataDrivenConfig {
715 file_path: data_file.to_string_lossy().to_string(),
716 distribution,
717 mappings,
718 csv_has_header: true,
719 per_uri_control: self.per_uri_control,
720 per_uri_columns: crate::data_driven::PerUriColumns::default(),
721 })
722 }
723
724 fn build_invalid_data_config(&self) -> Option<InvalidDataConfig> {
726 let error_rate = self.error_rate?;
727
728 let error_types = self
729 .error_types
730 .as_ref()
731 .map(|types| InvalidDataConfig::parse_error_types(types).unwrap_or_default())
732 .unwrap_or_default();
733
734 Some(InvalidDataConfig {
735 error_rate,
736 error_types,
737 target_fields: Vec::new(),
738 })
739 }
740
741 fn build_security_config(&self) -> Option<SecurityTestConfig> {
743 if !self.security_test {
744 return None;
745 }
746
747 let categories = self
748 .security_categories
749 .as_ref()
750 .map(|cats| SecurityTestConfig::parse_categories(cats).unwrap_or_default())
751 .unwrap_or_else(|| {
752 let mut default = std::collections::HashSet::new();
753 default.insert(SecurityCategory::SqlInjection);
754 default.insert(SecurityCategory::Xss);
755 default
756 });
757
758 let target_fields = self
759 .security_target_fields
760 .as_ref()
761 .map(|fields| fields.split(',').map(|f| f.trim().to_string()).collect())
762 .unwrap_or_default();
763
764 let custom_payloads_file =
765 self.security_payloads.as_ref().map(|p| p.to_string_lossy().to_string());
766
767 Some(SecurityTestConfig {
768 enabled: true,
769 categories,
770 target_fields,
771 custom_payloads_file,
772 include_high_risk: false,
773 })
774 }
775
776 fn build_parallel_config(&self) -> Option<ParallelConfig> {
778 let count = self.parallel_create?;
779
780 Some(ParallelConfig::new(count))
781 }
782
783 fn load_wafbench_payloads(&self) -> Vec<SecurityPayload> {
785 let Some(ref wafbench_dir) = self.wafbench_dir else {
786 return Vec::new();
787 };
788
789 let mut loader = WafBenchLoader::new();
790
791 if let Err(e) = loader.load_from_pattern(wafbench_dir) {
792 TerminalReporter::print_warning(&format!("Failed to load WAFBench tests: {}", e));
793 return Vec::new();
794 }
795
796 let stats = loader.stats();
797
798 if stats.files_processed == 0 {
799 TerminalReporter::print_warning(&format!(
800 "No WAFBench YAML files found matching '{}'",
801 wafbench_dir
802 ));
803 if !stats.parse_errors.is_empty() {
805 TerminalReporter::print_warning("Some files were found but failed to parse:");
806 for error in &stats.parse_errors {
807 TerminalReporter::print_warning(&format!(" - {}", error));
808 }
809 }
810 return Vec::new();
811 }
812
813 TerminalReporter::print_progress(&format!(
814 "Loaded {} WAFBench files, {} test cases, {} payloads",
815 stats.files_processed, stats.test_cases_loaded, stats.payloads_extracted
816 ));
817
818 for (category, count) in &stats.by_category {
820 TerminalReporter::print_progress(&format!(" - {}: {} tests", category, count));
821 }
822
823 for error in &stats.parse_errors {
825 TerminalReporter::print_warning(&format!(" Parse error: {}", error));
826 }
827
828 loader.to_security_payloads()
829 }
830
831 fn generate_enhanced_script(&self, base_script: &str) -> Result<String> {
833 let mut enhanced_script = base_script.to_string();
834 let mut additional_code = String::new();
835
836 if let Some(config) = self.build_data_driven_config() {
838 TerminalReporter::print_progress("Adding data-driven testing support...");
839 additional_code.push_str(&DataDrivenGenerator::generate_setup(&config));
840 additional_code.push('\n');
841 TerminalReporter::print_success("Data-driven testing enabled");
842 }
843
844 if let Some(config) = self.build_invalid_data_config() {
846 TerminalReporter::print_progress("Adding invalid data testing support...");
847 additional_code.push_str(&InvalidDataGenerator::generate_invalidation_logic());
848 additional_code.push('\n');
849 additional_code
850 .push_str(&InvalidDataGenerator::generate_should_invalidate(config.error_rate));
851 additional_code.push('\n');
852 additional_code
853 .push_str(&InvalidDataGenerator::generate_type_selection(&config.error_types));
854 additional_code.push('\n');
855 TerminalReporter::print_success(&format!(
856 "Invalid data testing enabled ({}% error rate)",
857 (self.error_rate.unwrap_or(0.0) * 100.0) as u32
858 ));
859 }
860
861 let security_config = self.build_security_config();
863 let wafbench_payloads = self.load_wafbench_payloads();
864
865 if security_config.is_some() || !wafbench_payloads.is_empty() {
866 TerminalReporter::print_progress("Adding security testing support...");
867
868 let mut payload_list: Vec<SecurityPayload> = Vec::new();
870
871 if let Some(ref config) = security_config {
872 payload_list.extend(SecurityPayloads::get_payloads(config));
873 }
874
875 if !wafbench_payloads.is_empty() {
877 TerminalReporter::print_progress(&format!(
878 "Loading {} WAFBench attack patterns...",
879 wafbench_payloads.len()
880 ));
881 payload_list.extend(wafbench_payloads);
882 }
883
884 let target_fields =
885 security_config.as_ref().map(|c| c.target_fields.clone()).unwrap_or_default();
886
887 additional_code.push_str(&SecurityTestGenerator::generate_payload_selection(
888 &payload_list,
889 self.wafbench_cycle_all,
890 ));
891 additional_code.push('\n');
892 additional_code
893 .push_str(&SecurityTestGenerator::generate_apply_payload(&target_fields));
894 additional_code.push('\n');
895 additional_code.push_str(&SecurityTestGenerator::generate_security_checks());
896 additional_code.push('\n');
897
898 let mode = if self.wafbench_cycle_all {
899 "cycle-all"
900 } else {
901 "random"
902 };
903 TerminalReporter::print_success(&format!(
904 "Security testing enabled ({} payloads, {} mode)",
905 payload_list.len(),
906 mode
907 ));
908 }
909
910 if let Some(config) = self.build_parallel_config() {
912 TerminalReporter::print_progress("Adding parallel execution support...");
913 additional_code.push_str(&ParallelRequestGenerator::generate_batch_helper(&config));
914 additional_code.push('\n');
915 TerminalReporter::print_success(&format!(
916 "Parallel execution enabled (count: {})",
917 config.count
918 ));
919 }
920
921 if !additional_code.is_empty() {
923 if let Some(import_end) = enhanced_script.find("export const options") {
925 enhanced_script.insert_str(
926 import_end,
927 &format!("\n// === Advanced Testing Features ===\n{}\n", additional_code),
928 );
929 }
930 }
931
932 Ok(enhanced_script)
933 }
934
935 async fn execute_sequential_specs(&self) -> Result<()> {
937 TerminalReporter::print_progress("Sequential spec mode: Loading specs individually...");
938
939 let mut all_specs: Vec<(PathBuf, OpenApiSpec)> = Vec::new();
941
942 if !self.spec.is_empty() {
943 let specs = load_specs_from_files(self.spec.clone())
944 .await
945 .map_err(|e| BenchError::Other(format!("Failed to load spec files: {}", e)))?;
946 all_specs.extend(specs);
947 }
948
949 if let Some(spec_dir) = &self.spec_dir {
950 let dir_specs = load_specs_from_directory(spec_dir).await.map_err(|e| {
951 BenchError::Other(format!("Failed to load specs from directory: {}", e))
952 })?;
953 all_specs.extend(dir_specs);
954 }
955
956 if all_specs.is_empty() {
957 return Err(BenchError::Other(
958 "No spec files found for sequential execution".to_string(),
959 ));
960 }
961
962 TerminalReporter::print_success(&format!("Loaded {} spec(s)", all_specs.len()));
963
964 let execution_order = if let Some(config_path) = &self.dependency_config {
966 TerminalReporter::print_progress("Loading dependency configuration...");
967 let config = SpecDependencyConfig::from_file(config_path)?;
968
969 if !config.disable_auto_detect && config.execution_order.is_empty() {
970 self.detect_and_sort_specs(&all_specs)?
972 } else {
973 config.execution_order.iter().flat_map(|g| g.specs.clone()).collect()
975 }
976 } else {
977 self.detect_and_sort_specs(&all_specs)?
979 };
980
981 TerminalReporter::print_success(&format!(
982 "Execution order: {}",
983 execution_order
984 .iter()
985 .map(|p| p.file_name().unwrap_or_default().to_string_lossy().to_string())
986 .collect::<Vec<_>>()
987 .join(" → ")
988 ));
989
990 let mut extracted_values = ExtractedValues::new();
992 let total_specs = execution_order.len();
993
994 for (index, spec_path) in execution_order.iter().enumerate() {
995 let spec_name = spec_path.file_name().unwrap_or_default().to_string_lossy().to_string();
996
997 TerminalReporter::print_progress(&format!(
998 "[{}/{}] Executing spec: {}",
999 index + 1,
1000 total_specs,
1001 spec_name
1002 ));
1003
1004 let spec = all_specs
1006 .iter()
1007 .find(|(p, _)| {
1008 p == spec_path
1009 || p.file_name() == spec_path.file_name()
1010 || p.file_name() == Some(spec_path.as_os_str())
1011 })
1012 .map(|(_, s)| s.clone())
1013 .ok_or_else(|| {
1014 BenchError::Other(format!("Spec not found: {}", spec_path.display()))
1015 })?;
1016
1017 let new_values = self.execute_single_spec(&spec, &spec_name, &extracted_values).await?;
1019
1020 extracted_values.merge(&new_values);
1022
1023 TerminalReporter::print_success(&format!(
1024 "[{}/{}] Completed: {} (extracted {} values)",
1025 index + 1,
1026 total_specs,
1027 spec_name,
1028 new_values.values.len()
1029 ));
1030 }
1031
1032 TerminalReporter::print_success(&format!(
1033 "Sequential execution complete: {} specs executed",
1034 total_specs
1035 ));
1036
1037 Ok(())
1038 }
1039
1040 fn detect_and_sort_specs(&self, specs: &[(PathBuf, OpenApiSpec)]) -> Result<Vec<PathBuf>> {
1042 TerminalReporter::print_progress("Auto-detecting spec dependencies...");
1043
1044 let mut detector = DependencyDetector::new();
1045 let dependencies = detector.detect_dependencies(specs);
1046
1047 if dependencies.is_empty() {
1048 TerminalReporter::print_progress("No dependencies detected, using file order");
1049 return Ok(specs.iter().map(|(p, _)| p.clone()).collect());
1050 }
1051
1052 TerminalReporter::print_progress(&format!(
1053 "Detected {} cross-spec dependencies",
1054 dependencies.len()
1055 ));
1056
1057 for dep in &dependencies {
1058 TerminalReporter::print_progress(&format!(
1059 " {} → {} (via field '{}')",
1060 dep.dependency_spec.file_name().unwrap_or_default().to_string_lossy(),
1061 dep.dependent_spec.file_name().unwrap_or_default().to_string_lossy(),
1062 dep.field_name
1063 ));
1064 }
1065
1066 topological_sort(specs, &dependencies)
1067 }
1068
1069 async fn execute_single_spec(
1071 &self,
1072 spec: &OpenApiSpec,
1073 spec_name: &str,
1074 _external_values: &ExtractedValues,
1075 ) -> Result<ExtractedValues> {
1076 let parser = SpecParser::from_spec(spec.clone());
1077
1078 if self.crud_flow {
1080 self.execute_crud_flow_with_extraction(&parser, spec_name).await
1082 } else {
1083 self.execute_standard_spec(&parser, spec_name).await?;
1085 Ok(ExtractedValues::new())
1086 }
1087 }
1088
1089 async fn execute_crud_flow_with_extraction(
1091 &self,
1092 parser: &SpecParser,
1093 spec_name: &str,
1094 ) -> Result<ExtractedValues> {
1095 let operations = parser.get_operations();
1096 let flows = CrudFlowDetector::detect_flows(&operations);
1097
1098 if flows.is_empty() {
1099 TerminalReporter::print_warning(&format!("No CRUD flows detected in {}", spec_name));
1100 return Ok(ExtractedValues::new());
1101 }
1102
1103 TerminalReporter::print_progress(&format!(
1104 " {} CRUD flow(s) in {}",
1105 flows.len(),
1106 spec_name
1107 ));
1108
1109 let mut handlebars = handlebars::Handlebars::new();
1111 handlebars.register_helper(
1113 "json",
1114 Box::new(
1115 |h: &handlebars::Helper,
1116 _: &handlebars::Handlebars,
1117 _: &handlebars::Context,
1118 _: &mut handlebars::RenderContext,
1119 out: &mut dyn handlebars::Output|
1120 -> handlebars::HelperResult {
1121 let param = h.param(0).map(|v| v.value()).unwrap_or(&serde_json::Value::Null);
1122 out.write(&serde_json::to_string(param).unwrap_or_else(|_| "[]".to_string()))?;
1123 Ok(())
1124 },
1125 ),
1126 );
1127 let template = include_str!("templates/k6_crud_flow.hbs");
1128
1129 let custom_headers = self.parse_headers()?;
1130 let config = self.build_crud_flow_config().unwrap_or_default();
1131
1132 let param_overrides = if let Some(params_file) = &self.params_file {
1134 let overrides = ParameterOverrides::from_file(params_file)?;
1135 Some(overrides)
1136 } else {
1137 None
1138 };
1139
1140 let duration_secs = Self::parse_duration(&self.duration)?;
1142 let scenario =
1143 LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1144 let stages = scenario.generate_stages(duration_secs, self.vus);
1145
1146 let api_base_path = self.resolve_base_path(parser);
1148
1149 let mut all_headers = custom_headers.clone();
1151 if let Some(auth) = &self.auth {
1152 all_headers.insert("Authorization".to_string(), auth.clone());
1153 }
1154 let headers_json = serde_json::to_string(&all_headers).unwrap_or_else(|_| "{}".to_string());
1155
1156 let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
1158
1159 let flows_data: Vec<serde_json::Value> = flows.iter().map(|f| {
1160 let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
1161 serde_json::json!({
1162 "name": sanitized_name.clone(),
1163 "display_name": f.name,
1164 "base_path": f.base_path,
1165 "steps": f.steps.iter().enumerate().map(|(idx, s)| {
1166 let parts: Vec<&str> = s.operation.splitn(2, ' ').collect();
1168 let method_raw = if !parts.is_empty() {
1169 parts[0].to_uppercase()
1170 } else {
1171 "GET".to_string()
1172 };
1173 let method = if !parts.is_empty() {
1174 let m = parts[0].to_lowercase();
1175 if m == "delete" { "del".to_string() } else { m }
1177 } else {
1178 "get".to_string()
1179 };
1180 let raw_path = if parts.len() >= 2 { parts[1] } else { "/" };
1181 let path = if let Some(ref bp) = api_base_path {
1183 format!("{}{}", bp, raw_path)
1184 } else {
1185 raw_path.to_string()
1186 };
1187 let is_get_or_head = method == "get" || method == "head";
1188 let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1190
1191 let body_value = if has_body {
1193 param_overrides.as_ref()
1194 .map(|po| po.get_for_operation(None, &method_raw, &raw_path))
1195 .and_then(|oo| oo.body)
1196 .unwrap_or_else(|| serde_json::json!({}))
1197 } else {
1198 serde_json::json!({})
1199 };
1200
1201 let processed_body = DynamicParamProcessor::process_json_body(&body_value);
1203
1204 let body_has_extracted_placeholders = processed_body.value.contains("${extracted.");
1206 let body_is_dynamic = processed_body.is_dynamic || body_has_extracted_placeholders;
1207
1208 serde_json::json!({
1209 "operation": s.operation,
1210 "method": method,
1211 "path": path,
1212 "extract": s.extract,
1213 "use_values": s.use_values,
1214 "use_body": s.use_body,
1215 "merge_body": if s.merge_body.is_empty() { None } else { Some(&s.merge_body) },
1216 "inject_attacks": s.inject_attacks,
1217 "attack_types": s.attack_types,
1218 "description": s.description,
1219 "display_name": s.description.clone().unwrap_or_else(|| format!("Step {}", idx)),
1220 "is_get_or_head": is_get_or_head,
1221 "has_body": has_body,
1222 "body": processed_body.value,
1223 "body_is_dynamic": body_is_dynamic,
1224 "_placeholders": processed_body.placeholders.iter().map(|p| format!("{:?}", p)).collect::<Vec<_>>(),
1225 })
1226 }).collect::<Vec<_>>(),
1227 })
1228 }).collect();
1229
1230 for flow_data in &flows_data {
1232 if let Some(steps) = flow_data.get("steps").and_then(|s| s.as_array()) {
1233 for step in steps {
1234 if let Some(placeholders_arr) =
1235 step.get("_placeholders").and_then(|p| p.as_array())
1236 {
1237 for p_str in placeholders_arr {
1238 if let Some(p_name) = p_str.as_str() {
1239 match p_name {
1240 "VU" => {
1241 all_placeholders.insert(DynamicPlaceholder::VU);
1242 }
1243 "Iteration" => {
1244 all_placeholders.insert(DynamicPlaceholder::Iteration);
1245 }
1246 "Timestamp" => {
1247 all_placeholders.insert(DynamicPlaceholder::Timestamp);
1248 }
1249 "UUID" => {
1250 all_placeholders.insert(DynamicPlaceholder::UUID);
1251 }
1252 "Random" => {
1253 all_placeholders.insert(DynamicPlaceholder::Random);
1254 }
1255 "Counter" => {
1256 all_placeholders.insert(DynamicPlaceholder::Counter);
1257 }
1258 "Date" => {
1259 all_placeholders.insert(DynamicPlaceholder::Date);
1260 }
1261 "VuIter" => {
1262 all_placeholders.insert(DynamicPlaceholder::VuIter);
1263 }
1264 _ => {}
1265 }
1266 }
1267 }
1268 }
1269 }
1270 }
1271 }
1272
1273 let required_imports = DynamicParamProcessor::get_required_imports(&all_placeholders);
1275 let required_globals = DynamicParamProcessor::get_required_globals(&all_placeholders);
1276
1277 let security_testing_enabled = self.wafbench_dir.is_some() || self.security_test;
1279
1280 let data = serde_json::json!({
1281 "base_url": self.target,
1282 "flows": flows_data,
1283 "extract_fields": config.default_extract_fields,
1284 "duration_secs": duration_secs,
1285 "max_vus": self.vus,
1286 "auth_header": self.auth,
1287 "custom_headers": custom_headers,
1288 "skip_tls_verify": self.skip_tls_verify,
1289 "stages": stages.iter().map(|s| serde_json::json!({
1291 "duration": s.duration,
1292 "target": s.target,
1293 })).collect::<Vec<_>>(),
1294 "threshold_percentile": self.threshold_percentile,
1295 "threshold_ms": self.threshold_ms,
1296 "max_error_rate": self.max_error_rate,
1297 "headers": headers_json,
1298 "dynamic_imports": required_imports,
1299 "dynamic_globals": required_globals,
1300 "security_testing_enabled": security_testing_enabled,
1302 });
1303
1304 let mut script = handlebars
1305 .render_template(template, &data)
1306 .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
1307
1308 if security_testing_enabled {
1310 script = self.generate_enhanced_script(&script)?;
1311 }
1312
1313 let script_path =
1315 self.output.join(format!("k6-{}-crud-flow.js", spec_name.replace('.', "_")));
1316
1317 std::fs::create_dir_all(self.output.clone())?;
1318 std::fs::write(&script_path, &script)?;
1319
1320 if !self.generate_only {
1321 let executor = K6Executor::new()?;
1322 let output_dir = self.output.join(format!("{}_results", spec_name.replace('.', "_")));
1323 std::fs::create_dir_all(&output_dir)?;
1324
1325 executor.execute(&script_path, Some(&output_dir), self.verbose).await?;
1326 }
1327
1328 Ok(ExtractedValues::new())
1331 }
1332
1333 async fn execute_standard_spec(&self, parser: &SpecParser, spec_name: &str) -> Result<()> {
1335 let mut operations = if let Some(filter) = &self.operations {
1336 parser.filter_operations(filter)?
1337 } else {
1338 parser.get_operations()
1339 };
1340
1341 if let Some(exclude) = &self.exclude_operations {
1342 operations = parser.exclude_operations(operations, exclude)?;
1343 }
1344
1345 if operations.is_empty() {
1346 TerminalReporter::print_warning(&format!("No operations found in {}", spec_name));
1347 return Ok(());
1348 }
1349
1350 TerminalReporter::print_progress(&format!(
1351 " {} operations in {}",
1352 operations.len(),
1353 spec_name
1354 ));
1355
1356 let templates: Vec<_> = operations
1358 .iter()
1359 .map(RequestGenerator::generate_template)
1360 .collect::<Result<Vec<_>>>()?;
1361
1362 let custom_headers = self.parse_headers()?;
1364
1365 let base_path = self.resolve_base_path(parser);
1367
1368 let scenario =
1370 LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1371
1372 let security_testing_enabled = self.security_test || self.wafbench_dir.is_some();
1374
1375 let k6_config = K6Config {
1376 target_url: self.target.clone(),
1377 base_path,
1378 scenario,
1379 duration_secs: Self::parse_duration(&self.duration)?,
1380 max_vus: self.vus,
1381 threshold_percentile: self.threshold_percentile.clone(),
1382 threshold_ms: self.threshold_ms,
1383 max_error_rate: self.max_error_rate,
1384 auth_header: self.auth.clone(),
1385 custom_headers,
1386 skip_tls_verify: self.skip_tls_verify,
1387 security_testing_enabled,
1388 };
1389
1390 let generator = K6ScriptGenerator::new(k6_config, templates);
1391 let script = generator.generate()?;
1392
1393 let script_path = self.output.join(format!("k6-{}.js", spec_name.replace('.', "_")));
1395
1396 std::fs::create_dir_all(self.output.clone())?;
1397 std::fs::write(&script_path, &script)?;
1398
1399 if !self.generate_only {
1400 let executor = K6Executor::new()?;
1401 let output_dir = self.output.join(format!("{}_results", spec_name.replace('.', "_")));
1402 std::fs::create_dir_all(&output_dir)?;
1403
1404 executor.execute(&script_path, Some(&output_dir), self.verbose).await?;
1405 }
1406
1407 Ok(())
1408 }
1409
1410 async fn execute_crud_flow(&self, parser: &SpecParser) -> Result<()> {
1412 let config = self.build_crud_flow_config().unwrap_or_default();
1414
1415 let flows = if !config.flows.is_empty() {
1417 TerminalReporter::print_progress("Using custom flow configuration...");
1418 config.flows.clone()
1419 } else {
1420 TerminalReporter::print_progress("Detecting CRUD operations...");
1421 let operations = parser.get_operations();
1422 CrudFlowDetector::detect_flows(&operations)
1423 };
1424
1425 if flows.is_empty() {
1426 return Err(BenchError::Other(
1427 "No CRUD flows detected in spec. Ensure spec has POST/GET/PUT/DELETE operations on related paths.".to_string(),
1428 ));
1429 }
1430
1431 if config.flows.is_empty() {
1432 TerminalReporter::print_success(&format!("Detected {} CRUD flow(s)", flows.len()));
1433 } else {
1434 TerminalReporter::print_success(&format!("Loaded {} custom flow(s)", flows.len()));
1435 }
1436
1437 for flow in &flows {
1438 TerminalReporter::print_progress(&format!(
1439 " - {}: {} steps",
1440 flow.name,
1441 flow.steps.len()
1442 ));
1443 }
1444
1445 let mut handlebars = handlebars::Handlebars::new();
1447 handlebars.register_helper(
1449 "json",
1450 Box::new(
1451 |h: &handlebars::Helper,
1452 _: &handlebars::Handlebars,
1453 _: &handlebars::Context,
1454 _: &mut handlebars::RenderContext,
1455 out: &mut dyn handlebars::Output|
1456 -> handlebars::HelperResult {
1457 let param = h.param(0).map(|v| v.value()).unwrap_or(&serde_json::Value::Null);
1458 out.write(&serde_json::to_string(param).unwrap_or_else(|_| "[]".to_string()))?;
1459 Ok(())
1460 },
1461 ),
1462 );
1463 let template = include_str!("templates/k6_crud_flow.hbs");
1464
1465 let custom_headers = self.parse_headers()?;
1466
1467 let param_overrides = if let Some(params_file) = &self.params_file {
1469 TerminalReporter::print_progress("Loading parameter overrides...");
1470 let overrides = ParameterOverrides::from_file(params_file)?;
1471 TerminalReporter::print_success(&format!(
1472 "Loaded parameter overrides ({} operation-specific, {} defaults)",
1473 overrides.operations.len(),
1474 if overrides.defaults.is_empty() { 0 } else { 1 }
1475 ));
1476 Some(overrides)
1477 } else {
1478 None
1479 };
1480
1481 let duration_secs = Self::parse_duration(&self.duration)?;
1483 let scenario =
1484 LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1485 let stages = scenario.generate_stages(duration_secs, self.vus);
1486
1487 let api_base_path = self.resolve_base_path(parser);
1489 if let Some(ref bp) = api_base_path {
1490 TerminalReporter::print_progress(&format!("Using base path: {}", bp));
1491 }
1492
1493 let mut all_headers = custom_headers.clone();
1495 if let Some(auth) = &self.auth {
1496 all_headers.insert("Authorization".to_string(), auth.clone());
1497 }
1498 let headers_json = serde_json::to_string(&all_headers).unwrap_or_else(|_| "{}".to_string());
1499
1500 let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
1502
1503 let flows_data: Vec<serde_json::Value> = flows.iter().map(|f| {
1504 let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
1506 serde_json::json!({
1507 "name": sanitized_name.clone(), "display_name": f.name, "base_path": f.base_path,
1510 "steps": f.steps.iter().enumerate().map(|(idx, s)| {
1511 let parts: Vec<&str> = s.operation.splitn(2, ' ').collect();
1513 let method_raw = if !parts.is_empty() {
1514 parts[0].to_uppercase()
1515 } else {
1516 "GET".to_string()
1517 };
1518 let method = if !parts.is_empty() {
1519 let m = parts[0].to_lowercase();
1520 if m == "delete" { "del".to_string() } else { m }
1522 } else {
1523 "get".to_string()
1524 };
1525 let raw_path = if parts.len() >= 2 { parts[1] } else { "/" };
1526 let path = if let Some(ref bp) = api_base_path {
1528 format!("{}{}", bp, raw_path)
1529 } else {
1530 raw_path.to_string()
1531 };
1532 let is_get_or_head = method == "get" || method == "head";
1533 let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1535
1536 let body_value = if has_body {
1538 param_overrides.as_ref()
1539 .map(|po| po.get_for_operation(None, &method_raw, raw_path))
1540 .and_then(|oo| oo.body)
1541 .unwrap_or_else(|| serde_json::json!({}))
1542 } else {
1543 serde_json::json!({})
1544 };
1545
1546 let processed_body = DynamicParamProcessor::process_json_body(&body_value);
1548 let body_has_extracted_placeholders = processed_body.value.contains("${extracted.");
1553 let body_is_dynamic = processed_body.is_dynamic || body_has_extracted_placeholders;
1554
1555 serde_json::json!({
1556 "operation": s.operation,
1557 "method": method,
1558 "path": path,
1559 "extract": s.extract,
1560 "use_values": s.use_values,
1561 "use_body": s.use_body,
1562 "merge_body": if s.merge_body.is_empty() { None } else { Some(&s.merge_body) },
1563 "inject_attacks": s.inject_attacks,
1564 "attack_types": s.attack_types,
1565 "description": s.description,
1566 "display_name": s.description.clone().unwrap_or_else(|| format!("Step {}", idx)),
1567 "is_get_or_head": is_get_or_head,
1568 "has_body": has_body,
1569 "body": processed_body.value,
1570 "body_is_dynamic": body_is_dynamic,
1571 "_placeholders": processed_body.placeholders.iter().map(|p| format!("{:?}", p)).collect::<Vec<_>>(),
1572 })
1573 }).collect::<Vec<_>>(),
1574 })
1575 }).collect();
1576
1577 for flow_data in &flows_data {
1579 if let Some(steps) = flow_data.get("steps").and_then(|s| s.as_array()) {
1580 for step in steps {
1581 if let Some(placeholders_arr) =
1582 step.get("_placeholders").and_then(|p| p.as_array())
1583 {
1584 for p_str in placeholders_arr {
1585 if let Some(p_name) = p_str.as_str() {
1586 match p_name {
1588 "VU" => {
1589 all_placeholders.insert(DynamicPlaceholder::VU);
1590 }
1591 "Iteration" => {
1592 all_placeholders.insert(DynamicPlaceholder::Iteration);
1593 }
1594 "Timestamp" => {
1595 all_placeholders.insert(DynamicPlaceholder::Timestamp);
1596 }
1597 "UUID" => {
1598 all_placeholders.insert(DynamicPlaceholder::UUID);
1599 }
1600 "Random" => {
1601 all_placeholders.insert(DynamicPlaceholder::Random);
1602 }
1603 "Counter" => {
1604 all_placeholders.insert(DynamicPlaceholder::Counter);
1605 }
1606 "Date" => {
1607 all_placeholders.insert(DynamicPlaceholder::Date);
1608 }
1609 "VuIter" => {
1610 all_placeholders.insert(DynamicPlaceholder::VuIter);
1611 }
1612 _ => {}
1613 }
1614 }
1615 }
1616 }
1617 }
1618 }
1619 }
1620
1621 let required_imports = DynamicParamProcessor::get_required_imports(&all_placeholders);
1623 let required_globals = DynamicParamProcessor::get_required_globals(&all_placeholders);
1624
1625 let invalid_data_config = self.build_invalid_data_config();
1627 let error_injection_enabled = invalid_data_config.is_some();
1628 let error_rate = self.error_rate.unwrap_or(0.0);
1629 let error_types: Vec<String> = invalid_data_config
1630 .as_ref()
1631 .map(|c| c.error_types.iter().map(|t| format!("{:?}", t)).collect())
1632 .unwrap_or_default();
1633
1634 if error_injection_enabled {
1635 TerminalReporter::print_progress(&format!(
1636 "Error injection enabled ({}% rate)",
1637 (error_rate * 100.0) as u32
1638 ));
1639 }
1640
1641 let security_testing_enabled = self.wafbench_dir.is_some() || self.security_test;
1643
1644 let data = serde_json::json!({
1645 "base_url": self.target,
1646 "flows": flows_data,
1647 "extract_fields": config.default_extract_fields,
1648 "duration_secs": duration_secs,
1649 "max_vus": self.vus,
1650 "auth_header": self.auth,
1651 "custom_headers": custom_headers,
1652 "skip_tls_verify": self.skip_tls_verify,
1653 "stages": stages.iter().map(|s| serde_json::json!({
1655 "duration": s.duration,
1656 "target": s.target,
1657 })).collect::<Vec<_>>(),
1658 "threshold_percentile": self.threshold_percentile,
1659 "threshold_ms": self.threshold_ms,
1660 "max_error_rate": self.max_error_rate,
1661 "headers": headers_json,
1662 "dynamic_imports": required_imports,
1663 "dynamic_globals": required_globals,
1664 "error_injection_enabled": error_injection_enabled,
1666 "error_rate": error_rate,
1667 "error_types": error_types,
1668 "security_testing_enabled": security_testing_enabled,
1670 });
1671
1672 let mut script = handlebars
1673 .render_template(template, &data)
1674 .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
1675
1676 if security_testing_enabled {
1678 script = self.generate_enhanced_script(&script)?;
1679 }
1680
1681 TerminalReporter::print_progress("Validating CRUD flow script...");
1683 let validation_errors = K6ScriptGenerator::validate_script(&script);
1684 if !validation_errors.is_empty() {
1685 TerminalReporter::print_error("CRUD flow script validation failed");
1686 for error in &validation_errors {
1687 eprintln!(" {}", error);
1688 }
1689 return Err(BenchError::Other(format!(
1690 "CRUD flow script validation failed with {} error(s)",
1691 validation_errors.len()
1692 )));
1693 }
1694
1695 TerminalReporter::print_success("CRUD flow script generated");
1696
1697 let script_path = if let Some(output) = &self.script_output {
1699 output.clone()
1700 } else {
1701 self.output.join("k6-crud-flow-script.js")
1702 };
1703
1704 if let Some(parent) = script_path.parent() {
1705 std::fs::create_dir_all(parent)?;
1706 }
1707 std::fs::write(&script_path, &script)?;
1708 TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
1709
1710 if self.generate_only {
1711 println!("\nScript generated successfully. Run it with:");
1712 println!(" k6 run {}", script_path.display());
1713 return Ok(());
1714 }
1715
1716 TerminalReporter::print_progress("Executing CRUD flow test...");
1718 let executor = K6Executor::new()?;
1719 std::fs::create_dir_all(&self.output)?;
1720
1721 let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
1722
1723 let duration_secs = Self::parse_duration(&self.duration)?;
1724 TerminalReporter::print_summary(&results, duration_secs);
1725
1726 Ok(())
1727 }
1728
1729 async fn execute_owasp_test(&self, parser: &SpecParser) -> Result<()> {
1731 TerminalReporter::print_progress("OWASP API Security Top 10 Testing Mode");
1732
1733 let custom_headers = self.parse_headers()?;
1735
1736 let mut config = OwaspApiConfig::new()
1738 .with_auth_header(&self.owasp_auth_header)
1739 .with_verbose(self.verbose)
1740 .with_insecure(self.skip_tls_verify)
1741 .with_concurrency(self.vus as usize)
1742 .with_iterations(self.owasp_iterations as usize)
1743 .with_base_path(self.base_path.clone())
1744 .with_custom_headers(custom_headers);
1745
1746 if let Some(ref token) = self.owasp_auth_token {
1748 config = config.with_valid_auth_token(token);
1749 }
1750
1751 if let Some(ref cats_str) = self.owasp_categories {
1753 let categories: Vec<OwaspCategory> = cats_str
1754 .split(',')
1755 .filter_map(|s| {
1756 let trimmed = s.trim();
1757 match trimmed.parse::<OwaspCategory>() {
1758 Ok(cat) => Some(cat),
1759 Err(e) => {
1760 TerminalReporter::print_warning(&e);
1761 None
1762 }
1763 }
1764 })
1765 .collect();
1766
1767 if !categories.is_empty() {
1768 config = config.with_categories(categories);
1769 }
1770 }
1771
1772 if let Some(ref admin_paths_file) = self.owasp_admin_paths {
1774 config.admin_paths_file = Some(admin_paths_file.clone());
1775 if let Err(e) = config.load_admin_paths() {
1776 TerminalReporter::print_warning(&format!("Failed to load admin paths file: {}", e));
1777 }
1778 }
1779
1780 if let Some(ref id_fields_str) = self.owasp_id_fields {
1782 let id_fields: Vec<String> = id_fields_str
1783 .split(',')
1784 .map(|s| s.trim().to_string())
1785 .filter(|s| !s.is_empty())
1786 .collect();
1787 if !id_fields.is_empty() {
1788 config = config.with_id_fields(id_fields);
1789 }
1790 }
1791
1792 if let Some(ref report_path) = self.owasp_report {
1794 config = config.with_report_path(report_path);
1795 }
1796 if let Ok(format) = self.owasp_report_format.parse::<ReportFormat>() {
1797 config = config.with_report_format(format);
1798 }
1799
1800 let categories = config.categories_to_test();
1802 TerminalReporter::print_success(&format!(
1803 "Testing {} OWASP categories: {}",
1804 categories.len(),
1805 categories.iter().map(|c| c.cli_name()).collect::<Vec<_>>().join(", ")
1806 ));
1807
1808 if config.valid_auth_token.is_some() {
1809 TerminalReporter::print_progress("Using provided auth token for baseline requests");
1810 }
1811
1812 TerminalReporter::print_progress("Generating OWASP security test script...");
1814 let generator = OwaspApiGenerator::new(config, self.target.clone(), parser);
1815
1816 let script = generator.generate()?;
1818 TerminalReporter::print_success("OWASP security test script generated");
1819
1820 let script_path = if let Some(output) = &self.script_output {
1822 output.clone()
1823 } else {
1824 self.output.join("k6-owasp-security-test.js")
1825 };
1826
1827 if let Some(parent) = script_path.parent() {
1828 std::fs::create_dir_all(parent)?;
1829 }
1830 std::fs::write(&script_path, &script)?;
1831 TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
1832
1833 if self.generate_only {
1835 println!("\nOWASP security test script generated. Run it with:");
1836 println!(" k6 run {}", script_path.display());
1837 return Ok(());
1838 }
1839
1840 TerminalReporter::print_progress("Executing OWASP security tests...");
1842 let executor = K6Executor::new()?;
1843 std::fs::create_dir_all(&self.output)?;
1844
1845 let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
1846
1847 let duration_secs = Self::parse_duration(&self.duration)?;
1848 TerminalReporter::print_summary(&results, duration_secs);
1849
1850 println!("\nOWASP security test results saved to: {}", self.output.display());
1851
1852 Ok(())
1853 }
1854}
1855
1856#[cfg(test)]
1857mod tests {
1858 use super::*;
1859
1860 #[test]
1861 fn test_parse_duration() {
1862 assert_eq!(BenchCommand::parse_duration("30s").unwrap(), 30);
1863 assert_eq!(BenchCommand::parse_duration("5m").unwrap(), 300);
1864 assert_eq!(BenchCommand::parse_duration("1h").unwrap(), 3600);
1865 assert_eq!(BenchCommand::parse_duration("60").unwrap(), 60);
1866 }
1867
1868 #[test]
1869 fn test_parse_duration_invalid() {
1870 assert!(BenchCommand::parse_duration("invalid").is_err());
1871 assert!(BenchCommand::parse_duration("30x").is_err());
1872 }
1873
1874 #[test]
1875 fn test_parse_headers() {
1876 let cmd = BenchCommand {
1877 spec: vec![PathBuf::from("test.yaml")],
1878 spec_dir: None,
1879 merge_conflicts: "error".to_string(),
1880 spec_mode: "merge".to_string(),
1881 dependency_config: None,
1882 target: "http://localhost".to_string(),
1883 base_path: None,
1884 duration: "1m".to_string(),
1885 vus: 10,
1886 scenario: "ramp-up".to_string(),
1887 operations: None,
1888 exclude_operations: None,
1889 auth: None,
1890 headers: Some("X-API-Key:test123,X-Client-ID:client456".to_string()),
1891 output: PathBuf::from("output"),
1892 generate_only: false,
1893 script_output: None,
1894 threshold_percentile: "p(95)".to_string(),
1895 threshold_ms: 500,
1896 max_error_rate: 0.05,
1897 verbose: false,
1898 skip_tls_verify: false,
1899 targets_file: None,
1900 max_concurrency: None,
1901 results_format: "both".to_string(),
1902 params_file: None,
1903 crud_flow: false,
1904 flow_config: None,
1905 extract_fields: None,
1906 parallel_create: None,
1907 data_file: None,
1908 data_distribution: "unique-per-vu".to_string(),
1909 data_mappings: None,
1910 per_uri_control: false,
1911 error_rate: None,
1912 error_types: None,
1913 security_test: false,
1914 security_payloads: None,
1915 security_categories: None,
1916 security_target_fields: None,
1917 wafbench_dir: None,
1918 wafbench_cycle_all: false,
1919 owasp_api_top10: false,
1920 owasp_categories: None,
1921 owasp_auth_header: "Authorization".to_string(),
1922 owasp_auth_token: None,
1923 owasp_admin_paths: None,
1924 owasp_id_fields: None,
1925 owasp_report: None,
1926 owasp_report_format: "json".to_string(),
1927 owasp_iterations: 1,
1928 };
1929
1930 let headers = cmd.parse_headers().unwrap();
1931 assert_eq!(headers.get("X-API-Key"), Some(&"test123".to_string()));
1932 assert_eq!(headers.get("X-Client-ID"), Some(&"client456".to_string()));
1933 }
1934
1935 #[test]
1936 fn test_get_spec_display_name() {
1937 let cmd = BenchCommand {
1938 spec: vec![PathBuf::from("test.yaml")],
1939 spec_dir: None,
1940 merge_conflicts: "error".to_string(),
1941 spec_mode: "merge".to_string(),
1942 dependency_config: None,
1943 target: "http://localhost".to_string(),
1944 base_path: None,
1945 duration: "1m".to_string(),
1946 vus: 10,
1947 scenario: "ramp-up".to_string(),
1948 operations: None,
1949 exclude_operations: None,
1950 auth: None,
1951 headers: None,
1952 output: PathBuf::from("output"),
1953 generate_only: false,
1954 script_output: None,
1955 threshold_percentile: "p(95)".to_string(),
1956 threshold_ms: 500,
1957 max_error_rate: 0.05,
1958 verbose: false,
1959 skip_tls_verify: false,
1960 targets_file: None,
1961 max_concurrency: None,
1962 results_format: "both".to_string(),
1963 params_file: None,
1964 crud_flow: false,
1965 flow_config: None,
1966 extract_fields: None,
1967 parallel_create: None,
1968 data_file: None,
1969 data_distribution: "unique-per-vu".to_string(),
1970 data_mappings: None,
1971 per_uri_control: false,
1972 error_rate: None,
1973 error_types: None,
1974 security_test: false,
1975 security_payloads: None,
1976 security_categories: None,
1977 security_target_fields: None,
1978 wafbench_dir: None,
1979 wafbench_cycle_all: false,
1980 owasp_api_top10: false,
1981 owasp_categories: None,
1982 owasp_auth_header: "Authorization".to_string(),
1983 owasp_auth_token: None,
1984 owasp_admin_paths: None,
1985 owasp_id_fields: None,
1986 owasp_report: None,
1987 owasp_report_format: "json".to_string(),
1988 owasp_iterations: 1,
1989 };
1990
1991 assert_eq!(cmd.get_spec_display_name(), "test.yaml");
1992
1993 let cmd_multi = BenchCommand {
1995 spec: vec![PathBuf::from("a.yaml"), PathBuf::from("b.yaml")],
1996 spec_dir: None,
1997 merge_conflicts: "error".to_string(),
1998 spec_mode: "merge".to_string(),
1999 dependency_config: None,
2000 target: "http://localhost".to_string(),
2001 base_path: None,
2002 duration: "1m".to_string(),
2003 vus: 10,
2004 scenario: "ramp-up".to_string(),
2005 operations: None,
2006 exclude_operations: None,
2007 auth: None,
2008 headers: None,
2009 output: PathBuf::from("output"),
2010 generate_only: false,
2011 script_output: None,
2012 threshold_percentile: "p(95)".to_string(),
2013 threshold_ms: 500,
2014 max_error_rate: 0.05,
2015 verbose: false,
2016 skip_tls_verify: false,
2017 targets_file: None,
2018 max_concurrency: None,
2019 results_format: "both".to_string(),
2020 params_file: None,
2021 crud_flow: false,
2022 flow_config: None,
2023 extract_fields: None,
2024 parallel_create: None,
2025 data_file: None,
2026 data_distribution: "unique-per-vu".to_string(),
2027 data_mappings: None,
2028 per_uri_control: false,
2029 error_rate: None,
2030 error_types: None,
2031 security_test: false,
2032 security_payloads: None,
2033 security_categories: None,
2034 security_target_fields: None,
2035 wafbench_dir: None,
2036 wafbench_cycle_all: false,
2037 owasp_api_top10: false,
2038 owasp_categories: None,
2039 owasp_auth_header: "Authorization".to_string(),
2040 owasp_auth_token: None,
2041 owasp_admin_paths: None,
2042 owasp_id_fields: None,
2043 owasp_report: None,
2044 owasp_report_format: "json".to_string(),
2045 owasp_iterations: 1,
2046 };
2047
2048 assert_eq!(cmd_multi.get_spec_display_name(), "2 spec files");
2049 }
2050}