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();
872
873 if !wafbench_payloads.is_empty() {
874 TerminalReporter::print_progress(&format!(
876 "Loading {} WAFBench attack patterns...",
877 wafbench_payloads.len()
878 ));
879 payload_list.extend(wafbench_payloads);
880 } else if let Some(ref config) = security_config {
881 payload_list.extend(SecurityPayloads::get_payloads(config));
883 }
884
885 let target_fields =
886 security_config.as_ref().map(|c| c.target_fields.clone()).unwrap_or_default();
887
888 additional_code.push_str(&SecurityTestGenerator::generate_payload_selection(
889 &payload_list,
890 self.wafbench_cycle_all,
891 ));
892 additional_code.push('\n');
893 additional_code
894 .push_str(&SecurityTestGenerator::generate_apply_payload(&target_fields));
895 additional_code.push('\n');
896 additional_code.push_str(&SecurityTestGenerator::generate_security_checks());
897 additional_code.push('\n');
898
899 let mode = if self.wafbench_cycle_all {
900 "cycle-all"
901 } else {
902 "random"
903 };
904 TerminalReporter::print_success(&format!(
905 "Security testing enabled ({} payloads, {} mode)",
906 payload_list.len(),
907 mode
908 ));
909 }
910
911 if let Some(config) = self.build_parallel_config() {
913 TerminalReporter::print_progress("Adding parallel execution support...");
914 additional_code.push_str(&ParallelRequestGenerator::generate_batch_helper(&config));
915 additional_code.push('\n');
916 TerminalReporter::print_success(&format!(
917 "Parallel execution enabled (count: {})",
918 config.count
919 ));
920 }
921
922 if !additional_code.is_empty() {
924 if let Some(import_end) = enhanced_script.find("export const options") {
926 enhanced_script.insert_str(
927 import_end,
928 &format!("\n// === Advanced Testing Features ===\n{}\n", additional_code),
929 );
930 }
931 }
932
933 Ok(enhanced_script)
934 }
935
936 async fn execute_sequential_specs(&self) -> Result<()> {
938 TerminalReporter::print_progress("Sequential spec mode: Loading specs individually...");
939
940 let mut all_specs: Vec<(PathBuf, OpenApiSpec)> = Vec::new();
942
943 if !self.spec.is_empty() {
944 let specs = load_specs_from_files(self.spec.clone())
945 .await
946 .map_err(|e| BenchError::Other(format!("Failed to load spec files: {}", e)))?;
947 all_specs.extend(specs);
948 }
949
950 if let Some(spec_dir) = &self.spec_dir {
951 let dir_specs = load_specs_from_directory(spec_dir).await.map_err(|e| {
952 BenchError::Other(format!("Failed to load specs from directory: {}", e))
953 })?;
954 all_specs.extend(dir_specs);
955 }
956
957 if all_specs.is_empty() {
958 return Err(BenchError::Other(
959 "No spec files found for sequential execution".to_string(),
960 ));
961 }
962
963 TerminalReporter::print_success(&format!("Loaded {} spec(s)", all_specs.len()));
964
965 let execution_order = if let Some(config_path) = &self.dependency_config {
967 TerminalReporter::print_progress("Loading dependency configuration...");
968 let config = SpecDependencyConfig::from_file(config_path)?;
969
970 if !config.disable_auto_detect && config.execution_order.is_empty() {
971 self.detect_and_sort_specs(&all_specs)?
973 } else {
974 config.execution_order.iter().flat_map(|g| g.specs.clone()).collect()
976 }
977 } else {
978 self.detect_and_sort_specs(&all_specs)?
980 };
981
982 TerminalReporter::print_success(&format!(
983 "Execution order: {}",
984 execution_order
985 .iter()
986 .map(|p| p.file_name().unwrap_or_default().to_string_lossy().to_string())
987 .collect::<Vec<_>>()
988 .join(" → ")
989 ));
990
991 let mut extracted_values = ExtractedValues::new();
993 let total_specs = execution_order.len();
994
995 for (index, spec_path) in execution_order.iter().enumerate() {
996 let spec_name = spec_path.file_name().unwrap_or_default().to_string_lossy().to_string();
997
998 TerminalReporter::print_progress(&format!(
999 "[{}/{}] Executing spec: {}",
1000 index + 1,
1001 total_specs,
1002 spec_name
1003 ));
1004
1005 let spec = all_specs
1007 .iter()
1008 .find(|(p, _)| {
1009 p == spec_path
1010 || p.file_name() == spec_path.file_name()
1011 || p.file_name() == Some(spec_path.as_os_str())
1012 })
1013 .map(|(_, s)| s.clone())
1014 .ok_or_else(|| {
1015 BenchError::Other(format!("Spec not found: {}", spec_path.display()))
1016 })?;
1017
1018 let new_values = self.execute_single_spec(&spec, &spec_name, &extracted_values).await?;
1020
1021 extracted_values.merge(&new_values);
1023
1024 TerminalReporter::print_success(&format!(
1025 "[{}/{}] Completed: {} (extracted {} values)",
1026 index + 1,
1027 total_specs,
1028 spec_name,
1029 new_values.values.len()
1030 ));
1031 }
1032
1033 TerminalReporter::print_success(&format!(
1034 "Sequential execution complete: {} specs executed",
1035 total_specs
1036 ));
1037
1038 Ok(())
1039 }
1040
1041 fn detect_and_sort_specs(&self, specs: &[(PathBuf, OpenApiSpec)]) -> Result<Vec<PathBuf>> {
1043 TerminalReporter::print_progress("Auto-detecting spec dependencies...");
1044
1045 let mut detector = DependencyDetector::new();
1046 let dependencies = detector.detect_dependencies(specs);
1047
1048 if dependencies.is_empty() {
1049 TerminalReporter::print_progress("No dependencies detected, using file order");
1050 return Ok(specs.iter().map(|(p, _)| p.clone()).collect());
1051 }
1052
1053 TerminalReporter::print_progress(&format!(
1054 "Detected {} cross-spec dependencies",
1055 dependencies.len()
1056 ));
1057
1058 for dep in &dependencies {
1059 TerminalReporter::print_progress(&format!(
1060 " {} → {} (via field '{}')",
1061 dep.dependency_spec.file_name().unwrap_or_default().to_string_lossy(),
1062 dep.dependent_spec.file_name().unwrap_or_default().to_string_lossy(),
1063 dep.field_name
1064 ));
1065 }
1066
1067 topological_sort(specs, &dependencies)
1068 }
1069
1070 async fn execute_single_spec(
1072 &self,
1073 spec: &OpenApiSpec,
1074 spec_name: &str,
1075 _external_values: &ExtractedValues,
1076 ) -> Result<ExtractedValues> {
1077 let parser = SpecParser::from_spec(spec.clone());
1078
1079 if self.crud_flow {
1081 self.execute_crud_flow_with_extraction(&parser, spec_name).await
1083 } else {
1084 self.execute_standard_spec(&parser, spec_name).await?;
1086 Ok(ExtractedValues::new())
1087 }
1088 }
1089
1090 async fn execute_crud_flow_with_extraction(
1092 &self,
1093 parser: &SpecParser,
1094 spec_name: &str,
1095 ) -> Result<ExtractedValues> {
1096 let operations = parser.get_operations();
1097 let flows = CrudFlowDetector::detect_flows(&operations);
1098
1099 if flows.is_empty() {
1100 TerminalReporter::print_warning(&format!("No CRUD flows detected in {}", spec_name));
1101 return Ok(ExtractedValues::new());
1102 }
1103
1104 TerminalReporter::print_progress(&format!(
1105 " {} CRUD flow(s) in {}",
1106 flows.len(),
1107 spec_name
1108 ));
1109
1110 let mut handlebars = handlebars::Handlebars::new();
1112 handlebars.register_helper(
1114 "json",
1115 Box::new(
1116 |h: &handlebars::Helper,
1117 _: &handlebars::Handlebars,
1118 _: &handlebars::Context,
1119 _: &mut handlebars::RenderContext,
1120 out: &mut dyn handlebars::Output|
1121 -> handlebars::HelperResult {
1122 let param = h.param(0).map(|v| v.value()).unwrap_or(&serde_json::Value::Null);
1123 out.write(&serde_json::to_string(param).unwrap_or_else(|_| "[]".to_string()))?;
1124 Ok(())
1125 },
1126 ),
1127 );
1128 let template = include_str!("templates/k6_crud_flow.hbs");
1129
1130 let custom_headers = self.parse_headers()?;
1131 let config = self.build_crud_flow_config().unwrap_or_default();
1132
1133 let param_overrides = if let Some(params_file) = &self.params_file {
1135 let overrides = ParameterOverrides::from_file(params_file)?;
1136 Some(overrides)
1137 } else {
1138 None
1139 };
1140
1141 let duration_secs = Self::parse_duration(&self.duration)?;
1143 let scenario =
1144 LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1145 let stages = scenario.generate_stages(duration_secs, self.vus);
1146
1147 let api_base_path = self.resolve_base_path(parser);
1149
1150 let mut all_headers = custom_headers.clone();
1152 if let Some(auth) = &self.auth {
1153 all_headers.insert("Authorization".to_string(), auth.clone());
1154 }
1155 let headers_json = serde_json::to_string(&all_headers).unwrap_or_else(|_| "{}".to_string());
1156
1157 let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
1159
1160 let flows_data: Vec<serde_json::Value> = flows.iter().map(|f| {
1161 let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
1162 serde_json::json!({
1163 "name": sanitized_name.clone(),
1164 "display_name": f.name,
1165 "base_path": f.base_path,
1166 "steps": f.steps.iter().enumerate().map(|(idx, s)| {
1167 let parts: Vec<&str> = s.operation.splitn(2, ' ').collect();
1169 let method_raw = if !parts.is_empty() {
1170 parts[0].to_uppercase()
1171 } else {
1172 "GET".to_string()
1173 };
1174 let method = if !parts.is_empty() {
1175 let m = parts[0].to_lowercase();
1176 if m == "delete" { "del".to_string() } else { m }
1178 } else {
1179 "get".to_string()
1180 };
1181 let raw_path = if parts.len() >= 2 { parts[1] } else { "/" };
1182 let path = if let Some(ref bp) = api_base_path {
1184 format!("{}{}", bp, raw_path)
1185 } else {
1186 raw_path.to_string()
1187 };
1188 let is_get_or_head = method == "get" || method == "head";
1189 let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1191
1192 let body_value = if has_body {
1194 param_overrides.as_ref()
1195 .map(|po| po.get_for_operation(None, &method_raw, &raw_path))
1196 .and_then(|oo| oo.body)
1197 .unwrap_or_else(|| serde_json::json!({}))
1198 } else {
1199 serde_json::json!({})
1200 };
1201
1202 let processed_body = DynamicParamProcessor::process_json_body(&body_value);
1204
1205 let body_has_extracted_placeholders = processed_body.value.contains("${extracted.");
1207 let body_is_dynamic = processed_body.is_dynamic || body_has_extracted_placeholders;
1208
1209 serde_json::json!({
1210 "operation": s.operation,
1211 "method": method,
1212 "path": path,
1213 "extract": s.extract,
1214 "use_values": s.use_values,
1215 "use_body": s.use_body,
1216 "merge_body": if s.merge_body.is_empty() { None } else { Some(&s.merge_body) },
1217 "inject_attacks": s.inject_attacks,
1218 "attack_types": s.attack_types,
1219 "description": s.description,
1220 "display_name": s.description.clone().unwrap_or_else(|| format!("Step {}", idx)),
1221 "is_get_or_head": is_get_or_head,
1222 "has_body": has_body,
1223 "body": processed_body.value,
1224 "body_is_dynamic": body_is_dynamic,
1225 "_placeholders": processed_body.placeholders.iter().map(|p| format!("{:?}", p)).collect::<Vec<_>>(),
1226 })
1227 }).collect::<Vec<_>>(),
1228 })
1229 }).collect();
1230
1231 for flow_data in &flows_data {
1233 if let Some(steps) = flow_data.get("steps").and_then(|s| s.as_array()) {
1234 for step in steps {
1235 if let Some(placeholders_arr) =
1236 step.get("_placeholders").and_then(|p| p.as_array())
1237 {
1238 for p_str in placeholders_arr {
1239 if let Some(p_name) = p_str.as_str() {
1240 match p_name {
1241 "VU" => {
1242 all_placeholders.insert(DynamicPlaceholder::VU);
1243 }
1244 "Iteration" => {
1245 all_placeholders.insert(DynamicPlaceholder::Iteration);
1246 }
1247 "Timestamp" => {
1248 all_placeholders.insert(DynamicPlaceholder::Timestamp);
1249 }
1250 "UUID" => {
1251 all_placeholders.insert(DynamicPlaceholder::UUID);
1252 }
1253 "Random" => {
1254 all_placeholders.insert(DynamicPlaceholder::Random);
1255 }
1256 "Counter" => {
1257 all_placeholders.insert(DynamicPlaceholder::Counter);
1258 }
1259 "Date" => {
1260 all_placeholders.insert(DynamicPlaceholder::Date);
1261 }
1262 "VuIter" => {
1263 all_placeholders.insert(DynamicPlaceholder::VuIter);
1264 }
1265 _ => {}
1266 }
1267 }
1268 }
1269 }
1270 }
1271 }
1272 }
1273
1274 let required_imports = DynamicParamProcessor::get_required_imports(&all_placeholders);
1276 let required_globals = DynamicParamProcessor::get_required_globals(&all_placeholders);
1277
1278 let security_testing_enabled = self.wafbench_dir.is_some() || self.security_test;
1280
1281 let data = serde_json::json!({
1282 "base_url": self.target,
1283 "flows": flows_data,
1284 "extract_fields": config.default_extract_fields,
1285 "duration_secs": duration_secs,
1286 "max_vus": self.vus,
1287 "auth_header": self.auth,
1288 "custom_headers": custom_headers,
1289 "skip_tls_verify": self.skip_tls_verify,
1290 "stages": stages.iter().map(|s| serde_json::json!({
1292 "duration": s.duration,
1293 "target": s.target,
1294 })).collect::<Vec<_>>(),
1295 "threshold_percentile": self.threshold_percentile,
1296 "threshold_ms": self.threshold_ms,
1297 "max_error_rate": self.max_error_rate,
1298 "headers": headers_json,
1299 "dynamic_imports": required_imports,
1300 "dynamic_globals": required_globals,
1301 "security_testing_enabled": security_testing_enabled,
1303 });
1304
1305 let mut script = handlebars
1306 .render_template(template, &data)
1307 .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
1308
1309 if security_testing_enabled {
1311 script = self.generate_enhanced_script(&script)?;
1312 }
1313
1314 let script_path =
1316 self.output.join(format!("k6-{}-crud-flow.js", spec_name.replace('.', "_")));
1317
1318 std::fs::create_dir_all(self.output.clone())?;
1319 std::fs::write(&script_path, &script)?;
1320
1321 if !self.generate_only {
1322 let executor = K6Executor::new()?;
1323 let output_dir = self.output.join(format!("{}_results", spec_name.replace('.', "_")));
1324 std::fs::create_dir_all(&output_dir)?;
1325
1326 executor.execute(&script_path, Some(&output_dir), self.verbose).await?;
1327 }
1328
1329 Ok(ExtractedValues::new())
1332 }
1333
1334 async fn execute_standard_spec(&self, parser: &SpecParser, spec_name: &str) -> Result<()> {
1336 let mut operations = if let Some(filter) = &self.operations {
1337 parser.filter_operations(filter)?
1338 } else {
1339 parser.get_operations()
1340 };
1341
1342 if let Some(exclude) = &self.exclude_operations {
1343 operations = parser.exclude_operations(operations, exclude)?;
1344 }
1345
1346 if operations.is_empty() {
1347 TerminalReporter::print_warning(&format!("No operations found in {}", spec_name));
1348 return Ok(());
1349 }
1350
1351 TerminalReporter::print_progress(&format!(
1352 " {} operations in {}",
1353 operations.len(),
1354 spec_name
1355 ));
1356
1357 let templates: Vec<_> = operations
1359 .iter()
1360 .map(RequestGenerator::generate_template)
1361 .collect::<Result<Vec<_>>>()?;
1362
1363 let custom_headers = self.parse_headers()?;
1365
1366 let base_path = self.resolve_base_path(parser);
1368
1369 let scenario =
1371 LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1372
1373 let security_testing_enabled = self.security_test || self.wafbench_dir.is_some();
1375
1376 let k6_config = K6Config {
1377 target_url: self.target.clone(),
1378 base_path,
1379 scenario,
1380 duration_secs: Self::parse_duration(&self.duration)?,
1381 max_vus: self.vus,
1382 threshold_percentile: self.threshold_percentile.clone(),
1383 threshold_ms: self.threshold_ms,
1384 max_error_rate: self.max_error_rate,
1385 auth_header: self.auth.clone(),
1386 custom_headers,
1387 skip_tls_verify: self.skip_tls_verify,
1388 security_testing_enabled,
1389 };
1390
1391 let generator = K6ScriptGenerator::new(k6_config, templates);
1392 let script = generator.generate()?;
1393
1394 let script_path = self.output.join(format!("k6-{}.js", spec_name.replace('.', "_")));
1396
1397 std::fs::create_dir_all(self.output.clone())?;
1398 std::fs::write(&script_path, &script)?;
1399
1400 if !self.generate_only {
1401 let executor = K6Executor::new()?;
1402 let output_dir = self.output.join(format!("{}_results", spec_name.replace('.', "_")));
1403 std::fs::create_dir_all(&output_dir)?;
1404
1405 executor.execute(&script_path, Some(&output_dir), self.verbose).await?;
1406 }
1407
1408 Ok(())
1409 }
1410
1411 async fn execute_crud_flow(&self, parser: &SpecParser) -> Result<()> {
1413 let config = self.build_crud_flow_config().unwrap_or_default();
1415
1416 let flows = if !config.flows.is_empty() {
1418 TerminalReporter::print_progress("Using custom flow configuration...");
1419 config.flows.clone()
1420 } else {
1421 TerminalReporter::print_progress("Detecting CRUD operations...");
1422 let operations = parser.get_operations();
1423 CrudFlowDetector::detect_flows(&operations)
1424 };
1425
1426 if flows.is_empty() {
1427 return Err(BenchError::Other(
1428 "No CRUD flows detected in spec. Ensure spec has POST/GET/PUT/DELETE operations on related paths.".to_string(),
1429 ));
1430 }
1431
1432 if config.flows.is_empty() {
1433 TerminalReporter::print_success(&format!("Detected {} CRUD flow(s)", flows.len()));
1434 } else {
1435 TerminalReporter::print_success(&format!("Loaded {} custom flow(s)", flows.len()));
1436 }
1437
1438 for flow in &flows {
1439 TerminalReporter::print_progress(&format!(
1440 " - {}: {} steps",
1441 flow.name,
1442 flow.steps.len()
1443 ));
1444 }
1445
1446 let mut handlebars = handlebars::Handlebars::new();
1448 handlebars.register_helper(
1450 "json",
1451 Box::new(
1452 |h: &handlebars::Helper,
1453 _: &handlebars::Handlebars,
1454 _: &handlebars::Context,
1455 _: &mut handlebars::RenderContext,
1456 out: &mut dyn handlebars::Output|
1457 -> handlebars::HelperResult {
1458 let param = h.param(0).map(|v| v.value()).unwrap_or(&serde_json::Value::Null);
1459 out.write(&serde_json::to_string(param).unwrap_or_else(|_| "[]".to_string()))?;
1460 Ok(())
1461 },
1462 ),
1463 );
1464 let template = include_str!("templates/k6_crud_flow.hbs");
1465
1466 let custom_headers = self.parse_headers()?;
1467
1468 let param_overrides = if let Some(params_file) = &self.params_file {
1470 TerminalReporter::print_progress("Loading parameter overrides...");
1471 let overrides = ParameterOverrides::from_file(params_file)?;
1472 TerminalReporter::print_success(&format!(
1473 "Loaded parameter overrides ({} operation-specific, {} defaults)",
1474 overrides.operations.len(),
1475 if overrides.defaults.is_empty() { 0 } else { 1 }
1476 ));
1477 Some(overrides)
1478 } else {
1479 None
1480 };
1481
1482 let duration_secs = Self::parse_duration(&self.duration)?;
1484 let scenario =
1485 LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1486 let stages = scenario.generate_stages(duration_secs, self.vus);
1487
1488 let api_base_path = self.resolve_base_path(parser);
1490 if let Some(ref bp) = api_base_path {
1491 TerminalReporter::print_progress(&format!("Using base path: {}", bp));
1492 }
1493
1494 let mut all_headers = custom_headers.clone();
1496 if let Some(auth) = &self.auth {
1497 all_headers.insert("Authorization".to_string(), auth.clone());
1498 }
1499 let headers_json = serde_json::to_string(&all_headers).unwrap_or_else(|_| "{}".to_string());
1500
1501 let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
1503
1504 let flows_data: Vec<serde_json::Value> = flows.iter().map(|f| {
1505 let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
1507 serde_json::json!({
1508 "name": sanitized_name.clone(), "display_name": f.name, "base_path": f.base_path,
1511 "steps": f.steps.iter().enumerate().map(|(idx, s)| {
1512 let parts: Vec<&str> = s.operation.splitn(2, ' ').collect();
1514 let method_raw = if !parts.is_empty() {
1515 parts[0].to_uppercase()
1516 } else {
1517 "GET".to_string()
1518 };
1519 let method = if !parts.is_empty() {
1520 let m = parts[0].to_lowercase();
1521 if m == "delete" { "del".to_string() } else { m }
1523 } else {
1524 "get".to_string()
1525 };
1526 let raw_path = if parts.len() >= 2 { parts[1] } else { "/" };
1527 let path = if let Some(ref bp) = api_base_path {
1529 format!("{}{}", bp, raw_path)
1530 } else {
1531 raw_path.to_string()
1532 };
1533 let is_get_or_head = method == "get" || method == "head";
1534 let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1536
1537 let body_value = if has_body {
1539 param_overrides.as_ref()
1540 .map(|po| po.get_for_operation(None, &method_raw, raw_path))
1541 .and_then(|oo| oo.body)
1542 .unwrap_or_else(|| serde_json::json!({}))
1543 } else {
1544 serde_json::json!({})
1545 };
1546
1547 let processed_body = DynamicParamProcessor::process_json_body(&body_value);
1549 let body_has_extracted_placeholders = processed_body.value.contains("${extracted.");
1554 let body_is_dynamic = processed_body.is_dynamic || body_has_extracted_placeholders;
1555
1556 serde_json::json!({
1557 "operation": s.operation,
1558 "method": method,
1559 "path": path,
1560 "extract": s.extract,
1561 "use_values": s.use_values,
1562 "use_body": s.use_body,
1563 "merge_body": if s.merge_body.is_empty() { None } else { Some(&s.merge_body) },
1564 "inject_attacks": s.inject_attacks,
1565 "attack_types": s.attack_types,
1566 "description": s.description,
1567 "display_name": s.description.clone().unwrap_or_else(|| format!("Step {}", idx)),
1568 "is_get_or_head": is_get_or_head,
1569 "has_body": has_body,
1570 "body": processed_body.value,
1571 "body_is_dynamic": body_is_dynamic,
1572 "_placeholders": processed_body.placeholders.iter().map(|p| format!("{:?}", p)).collect::<Vec<_>>(),
1573 })
1574 }).collect::<Vec<_>>(),
1575 })
1576 }).collect();
1577
1578 for flow_data in &flows_data {
1580 if let Some(steps) = flow_data.get("steps").and_then(|s| s.as_array()) {
1581 for step in steps {
1582 if let Some(placeholders_arr) =
1583 step.get("_placeholders").and_then(|p| p.as_array())
1584 {
1585 for p_str in placeholders_arr {
1586 if let Some(p_name) = p_str.as_str() {
1587 match p_name {
1589 "VU" => {
1590 all_placeholders.insert(DynamicPlaceholder::VU);
1591 }
1592 "Iteration" => {
1593 all_placeholders.insert(DynamicPlaceholder::Iteration);
1594 }
1595 "Timestamp" => {
1596 all_placeholders.insert(DynamicPlaceholder::Timestamp);
1597 }
1598 "UUID" => {
1599 all_placeholders.insert(DynamicPlaceholder::UUID);
1600 }
1601 "Random" => {
1602 all_placeholders.insert(DynamicPlaceholder::Random);
1603 }
1604 "Counter" => {
1605 all_placeholders.insert(DynamicPlaceholder::Counter);
1606 }
1607 "Date" => {
1608 all_placeholders.insert(DynamicPlaceholder::Date);
1609 }
1610 "VuIter" => {
1611 all_placeholders.insert(DynamicPlaceholder::VuIter);
1612 }
1613 _ => {}
1614 }
1615 }
1616 }
1617 }
1618 }
1619 }
1620 }
1621
1622 let required_imports = DynamicParamProcessor::get_required_imports(&all_placeholders);
1624 let required_globals = DynamicParamProcessor::get_required_globals(&all_placeholders);
1625
1626 let invalid_data_config = self.build_invalid_data_config();
1628 let error_injection_enabled = invalid_data_config.is_some();
1629 let error_rate = self.error_rate.unwrap_or(0.0);
1630 let error_types: Vec<String> = invalid_data_config
1631 .as_ref()
1632 .map(|c| c.error_types.iter().map(|t| format!("{:?}", t)).collect())
1633 .unwrap_or_default();
1634
1635 if error_injection_enabled {
1636 TerminalReporter::print_progress(&format!(
1637 "Error injection enabled ({}% rate)",
1638 (error_rate * 100.0) as u32
1639 ));
1640 }
1641
1642 let security_testing_enabled = self.wafbench_dir.is_some() || self.security_test;
1644
1645 let data = serde_json::json!({
1646 "base_url": self.target,
1647 "flows": flows_data,
1648 "extract_fields": config.default_extract_fields,
1649 "duration_secs": duration_secs,
1650 "max_vus": self.vus,
1651 "auth_header": self.auth,
1652 "custom_headers": custom_headers,
1653 "skip_tls_verify": self.skip_tls_verify,
1654 "stages": stages.iter().map(|s| serde_json::json!({
1656 "duration": s.duration,
1657 "target": s.target,
1658 })).collect::<Vec<_>>(),
1659 "threshold_percentile": self.threshold_percentile,
1660 "threshold_ms": self.threshold_ms,
1661 "max_error_rate": self.max_error_rate,
1662 "headers": headers_json,
1663 "dynamic_imports": required_imports,
1664 "dynamic_globals": required_globals,
1665 "error_injection_enabled": error_injection_enabled,
1667 "error_rate": error_rate,
1668 "error_types": error_types,
1669 "security_testing_enabled": security_testing_enabled,
1671 });
1672
1673 let mut script = handlebars
1674 .render_template(template, &data)
1675 .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
1676
1677 if security_testing_enabled {
1679 script = self.generate_enhanced_script(&script)?;
1680 }
1681
1682 TerminalReporter::print_progress("Validating CRUD flow script...");
1684 let validation_errors = K6ScriptGenerator::validate_script(&script);
1685 if !validation_errors.is_empty() {
1686 TerminalReporter::print_error("CRUD flow script validation failed");
1687 for error in &validation_errors {
1688 eprintln!(" {}", error);
1689 }
1690 return Err(BenchError::Other(format!(
1691 "CRUD flow script validation failed with {} error(s)",
1692 validation_errors.len()
1693 )));
1694 }
1695
1696 TerminalReporter::print_success("CRUD flow script generated");
1697
1698 let script_path = if let Some(output) = &self.script_output {
1700 output.clone()
1701 } else {
1702 self.output.join("k6-crud-flow-script.js")
1703 };
1704
1705 if let Some(parent) = script_path.parent() {
1706 std::fs::create_dir_all(parent)?;
1707 }
1708 std::fs::write(&script_path, &script)?;
1709 TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
1710
1711 if self.generate_only {
1712 println!("\nScript generated successfully. Run it with:");
1713 println!(" k6 run {}", script_path.display());
1714 return Ok(());
1715 }
1716
1717 TerminalReporter::print_progress("Executing CRUD flow test...");
1719 let executor = K6Executor::new()?;
1720 std::fs::create_dir_all(&self.output)?;
1721
1722 let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
1723
1724 let duration_secs = Self::parse_duration(&self.duration)?;
1725 TerminalReporter::print_summary(&results, duration_secs);
1726
1727 Ok(())
1728 }
1729
1730 async fn execute_owasp_test(&self, parser: &SpecParser) -> Result<()> {
1732 TerminalReporter::print_progress("OWASP API Security Top 10 Testing Mode");
1733
1734 let custom_headers = self.parse_headers()?;
1736
1737 let mut config = OwaspApiConfig::new()
1739 .with_auth_header(&self.owasp_auth_header)
1740 .with_verbose(self.verbose)
1741 .with_insecure(self.skip_tls_verify)
1742 .with_concurrency(self.vus as usize)
1743 .with_iterations(self.owasp_iterations as usize)
1744 .with_base_path(self.base_path.clone())
1745 .with_custom_headers(custom_headers);
1746
1747 if let Some(ref token) = self.owasp_auth_token {
1749 config = config.with_valid_auth_token(token);
1750 }
1751
1752 if let Some(ref cats_str) = self.owasp_categories {
1754 let categories: Vec<OwaspCategory> = cats_str
1755 .split(',')
1756 .filter_map(|s| {
1757 let trimmed = s.trim();
1758 match trimmed.parse::<OwaspCategory>() {
1759 Ok(cat) => Some(cat),
1760 Err(e) => {
1761 TerminalReporter::print_warning(&e);
1762 None
1763 }
1764 }
1765 })
1766 .collect();
1767
1768 if !categories.is_empty() {
1769 config = config.with_categories(categories);
1770 }
1771 }
1772
1773 if let Some(ref admin_paths_file) = self.owasp_admin_paths {
1775 config.admin_paths_file = Some(admin_paths_file.clone());
1776 if let Err(e) = config.load_admin_paths() {
1777 TerminalReporter::print_warning(&format!("Failed to load admin paths file: {}", e));
1778 }
1779 }
1780
1781 if let Some(ref id_fields_str) = self.owasp_id_fields {
1783 let id_fields: Vec<String> = id_fields_str
1784 .split(',')
1785 .map(|s| s.trim().to_string())
1786 .filter(|s| !s.is_empty())
1787 .collect();
1788 if !id_fields.is_empty() {
1789 config = config.with_id_fields(id_fields);
1790 }
1791 }
1792
1793 if let Some(ref report_path) = self.owasp_report {
1795 config = config.with_report_path(report_path);
1796 }
1797 if let Ok(format) = self.owasp_report_format.parse::<ReportFormat>() {
1798 config = config.with_report_format(format);
1799 }
1800
1801 let categories = config.categories_to_test();
1803 TerminalReporter::print_success(&format!(
1804 "Testing {} OWASP categories: {}",
1805 categories.len(),
1806 categories.iter().map(|c| c.cli_name()).collect::<Vec<_>>().join(", ")
1807 ));
1808
1809 if config.valid_auth_token.is_some() {
1810 TerminalReporter::print_progress("Using provided auth token for baseline requests");
1811 }
1812
1813 TerminalReporter::print_progress("Generating OWASP security test script...");
1815 let generator = OwaspApiGenerator::new(config, self.target.clone(), parser);
1816
1817 let script = generator.generate()?;
1819 TerminalReporter::print_success("OWASP security test script generated");
1820
1821 let script_path = if let Some(output) = &self.script_output {
1823 output.clone()
1824 } else {
1825 self.output.join("k6-owasp-security-test.js")
1826 };
1827
1828 if let Some(parent) = script_path.parent() {
1829 std::fs::create_dir_all(parent)?;
1830 }
1831 std::fs::write(&script_path, &script)?;
1832 TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
1833
1834 if self.generate_only {
1836 println!("\nOWASP security test script generated. Run it with:");
1837 println!(" k6 run {}", script_path.display());
1838 return Ok(());
1839 }
1840
1841 TerminalReporter::print_progress("Executing OWASP security tests...");
1843 let executor = K6Executor::new()?;
1844 std::fs::create_dir_all(&self.output)?;
1845
1846 let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
1847
1848 let duration_secs = Self::parse_duration(&self.duration)?;
1849 TerminalReporter::print_summary(&results, duration_secs);
1850
1851 println!("\nOWASP security test results saved to: {}", self.output.display());
1852
1853 Ok(())
1854 }
1855}
1856
1857#[cfg(test)]
1858mod tests {
1859 use super::*;
1860
1861 #[test]
1862 fn test_parse_duration() {
1863 assert_eq!(BenchCommand::parse_duration("30s").unwrap(), 30);
1864 assert_eq!(BenchCommand::parse_duration("5m").unwrap(), 300);
1865 assert_eq!(BenchCommand::parse_duration("1h").unwrap(), 3600);
1866 assert_eq!(BenchCommand::parse_duration("60").unwrap(), 60);
1867 }
1868
1869 #[test]
1870 fn test_parse_duration_invalid() {
1871 assert!(BenchCommand::parse_duration("invalid").is_err());
1872 assert!(BenchCommand::parse_duration("30x").is_err());
1873 }
1874
1875 #[test]
1876 fn test_parse_headers() {
1877 let cmd = BenchCommand {
1878 spec: vec![PathBuf::from("test.yaml")],
1879 spec_dir: None,
1880 merge_conflicts: "error".to_string(),
1881 spec_mode: "merge".to_string(),
1882 dependency_config: None,
1883 target: "http://localhost".to_string(),
1884 base_path: None,
1885 duration: "1m".to_string(),
1886 vus: 10,
1887 scenario: "ramp-up".to_string(),
1888 operations: None,
1889 exclude_operations: None,
1890 auth: None,
1891 headers: Some("X-API-Key:test123,X-Client-ID:client456".to_string()),
1892 output: PathBuf::from("output"),
1893 generate_only: false,
1894 script_output: None,
1895 threshold_percentile: "p(95)".to_string(),
1896 threshold_ms: 500,
1897 max_error_rate: 0.05,
1898 verbose: false,
1899 skip_tls_verify: false,
1900 targets_file: None,
1901 max_concurrency: None,
1902 results_format: "both".to_string(),
1903 params_file: None,
1904 crud_flow: false,
1905 flow_config: None,
1906 extract_fields: None,
1907 parallel_create: None,
1908 data_file: None,
1909 data_distribution: "unique-per-vu".to_string(),
1910 data_mappings: None,
1911 per_uri_control: false,
1912 error_rate: None,
1913 error_types: None,
1914 security_test: false,
1915 security_payloads: None,
1916 security_categories: None,
1917 security_target_fields: None,
1918 wafbench_dir: None,
1919 wafbench_cycle_all: false,
1920 owasp_api_top10: false,
1921 owasp_categories: None,
1922 owasp_auth_header: "Authorization".to_string(),
1923 owasp_auth_token: None,
1924 owasp_admin_paths: None,
1925 owasp_id_fields: None,
1926 owasp_report: None,
1927 owasp_report_format: "json".to_string(),
1928 owasp_iterations: 1,
1929 };
1930
1931 let headers = cmd.parse_headers().unwrap();
1932 assert_eq!(headers.get("X-API-Key"), Some(&"test123".to_string()));
1933 assert_eq!(headers.get("X-Client-ID"), Some(&"client456".to_string()));
1934 }
1935
1936 #[test]
1937 fn test_get_spec_display_name() {
1938 let cmd = BenchCommand {
1939 spec: vec![PathBuf::from("test.yaml")],
1940 spec_dir: None,
1941 merge_conflicts: "error".to_string(),
1942 spec_mode: "merge".to_string(),
1943 dependency_config: None,
1944 target: "http://localhost".to_string(),
1945 base_path: None,
1946 duration: "1m".to_string(),
1947 vus: 10,
1948 scenario: "ramp-up".to_string(),
1949 operations: None,
1950 exclude_operations: None,
1951 auth: None,
1952 headers: None,
1953 output: PathBuf::from("output"),
1954 generate_only: false,
1955 script_output: None,
1956 threshold_percentile: "p(95)".to_string(),
1957 threshold_ms: 500,
1958 max_error_rate: 0.05,
1959 verbose: false,
1960 skip_tls_verify: false,
1961 targets_file: None,
1962 max_concurrency: None,
1963 results_format: "both".to_string(),
1964 params_file: None,
1965 crud_flow: false,
1966 flow_config: None,
1967 extract_fields: None,
1968 parallel_create: None,
1969 data_file: None,
1970 data_distribution: "unique-per-vu".to_string(),
1971 data_mappings: None,
1972 per_uri_control: false,
1973 error_rate: None,
1974 error_types: None,
1975 security_test: false,
1976 security_payloads: None,
1977 security_categories: None,
1978 security_target_fields: None,
1979 wafbench_dir: None,
1980 wafbench_cycle_all: false,
1981 owasp_api_top10: false,
1982 owasp_categories: None,
1983 owasp_auth_header: "Authorization".to_string(),
1984 owasp_auth_token: None,
1985 owasp_admin_paths: None,
1986 owasp_id_fields: None,
1987 owasp_report: None,
1988 owasp_report_format: "json".to_string(),
1989 owasp_iterations: 1,
1990 };
1991
1992 assert_eq!(cmd.get_spec_display_name(), "test.yaml");
1993
1994 let cmd_multi = BenchCommand {
1996 spec: vec![PathBuf::from("a.yaml"), PathBuf::from("b.yaml")],
1997 spec_dir: None,
1998 merge_conflicts: "error".to_string(),
1999 spec_mode: "merge".to_string(),
2000 dependency_config: None,
2001 target: "http://localhost".to_string(),
2002 base_path: None,
2003 duration: "1m".to_string(),
2004 vus: 10,
2005 scenario: "ramp-up".to_string(),
2006 operations: None,
2007 exclude_operations: None,
2008 auth: None,
2009 headers: None,
2010 output: PathBuf::from("output"),
2011 generate_only: false,
2012 script_output: None,
2013 threshold_percentile: "p(95)".to_string(),
2014 threshold_ms: 500,
2015 max_error_rate: 0.05,
2016 verbose: false,
2017 skip_tls_verify: false,
2018 targets_file: None,
2019 max_concurrency: None,
2020 results_format: "both".to_string(),
2021 params_file: None,
2022 crud_flow: false,
2023 flow_config: None,
2024 extract_fields: None,
2025 parallel_create: None,
2026 data_file: None,
2027 data_distribution: "unique-per-vu".to_string(),
2028 data_mappings: None,
2029 per_uri_control: false,
2030 error_rate: None,
2031 error_types: None,
2032 security_test: false,
2033 security_payloads: None,
2034 security_categories: None,
2035 security_target_fields: None,
2036 wafbench_dir: None,
2037 wafbench_cycle_all: false,
2038 owasp_api_top10: false,
2039 owasp_categories: None,
2040 owasp_auth_header: "Authorization".to_string(),
2041 owasp_auth_token: None,
2042 owasp_admin_paths: None,
2043 owasp_id_fields: None,
2044 owasp_report: None,
2045 owasp_report_format: "json".to_string(),
2046 owasp_iterations: 1,
2047 };
2048
2049 assert_eq!(cmd_multi.get_spec_display_name(), "2 spec files");
2050 }
2051}