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 k6_config = K6Config {
334 target_url: self.target.clone(),
335 base_path,
336 scenario,
337 duration_secs: Self::parse_duration(&self.duration)?,
338 max_vus: self.vus,
339 threshold_percentile: self.threshold_percentile.clone(),
340 threshold_ms: self.threshold_ms,
341 max_error_rate: self.max_error_rate,
342 auth_header: self.auth.clone(),
343 custom_headers,
344 skip_tls_verify: self.skip_tls_verify,
345 };
346
347 let generator = K6ScriptGenerator::new(k6_config, templates);
348 let mut script = generator.generate()?;
349 TerminalReporter::print_success("k6 script generated");
350
351 let has_advanced_features = self.data_file.is_some()
353 || self.error_rate.is_some()
354 || self.security_test
355 || self.parallel_create.is_some()
356 || self.wafbench_dir.is_some();
357
358 if has_advanced_features {
360 script = self.generate_enhanced_script(&script)?;
361 }
362
363 if mock_config.is_mock_server {
365 let setup_code = MockIntegrationGenerator::generate_setup(&mock_config);
366 let teardown_code = MockIntegrationGenerator::generate_teardown(&mock_config);
367 let helper_code = MockIntegrationGenerator::generate_vu_id_helper();
368
369 if let Some(import_end) = script.find("export const options") {
371 script.insert_str(
372 import_end,
373 &format!(
374 "\n// === Mock Server Integration ===\n{}\n{}\n{}\n",
375 helper_code, setup_code, teardown_code
376 ),
377 );
378 }
379 }
380
381 TerminalReporter::print_progress("Validating k6 script...");
383 let validation_errors = K6ScriptGenerator::validate_script(&script);
384 if !validation_errors.is_empty() {
385 TerminalReporter::print_error("Script validation failed");
386 for error in &validation_errors {
387 eprintln!(" {}", error);
388 }
389 return Err(BenchError::Other(format!(
390 "Generated k6 script has {} validation error(s). Please check the output above.",
391 validation_errors.len()
392 )));
393 }
394 TerminalReporter::print_success("Script validation passed");
395
396 let script_path = if let Some(output) = &self.script_output {
398 output.clone()
399 } else {
400 self.output.join("k6-script.js")
401 };
402
403 if let Some(parent) = script_path.parent() {
404 std::fs::create_dir_all(parent)?;
405 }
406 std::fs::write(&script_path, &script)?;
407 TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
408
409 if self.generate_only {
411 println!("\nScript generated successfully. Run it with:");
412 println!(" k6 run {}", script_path.display());
413 return Ok(());
414 }
415
416 TerminalReporter::print_progress("Executing load test...");
418 let executor = K6Executor::new()?;
419
420 std::fs::create_dir_all(&self.output)?;
421
422 let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
423
424 let duration_secs = Self::parse_duration(&self.duration)?;
426 TerminalReporter::print_summary(&results, duration_secs);
427
428 println!("\nResults saved to: {}", self.output.display());
429
430 Ok(())
431 }
432
433 async fn execute_multi_target(&self, targets_file: &PathBuf) -> Result<()> {
435 TerminalReporter::print_progress("Parsing targets file...");
436 let targets = parse_targets_file(targets_file)?;
437 let num_targets = targets.len();
438 TerminalReporter::print_success(&format!("Loaded {} targets", num_targets));
439
440 if targets.is_empty() {
441 return Err(BenchError::Other("No targets found in file".to_string()));
442 }
443
444 let max_concurrency = self.max_concurrency.unwrap_or(10) as usize;
446 let max_concurrency = max_concurrency.min(num_targets); TerminalReporter::print_header(
450 &self.get_spec_display_name(),
451 &format!("{} targets", num_targets),
452 0,
453 &self.scenario,
454 Self::parse_duration(&self.duration)?,
455 );
456
457 let executor = ParallelExecutor::new(
459 BenchCommand {
460 spec: self.spec.clone(),
462 spec_dir: self.spec_dir.clone(),
463 merge_conflicts: self.merge_conflicts.clone(),
464 spec_mode: self.spec_mode.clone(),
465 dependency_config: self.dependency_config.clone(),
466 target: self.target.clone(), base_path: self.base_path.clone(),
468 duration: self.duration.clone(),
469 vus: self.vus,
470 scenario: self.scenario.clone(),
471 operations: self.operations.clone(),
472 exclude_operations: self.exclude_operations.clone(),
473 auth: self.auth.clone(),
474 headers: self.headers.clone(),
475 output: self.output.clone(),
476 generate_only: self.generate_only,
477 script_output: self.script_output.clone(),
478 threshold_percentile: self.threshold_percentile.clone(),
479 threshold_ms: self.threshold_ms,
480 max_error_rate: self.max_error_rate,
481 verbose: self.verbose,
482 skip_tls_verify: self.skip_tls_verify,
483 targets_file: None,
484 max_concurrency: None,
485 results_format: self.results_format.clone(),
486 params_file: self.params_file.clone(),
487 crud_flow: self.crud_flow,
488 flow_config: self.flow_config.clone(),
489 extract_fields: self.extract_fields.clone(),
490 parallel_create: self.parallel_create,
491 data_file: self.data_file.clone(),
492 data_distribution: self.data_distribution.clone(),
493 data_mappings: self.data_mappings.clone(),
494 per_uri_control: self.per_uri_control,
495 error_rate: self.error_rate,
496 error_types: self.error_types.clone(),
497 security_test: self.security_test,
498 security_payloads: self.security_payloads.clone(),
499 security_categories: self.security_categories.clone(),
500 security_target_fields: self.security_target_fields.clone(),
501 wafbench_dir: self.wafbench_dir.clone(),
502 wafbench_cycle_all: self.wafbench_cycle_all,
503 owasp_api_top10: self.owasp_api_top10,
504 owasp_categories: self.owasp_categories.clone(),
505 owasp_auth_header: self.owasp_auth_header.clone(),
506 owasp_auth_token: self.owasp_auth_token.clone(),
507 owasp_admin_paths: self.owasp_admin_paths.clone(),
508 owasp_id_fields: self.owasp_id_fields.clone(),
509 owasp_report: self.owasp_report.clone(),
510 owasp_report_format: self.owasp_report_format.clone(),
511 owasp_iterations: self.owasp_iterations,
512 },
513 targets,
514 max_concurrency,
515 );
516
517 let aggregated_results = executor.execute_all().await?;
519
520 self.report_multi_target_results(&aggregated_results)?;
522
523 Ok(())
524 }
525
526 fn report_multi_target_results(&self, results: &AggregatedResults) -> Result<()> {
528 TerminalReporter::print_multi_target_summary(results);
530
531 if self.results_format == "aggregated" || self.results_format == "both" {
533 let summary_path = self.output.join("aggregated_summary.json");
534 let summary_json = serde_json::json!({
535 "total_targets": results.total_targets,
536 "successful_targets": results.successful_targets,
537 "failed_targets": results.failed_targets,
538 "aggregated_metrics": {
539 "total_requests": results.aggregated_metrics.total_requests,
540 "total_failed_requests": results.aggregated_metrics.total_failed_requests,
541 "avg_duration_ms": results.aggregated_metrics.avg_duration_ms,
542 "p95_duration_ms": results.aggregated_metrics.p95_duration_ms,
543 "p99_duration_ms": results.aggregated_metrics.p99_duration_ms,
544 "error_rate": results.aggregated_metrics.error_rate,
545 },
546 "target_results": results.target_results.iter().map(|r| {
547 serde_json::json!({
548 "target_url": r.target_url,
549 "target_index": r.target_index,
550 "success": r.success,
551 "error": r.error,
552 "total_requests": r.results.total_requests,
553 "failed_requests": r.results.failed_requests,
554 "avg_duration_ms": r.results.avg_duration_ms,
555 "p95_duration_ms": r.results.p95_duration_ms,
556 "p99_duration_ms": r.results.p99_duration_ms,
557 "output_dir": r.output_dir.to_string_lossy(),
558 })
559 }).collect::<Vec<_>>(),
560 });
561
562 std::fs::write(&summary_path, serde_json::to_string_pretty(&summary_json)?)?;
563 TerminalReporter::print_success(&format!(
564 "Aggregated summary saved to: {}",
565 summary_path.display()
566 ));
567 }
568
569 println!("\nResults saved to: {}", self.output.display());
570 println!(" - Per-target results: {}", self.output.join("target_*").display());
571 if self.results_format == "aggregated" || self.results_format == "both" {
572 println!(
573 " - Aggregated summary: {}",
574 self.output.join("aggregated_summary.json").display()
575 );
576 }
577
578 Ok(())
579 }
580
581 pub fn parse_duration(duration: &str) -> Result<u64> {
583 let duration = duration.trim();
584
585 if let Some(secs) = duration.strip_suffix('s') {
586 secs.parse::<u64>()
587 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
588 } else if let Some(mins) = duration.strip_suffix('m') {
589 mins.parse::<u64>()
590 .map(|m| m * 60)
591 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
592 } else if let Some(hours) = duration.strip_suffix('h') {
593 hours
594 .parse::<u64>()
595 .map(|h| h * 3600)
596 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
597 } else {
598 duration
600 .parse::<u64>()
601 .map_err(|_| BenchError::Other(format!("Invalid duration: {}", duration)))
602 }
603 }
604
605 pub fn parse_headers(&self) -> Result<HashMap<String, String>> {
607 let mut headers = HashMap::new();
608
609 if let Some(header_str) = &self.headers {
610 for pair in header_str.split(',') {
611 let parts: Vec<&str> = pair.splitn(2, ':').collect();
612 if parts.len() != 2 {
613 return Err(BenchError::Other(format!(
614 "Invalid header format: '{}'. Expected 'Key:Value'",
615 pair
616 )));
617 }
618 headers.insert(parts[0].trim().to_string(), parts[1].trim().to_string());
619 }
620 }
621
622 Ok(headers)
623 }
624
625 fn resolve_base_path(&self, parser: &SpecParser) -> Option<String> {
634 if let Some(cli_base_path) = &self.base_path {
636 if cli_base_path.is_empty() {
637 return None;
639 }
640 return Some(cli_base_path.clone());
641 }
642
643 parser.get_base_path()
645 }
646
647 async fn build_mock_config(&self) -> MockIntegrationConfig {
649 if MockServerDetector::looks_like_mock_server(&self.target) {
651 if let Ok(info) = MockServerDetector::detect(&self.target).await {
653 if info.is_mockforge {
654 TerminalReporter::print_success(&format!(
655 "Detected MockForge server (version: {})",
656 info.version.as_deref().unwrap_or("unknown")
657 ));
658 return MockIntegrationConfig::mock_server();
659 }
660 }
661 }
662 MockIntegrationConfig::real_api()
663 }
664
665 fn build_crud_flow_config(&self) -> Option<CrudFlowConfig> {
667 if !self.crud_flow {
668 return None;
669 }
670
671 if let Some(config_path) = &self.flow_config {
673 match CrudFlowConfig::from_file(config_path) {
674 Ok(config) => return Some(config),
675 Err(e) => {
676 TerminalReporter::print_warning(&format!(
677 "Failed to load flow config: {}. Using auto-detection.",
678 e
679 ));
680 }
681 }
682 }
683
684 let extract_fields = self
686 .extract_fields
687 .as_ref()
688 .map(|f| f.split(',').map(|s| s.trim().to_string()).collect())
689 .unwrap_or_else(|| vec!["id".to_string(), "uuid".to_string()]);
690
691 Some(CrudFlowConfig {
692 flows: Vec::new(), default_extract_fields: extract_fields,
694 })
695 }
696
697 fn build_data_driven_config(&self) -> Option<DataDrivenConfig> {
699 let data_file = self.data_file.as_ref()?;
700
701 let distribution = DataDistribution::from_str(&self.data_distribution)
702 .unwrap_or(DataDistribution::UniquePerVu);
703
704 let mappings = self
705 .data_mappings
706 .as_ref()
707 .map(|m| DataMapping::parse_mappings(m).unwrap_or_default())
708 .unwrap_or_default();
709
710 Some(DataDrivenConfig {
711 file_path: data_file.to_string_lossy().to_string(),
712 distribution,
713 mappings,
714 csv_has_header: true,
715 per_uri_control: self.per_uri_control,
716 per_uri_columns: crate::data_driven::PerUriColumns::default(),
717 })
718 }
719
720 fn build_invalid_data_config(&self) -> Option<InvalidDataConfig> {
722 let error_rate = self.error_rate?;
723
724 let error_types = self
725 .error_types
726 .as_ref()
727 .map(|types| InvalidDataConfig::parse_error_types(types).unwrap_or_default())
728 .unwrap_or_default();
729
730 Some(InvalidDataConfig {
731 error_rate,
732 error_types,
733 target_fields: Vec::new(),
734 })
735 }
736
737 fn build_security_config(&self) -> Option<SecurityTestConfig> {
739 if !self.security_test {
740 return None;
741 }
742
743 let categories = self
744 .security_categories
745 .as_ref()
746 .map(|cats| SecurityTestConfig::parse_categories(cats).unwrap_or_default())
747 .unwrap_or_else(|| {
748 let mut default = std::collections::HashSet::new();
749 default.insert(SecurityCategory::SqlInjection);
750 default.insert(SecurityCategory::Xss);
751 default
752 });
753
754 let target_fields = self
755 .security_target_fields
756 .as_ref()
757 .map(|fields| fields.split(',').map(|f| f.trim().to_string()).collect())
758 .unwrap_or_default();
759
760 let custom_payloads_file =
761 self.security_payloads.as_ref().map(|p| p.to_string_lossy().to_string());
762
763 Some(SecurityTestConfig {
764 enabled: true,
765 categories,
766 target_fields,
767 custom_payloads_file,
768 include_high_risk: false,
769 })
770 }
771
772 fn build_parallel_config(&self) -> Option<ParallelConfig> {
774 let count = self.parallel_create?;
775
776 Some(ParallelConfig::new(count))
777 }
778
779 fn load_wafbench_payloads(&self) -> Vec<SecurityPayload> {
781 let Some(ref wafbench_dir) = self.wafbench_dir else {
782 return Vec::new();
783 };
784
785 let mut loader = WafBenchLoader::new();
786
787 if let Err(e) = loader.load_from_pattern(wafbench_dir) {
788 TerminalReporter::print_warning(&format!("Failed to load WAFBench tests: {}", e));
789 return Vec::new();
790 }
791
792 let stats = loader.stats();
793
794 if stats.files_processed == 0 {
795 TerminalReporter::print_warning(&format!(
796 "No WAFBench YAML files found matching '{}'",
797 wafbench_dir
798 ));
799 if !stats.parse_errors.is_empty() {
801 TerminalReporter::print_warning("Some files were found but failed to parse:");
802 for error in &stats.parse_errors {
803 TerminalReporter::print_warning(&format!(" - {}", error));
804 }
805 }
806 return Vec::new();
807 }
808
809 TerminalReporter::print_progress(&format!(
810 "Loaded {} WAFBench files, {} test cases, {} payloads",
811 stats.files_processed, stats.test_cases_loaded, stats.payloads_extracted
812 ));
813
814 for (category, count) in &stats.by_category {
816 TerminalReporter::print_progress(&format!(" - {}: {} tests", category, count));
817 }
818
819 for error in &stats.parse_errors {
821 TerminalReporter::print_warning(&format!(" Parse error: {}", error));
822 }
823
824 loader.to_security_payloads()
825 }
826
827 fn generate_enhanced_script(&self, base_script: &str) -> Result<String> {
829 let mut enhanced_script = base_script.to_string();
830 let mut additional_code = String::new();
831
832 if let Some(config) = self.build_data_driven_config() {
834 TerminalReporter::print_progress("Adding data-driven testing support...");
835 additional_code.push_str(&DataDrivenGenerator::generate_setup(&config));
836 additional_code.push('\n');
837 TerminalReporter::print_success("Data-driven testing enabled");
838 }
839
840 if let Some(config) = self.build_invalid_data_config() {
842 TerminalReporter::print_progress("Adding invalid data testing support...");
843 additional_code.push_str(&InvalidDataGenerator::generate_invalidation_logic());
844 additional_code.push('\n');
845 additional_code
846 .push_str(&InvalidDataGenerator::generate_should_invalidate(config.error_rate));
847 additional_code.push('\n');
848 additional_code
849 .push_str(&InvalidDataGenerator::generate_type_selection(&config.error_types));
850 additional_code.push('\n');
851 TerminalReporter::print_success(&format!(
852 "Invalid data testing enabled ({}% error rate)",
853 (self.error_rate.unwrap_or(0.0) * 100.0) as u32
854 ));
855 }
856
857 let security_config = self.build_security_config();
859 let wafbench_payloads = self.load_wafbench_payloads();
860
861 if security_config.is_some() || !wafbench_payloads.is_empty() {
862 TerminalReporter::print_progress("Adding security testing support...");
863
864 let mut payload_list: Vec<SecurityPayload> = Vec::new();
866
867 if let Some(ref config) = security_config {
868 payload_list.extend(SecurityPayloads::get_payloads(config));
869 }
870
871 if !wafbench_payloads.is_empty() {
873 TerminalReporter::print_progress(&format!(
874 "Loading {} WAFBench attack patterns...",
875 wafbench_payloads.len()
876 ));
877 payload_list.extend(wafbench_payloads);
878 }
879
880 let target_fields =
881 security_config.as_ref().map(|c| c.target_fields.clone()).unwrap_or_default();
882
883 additional_code.push_str(&SecurityTestGenerator::generate_payload_selection(
884 &payload_list,
885 self.wafbench_cycle_all,
886 ));
887 additional_code.push('\n');
888 additional_code
889 .push_str(&SecurityTestGenerator::generate_apply_payload(&target_fields));
890 additional_code.push('\n');
891 additional_code.push_str(&SecurityTestGenerator::generate_security_checks());
892 additional_code.push('\n');
893
894 let mode = if self.wafbench_cycle_all {
895 "cycle-all"
896 } else {
897 "random"
898 };
899 TerminalReporter::print_success(&format!(
900 "Security testing enabled ({} payloads, {} mode)",
901 payload_list.len(),
902 mode
903 ));
904 }
905
906 if let Some(config) = self.build_parallel_config() {
908 TerminalReporter::print_progress("Adding parallel execution support...");
909 additional_code.push_str(&ParallelRequestGenerator::generate_batch_helper(&config));
910 additional_code.push('\n');
911 TerminalReporter::print_success(&format!(
912 "Parallel execution enabled (count: {})",
913 config.count
914 ));
915 }
916
917 if !additional_code.is_empty() {
919 if let Some(import_end) = enhanced_script.find("export const options") {
921 enhanced_script.insert_str(
922 import_end,
923 &format!("\n// === Advanced Testing Features ===\n{}\n", additional_code),
924 );
925 }
926 }
927
928 Ok(enhanced_script)
929 }
930
931 async fn execute_sequential_specs(&self) -> Result<()> {
933 TerminalReporter::print_progress("Sequential spec mode: Loading specs individually...");
934
935 let mut all_specs: Vec<(PathBuf, OpenApiSpec)> = Vec::new();
937
938 if !self.spec.is_empty() {
939 let specs = load_specs_from_files(self.spec.clone())
940 .await
941 .map_err(|e| BenchError::Other(format!("Failed to load spec files: {}", e)))?;
942 all_specs.extend(specs);
943 }
944
945 if let Some(spec_dir) = &self.spec_dir {
946 let dir_specs = load_specs_from_directory(spec_dir).await.map_err(|e| {
947 BenchError::Other(format!("Failed to load specs from directory: {}", e))
948 })?;
949 all_specs.extend(dir_specs);
950 }
951
952 if all_specs.is_empty() {
953 return Err(BenchError::Other(
954 "No spec files found for sequential execution".to_string(),
955 ));
956 }
957
958 TerminalReporter::print_success(&format!("Loaded {} spec(s)", all_specs.len()));
959
960 let execution_order = if let Some(config_path) = &self.dependency_config {
962 TerminalReporter::print_progress("Loading dependency configuration...");
963 let config = SpecDependencyConfig::from_file(config_path)?;
964
965 if !config.disable_auto_detect && config.execution_order.is_empty() {
966 self.detect_and_sort_specs(&all_specs)?
968 } else {
969 config.execution_order.iter().flat_map(|g| g.specs.clone()).collect()
971 }
972 } else {
973 self.detect_and_sort_specs(&all_specs)?
975 };
976
977 TerminalReporter::print_success(&format!(
978 "Execution order: {}",
979 execution_order
980 .iter()
981 .map(|p| p.file_name().unwrap_or_default().to_string_lossy().to_string())
982 .collect::<Vec<_>>()
983 .join(" → ")
984 ));
985
986 let mut extracted_values = ExtractedValues::new();
988 let total_specs = execution_order.len();
989
990 for (index, spec_path) in execution_order.iter().enumerate() {
991 let spec_name = spec_path.file_name().unwrap_or_default().to_string_lossy().to_string();
992
993 TerminalReporter::print_progress(&format!(
994 "[{}/{}] Executing spec: {}",
995 index + 1,
996 total_specs,
997 spec_name
998 ));
999
1000 let spec = all_specs
1002 .iter()
1003 .find(|(p, _)| {
1004 p == spec_path
1005 || p.file_name() == spec_path.file_name()
1006 || p.file_name() == Some(spec_path.as_os_str())
1007 })
1008 .map(|(_, s)| s.clone())
1009 .ok_or_else(|| {
1010 BenchError::Other(format!("Spec not found: {}", spec_path.display()))
1011 })?;
1012
1013 let new_values = self.execute_single_spec(&spec, &spec_name, &extracted_values).await?;
1015
1016 extracted_values.merge(&new_values);
1018
1019 TerminalReporter::print_success(&format!(
1020 "[{}/{}] Completed: {} (extracted {} values)",
1021 index + 1,
1022 total_specs,
1023 spec_name,
1024 new_values.values.len()
1025 ));
1026 }
1027
1028 TerminalReporter::print_success(&format!(
1029 "Sequential execution complete: {} specs executed",
1030 total_specs
1031 ));
1032
1033 Ok(())
1034 }
1035
1036 fn detect_and_sort_specs(&self, specs: &[(PathBuf, OpenApiSpec)]) -> Result<Vec<PathBuf>> {
1038 TerminalReporter::print_progress("Auto-detecting spec dependencies...");
1039
1040 let mut detector = DependencyDetector::new();
1041 let dependencies = detector.detect_dependencies(specs);
1042
1043 if dependencies.is_empty() {
1044 TerminalReporter::print_progress("No dependencies detected, using file order");
1045 return Ok(specs.iter().map(|(p, _)| p.clone()).collect());
1046 }
1047
1048 TerminalReporter::print_progress(&format!(
1049 "Detected {} cross-spec dependencies",
1050 dependencies.len()
1051 ));
1052
1053 for dep in &dependencies {
1054 TerminalReporter::print_progress(&format!(
1055 " {} → {} (via field '{}')",
1056 dep.dependency_spec.file_name().unwrap_or_default().to_string_lossy(),
1057 dep.dependent_spec.file_name().unwrap_or_default().to_string_lossy(),
1058 dep.field_name
1059 ));
1060 }
1061
1062 topological_sort(specs, &dependencies)
1063 }
1064
1065 async fn execute_single_spec(
1067 &self,
1068 spec: &OpenApiSpec,
1069 spec_name: &str,
1070 _external_values: &ExtractedValues,
1071 ) -> Result<ExtractedValues> {
1072 let parser = SpecParser::from_spec(spec.clone());
1073
1074 if self.crud_flow {
1076 self.execute_crud_flow_with_extraction(&parser, spec_name).await
1078 } else {
1079 self.execute_standard_spec(&parser, spec_name).await?;
1081 Ok(ExtractedValues::new())
1082 }
1083 }
1084
1085 async fn execute_crud_flow_with_extraction(
1087 &self,
1088 parser: &SpecParser,
1089 spec_name: &str,
1090 ) -> Result<ExtractedValues> {
1091 let operations = parser.get_operations();
1092 let flows = CrudFlowDetector::detect_flows(&operations);
1093
1094 if flows.is_empty() {
1095 TerminalReporter::print_warning(&format!("No CRUD flows detected in {}", spec_name));
1096 return Ok(ExtractedValues::new());
1097 }
1098
1099 TerminalReporter::print_progress(&format!(
1100 " {} CRUD flow(s) in {}",
1101 flows.len(),
1102 spec_name
1103 ));
1104
1105 let mut handlebars = handlebars::Handlebars::new();
1107 handlebars.register_helper(
1109 "json",
1110 Box::new(
1111 |h: &handlebars::Helper,
1112 _: &handlebars::Handlebars,
1113 _: &handlebars::Context,
1114 _: &mut handlebars::RenderContext,
1115 out: &mut dyn handlebars::Output|
1116 -> handlebars::HelperResult {
1117 let param = h.param(0).map(|v| v.value()).unwrap_or(&serde_json::Value::Null);
1118 out.write(&serde_json::to_string(param).unwrap_or_else(|_| "[]".to_string()))?;
1119 Ok(())
1120 },
1121 ),
1122 );
1123 let template = include_str!("templates/k6_crud_flow.hbs");
1124
1125 let custom_headers = self.parse_headers()?;
1126 let config = self.build_crud_flow_config().unwrap_or_default();
1127
1128 let param_overrides = if let Some(params_file) = &self.params_file {
1130 let overrides = ParameterOverrides::from_file(params_file)?;
1131 Some(overrides)
1132 } else {
1133 None
1134 };
1135
1136 let duration_secs = Self::parse_duration(&self.duration)?;
1138 let scenario =
1139 LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1140 let stages = scenario.generate_stages(duration_secs, self.vus);
1141
1142 let api_base_path = self.resolve_base_path(parser);
1144
1145 let mut all_headers = custom_headers.clone();
1147 if let Some(auth) = &self.auth {
1148 all_headers.insert("Authorization".to_string(), auth.clone());
1149 }
1150 let headers_json = serde_json::to_string(&all_headers).unwrap_or_else(|_| "{}".to_string());
1151
1152 let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
1154
1155 let flows_data: Vec<serde_json::Value> = flows.iter().map(|f| {
1156 let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
1157 serde_json::json!({
1158 "name": sanitized_name.clone(),
1159 "display_name": f.name,
1160 "base_path": f.base_path,
1161 "steps": f.steps.iter().enumerate().map(|(idx, s)| {
1162 let parts: Vec<&str> = s.operation.splitn(2, ' ').collect();
1164 let method_raw = if !parts.is_empty() {
1165 parts[0].to_uppercase()
1166 } else {
1167 "GET".to_string()
1168 };
1169 let method = if !parts.is_empty() {
1170 let m = parts[0].to_lowercase();
1171 if m == "delete" { "del".to_string() } else { m }
1173 } else {
1174 "get".to_string()
1175 };
1176 let raw_path = if parts.len() >= 2 { parts[1] } else { "/" };
1177 let path = if let Some(ref bp) = api_base_path {
1179 format!("{}{}", bp, raw_path)
1180 } else {
1181 raw_path.to_string()
1182 };
1183 let is_get_or_head = method == "get" || method == "head";
1184 let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1186
1187 let body_value = if has_body {
1189 param_overrides.as_ref()
1190 .map(|po| po.get_for_operation(None, &method_raw, &raw_path))
1191 .and_then(|oo| oo.body)
1192 .unwrap_or_else(|| serde_json::json!({}))
1193 } else {
1194 serde_json::json!({})
1195 };
1196
1197 let processed_body = DynamicParamProcessor::process_json_body(&body_value);
1199
1200 let body_has_extracted_placeholders = processed_body.value.contains("${extracted.");
1202 let body_is_dynamic = processed_body.is_dynamic || body_has_extracted_placeholders;
1203
1204 serde_json::json!({
1205 "operation": s.operation,
1206 "method": method,
1207 "path": path,
1208 "extract": s.extract,
1209 "use_values": s.use_values,
1210 "use_body": s.use_body,
1211 "merge_body": if s.merge_body.is_empty() { None } else { Some(&s.merge_body) },
1212 "inject_attacks": s.inject_attacks,
1213 "attack_types": s.attack_types,
1214 "description": s.description,
1215 "display_name": s.description.clone().unwrap_or_else(|| format!("Step {}", idx)),
1216 "is_get_or_head": is_get_or_head,
1217 "has_body": has_body,
1218 "body": processed_body.value,
1219 "body_is_dynamic": body_is_dynamic,
1220 "_placeholders": processed_body.placeholders.iter().map(|p| format!("{:?}", p)).collect::<Vec<_>>(),
1221 })
1222 }).collect::<Vec<_>>(),
1223 })
1224 }).collect();
1225
1226 for flow_data in &flows_data {
1228 if let Some(steps) = flow_data.get("steps").and_then(|s| s.as_array()) {
1229 for step in steps {
1230 if let Some(placeholders_arr) =
1231 step.get("_placeholders").and_then(|p| p.as_array())
1232 {
1233 for p_str in placeholders_arr {
1234 if let Some(p_name) = p_str.as_str() {
1235 match p_name {
1236 "VU" => {
1237 all_placeholders.insert(DynamicPlaceholder::VU);
1238 }
1239 "Iteration" => {
1240 all_placeholders.insert(DynamicPlaceholder::Iteration);
1241 }
1242 "Timestamp" => {
1243 all_placeholders.insert(DynamicPlaceholder::Timestamp);
1244 }
1245 "UUID" => {
1246 all_placeholders.insert(DynamicPlaceholder::UUID);
1247 }
1248 "Random" => {
1249 all_placeholders.insert(DynamicPlaceholder::Random);
1250 }
1251 "Counter" => {
1252 all_placeholders.insert(DynamicPlaceholder::Counter);
1253 }
1254 "Date" => {
1255 all_placeholders.insert(DynamicPlaceholder::Date);
1256 }
1257 "VuIter" => {
1258 all_placeholders.insert(DynamicPlaceholder::VuIter);
1259 }
1260 _ => {}
1261 }
1262 }
1263 }
1264 }
1265 }
1266 }
1267 }
1268
1269 let required_imports = DynamicParamProcessor::get_required_imports(&all_placeholders);
1271 let required_globals = DynamicParamProcessor::get_required_globals(&all_placeholders);
1272
1273 let security_testing_enabled = self.wafbench_dir.is_some() || self.security_test;
1275
1276 let data = serde_json::json!({
1277 "base_url": self.target,
1278 "flows": flows_data,
1279 "extract_fields": config.default_extract_fields,
1280 "duration_secs": duration_secs,
1281 "max_vus": self.vus,
1282 "auth_header": self.auth,
1283 "custom_headers": custom_headers,
1284 "skip_tls_verify": self.skip_tls_verify,
1285 "stages": stages.iter().map(|s| serde_json::json!({
1287 "duration": s.duration,
1288 "target": s.target,
1289 })).collect::<Vec<_>>(),
1290 "threshold_percentile": self.threshold_percentile,
1291 "threshold_ms": self.threshold_ms,
1292 "max_error_rate": self.max_error_rate,
1293 "headers": headers_json,
1294 "dynamic_imports": required_imports,
1295 "dynamic_globals": required_globals,
1296 "security_testing_enabled": security_testing_enabled,
1298 });
1299
1300 let mut script = handlebars
1301 .render_template(template, &data)
1302 .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
1303
1304 if security_testing_enabled {
1306 script = self.generate_enhanced_script(&script)?;
1307 }
1308
1309 let script_path =
1311 self.output.join(format!("k6-{}-crud-flow.js", spec_name.replace('.', "_")));
1312
1313 std::fs::create_dir_all(self.output.clone())?;
1314 std::fs::write(&script_path, &script)?;
1315
1316 if !self.generate_only {
1317 let executor = K6Executor::new()?;
1318 let output_dir = self.output.join(format!("{}_results", spec_name.replace('.', "_")));
1319 std::fs::create_dir_all(&output_dir)?;
1320
1321 executor.execute(&script_path, Some(&output_dir), self.verbose).await?;
1322 }
1323
1324 Ok(ExtractedValues::new())
1327 }
1328
1329 async fn execute_standard_spec(&self, parser: &SpecParser, spec_name: &str) -> Result<()> {
1331 let mut operations = if let Some(filter) = &self.operations {
1332 parser.filter_operations(filter)?
1333 } else {
1334 parser.get_operations()
1335 };
1336
1337 if let Some(exclude) = &self.exclude_operations {
1338 operations = parser.exclude_operations(operations, exclude)?;
1339 }
1340
1341 if operations.is_empty() {
1342 TerminalReporter::print_warning(&format!("No operations found in {}", spec_name));
1343 return Ok(());
1344 }
1345
1346 TerminalReporter::print_progress(&format!(
1347 " {} operations in {}",
1348 operations.len(),
1349 spec_name
1350 ));
1351
1352 let templates: Vec<_> = operations
1354 .iter()
1355 .map(RequestGenerator::generate_template)
1356 .collect::<Result<Vec<_>>>()?;
1357
1358 let custom_headers = self.parse_headers()?;
1360
1361 let base_path = self.resolve_base_path(parser);
1363
1364 let scenario =
1366 LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1367
1368 let k6_config = K6Config {
1369 target_url: self.target.clone(),
1370 base_path,
1371 scenario,
1372 duration_secs: Self::parse_duration(&self.duration)?,
1373 max_vus: self.vus,
1374 threshold_percentile: self.threshold_percentile.clone(),
1375 threshold_ms: self.threshold_ms,
1376 max_error_rate: self.max_error_rate,
1377 auth_header: self.auth.clone(),
1378 custom_headers,
1379 skip_tls_verify: self.skip_tls_verify,
1380 };
1381
1382 let generator = K6ScriptGenerator::new(k6_config, templates);
1383 let script = generator.generate()?;
1384
1385 let script_path = self.output.join(format!("k6-{}.js", spec_name.replace('.', "_")));
1387
1388 std::fs::create_dir_all(self.output.clone())?;
1389 std::fs::write(&script_path, &script)?;
1390
1391 if !self.generate_only {
1392 let executor = K6Executor::new()?;
1393 let output_dir = self.output.join(format!("{}_results", spec_name.replace('.', "_")));
1394 std::fs::create_dir_all(&output_dir)?;
1395
1396 executor.execute(&script_path, Some(&output_dir), self.verbose).await?;
1397 }
1398
1399 Ok(())
1400 }
1401
1402 async fn execute_crud_flow(&self, parser: &SpecParser) -> Result<()> {
1404 let config = self.build_crud_flow_config().unwrap_or_default();
1406
1407 let flows = if !config.flows.is_empty() {
1409 TerminalReporter::print_progress("Using custom flow configuration...");
1410 config.flows.clone()
1411 } else {
1412 TerminalReporter::print_progress("Detecting CRUD operations...");
1413 let operations = parser.get_operations();
1414 CrudFlowDetector::detect_flows(&operations)
1415 };
1416
1417 if flows.is_empty() {
1418 return Err(BenchError::Other(
1419 "No CRUD flows detected in spec. Ensure spec has POST/GET/PUT/DELETE operations on related paths.".to_string(),
1420 ));
1421 }
1422
1423 if config.flows.is_empty() {
1424 TerminalReporter::print_success(&format!("Detected {} CRUD flow(s)", flows.len()));
1425 } else {
1426 TerminalReporter::print_success(&format!("Loaded {} custom flow(s)", flows.len()));
1427 }
1428
1429 for flow in &flows {
1430 TerminalReporter::print_progress(&format!(
1431 " - {}: {} steps",
1432 flow.name,
1433 flow.steps.len()
1434 ));
1435 }
1436
1437 let mut handlebars = handlebars::Handlebars::new();
1439 handlebars.register_helper(
1441 "json",
1442 Box::new(
1443 |h: &handlebars::Helper,
1444 _: &handlebars::Handlebars,
1445 _: &handlebars::Context,
1446 _: &mut handlebars::RenderContext,
1447 out: &mut dyn handlebars::Output|
1448 -> handlebars::HelperResult {
1449 let param = h.param(0).map(|v| v.value()).unwrap_or(&serde_json::Value::Null);
1450 out.write(&serde_json::to_string(param).unwrap_or_else(|_| "[]".to_string()))?;
1451 Ok(())
1452 },
1453 ),
1454 );
1455 let template = include_str!("templates/k6_crud_flow.hbs");
1456
1457 let custom_headers = self.parse_headers()?;
1458
1459 let param_overrides = if let Some(params_file) = &self.params_file {
1461 TerminalReporter::print_progress("Loading parameter overrides...");
1462 let overrides = ParameterOverrides::from_file(params_file)?;
1463 TerminalReporter::print_success(&format!(
1464 "Loaded parameter overrides ({} operation-specific, {} defaults)",
1465 overrides.operations.len(),
1466 if overrides.defaults.is_empty() { 0 } else { 1 }
1467 ));
1468 Some(overrides)
1469 } else {
1470 None
1471 };
1472
1473 let duration_secs = Self::parse_duration(&self.duration)?;
1475 let scenario =
1476 LoadScenario::from_str(&self.scenario).map_err(BenchError::InvalidScenario)?;
1477 let stages = scenario.generate_stages(duration_secs, self.vus);
1478
1479 let api_base_path = self.resolve_base_path(parser);
1481 if let Some(ref bp) = api_base_path {
1482 TerminalReporter::print_progress(&format!("Using base path: {}", bp));
1483 }
1484
1485 let mut all_headers = custom_headers.clone();
1487 if let Some(auth) = &self.auth {
1488 all_headers.insert("Authorization".to_string(), auth.clone());
1489 }
1490 let headers_json = serde_json::to_string(&all_headers).unwrap_or_else(|_| "{}".to_string());
1491
1492 let mut all_placeholders: HashSet<DynamicPlaceholder> = HashSet::new();
1494
1495 let flows_data: Vec<serde_json::Value> = flows.iter().map(|f| {
1496 let sanitized_name = K6ScriptGenerator::sanitize_js_identifier(&f.name);
1498 serde_json::json!({
1499 "name": sanitized_name.clone(), "display_name": f.name, "base_path": f.base_path,
1502 "steps": f.steps.iter().enumerate().map(|(idx, s)| {
1503 let parts: Vec<&str> = s.operation.splitn(2, ' ').collect();
1505 let method_raw = if !parts.is_empty() {
1506 parts[0].to_uppercase()
1507 } else {
1508 "GET".to_string()
1509 };
1510 let method = if !parts.is_empty() {
1511 let m = parts[0].to_lowercase();
1512 if m == "delete" { "del".to_string() } else { m }
1514 } else {
1515 "get".to_string()
1516 };
1517 let raw_path = if parts.len() >= 2 { parts[1] } else { "/" };
1518 let path = if let Some(ref bp) = api_base_path {
1520 format!("{}{}", bp, raw_path)
1521 } else {
1522 raw_path.to_string()
1523 };
1524 let is_get_or_head = method == "get" || method == "head";
1525 let has_body = matches!(method.as_str(), "post" | "put" | "patch");
1527
1528 let body_value = if has_body {
1530 param_overrides.as_ref()
1531 .map(|po| po.get_for_operation(None, &method_raw, raw_path))
1532 .and_then(|oo| oo.body)
1533 .unwrap_or_else(|| serde_json::json!({}))
1534 } else {
1535 serde_json::json!({})
1536 };
1537
1538 let processed_body = DynamicParamProcessor::process_json_body(&body_value);
1540 let body_has_extracted_placeholders = processed_body.value.contains("${extracted.");
1545 let body_is_dynamic = processed_body.is_dynamic || body_has_extracted_placeholders;
1546
1547 serde_json::json!({
1548 "operation": s.operation,
1549 "method": method,
1550 "path": path,
1551 "extract": s.extract,
1552 "use_values": s.use_values,
1553 "use_body": s.use_body,
1554 "merge_body": if s.merge_body.is_empty() { None } else { Some(&s.merge_body) },
1555 "inject_attacks": s.inject_attacks,
1556 "attack_types": s.attack_types,
1557 "description": s.description,
1558 "display_name": s.description.clone().unwrap_or_else(|| format!("Step {}", idx)),
1559 "is_get_or_head": is_get_or_head,
1560 "has_body": has_body,
1561 "body": processed_body.value,
1562 "body_is_dynamic": body_is_dynamic,
1563 "_placeholders": processed_body.placeholders.iter().map(|p| format!("{:?}", p)).collect::<Vec<_>>(),
1564 })
1565 }).collect::<Vec<_>>(),
1566 })
1567 }).collect();
1568
1569 for flow_data in &flows_data {
1571 if let Some(steps) = flow_data.get("steps").and_then(|s| s.as_array()) {
1572 for step in steps {
1573 if let Some(placeholders_arr) =
1574 step.get("_placeholders").and_then(|p| p.as_array())
1575 {
1576 for p_str in placeholders_arr {
1577 if let Some(p_name) = p_str.as_str() {
1578 match p_name {
1580 "VU" => {
1581 all_placeholders.insert(DynamicPlaceholder::VU);
1582 }
1583 "Iteration" => {
1584 all_placeholders.insert(DynamicPlaceholder::Iteration);
1585 }
1586 "Timestamp" => {
1587 all_placeholders.insert(DynamicPlaceholder::Timestamp);
1588 }
1589 "UUID" => {
1590 all_placeholders.insert(DynamicPlaceholder::UUID);
1591 }
1592 "Random" => {
1593 all_placeholders.insert(DynamicPlaceholder::Random);
1594 }
1595 "Counter" => {
1596 all_placeholders.insert(DynamicPlaceholder::Counter);
1597 }
1598 "Date" => {
1599 all_placeholders.insert(DynamicPlaceholder::Date);
1600 }
1601 "VuIter" => {
1602 all_placeholders.insert(DynamicPlaceholder::VuIter);
1603 }
1604 _ => {}
1605 }
1606 }
1607 }
1608 }
1609 }
1610 }
1611 }
1612
1613 let required_imports = DynamicParamProcessor::get_required_imports(&all_placeholders);
1615 let required_globals = DynamicParamProcessor::get_required_globals(&all_placeholders);
1616
1617 let invalid_data_config = self.build_invalid_data_config();
1619 let error_injection_enabled = invalid_data_config.is_some();
1620 let error_rate = self.error_rate.unwrap_or(0.0);
1621 let error_types: Vec<String> = invalid_data_config
1622 .as_ref()
1623 .map(|c| c.error_types.iter().map(|t| format!("{:?}", t)).collect())
1624 .unwrap_or_default();
1625
1626 if error_injection_enabled {
1627 TerminalReporter::print_progress(&format!(
1628 "Error injection enabled ({}% rate)",
1629 (error_rate * 100.0) as u32
1630 ));
1631 }
1632
1633 let security_testing_enabled = self.wafbench_dir.is_some() || self.security_test;
1635
1636 let data = serde_json::json!({
1637 "base_url": self.target,
1638 "flows": flows_data,
1639 "extract_fields": config.default_extract_fields,
1640 "duration_secs": duration_secs,
1641 "max_vus": self.vus,
1642 "auth_header": self.auth,
1643 "custom_headers": custom_headers,
1644 "skip_tls_verify": self.skip_tls_verify,
1645 "stages": stages.iter().map(|s| serde_json::json!({
1647 "duration": s.duration,
1648 "target": s.target,
1649 })).collect::<Vec<_>>(),
1650 "threshold_percentile": self.threshold_percentile,
1651 "threshold_ms": self.threshold_ms,
1652 "max_error_rate": self.max_error_rate,
1653 "headers": headers_json,
1654 "dynamic_imports": required_imports,
1655 "dynamic_globals": required_globals,
1656 "error_injection_enabled": error_injection_enabled,
1658 "error_rate": error_rate,
1659 "error_types": error_types,
1660 "security_testing_enabled": security_testing_enabled,
1662 });
1663
1664 let mut script = handlebars
1665 .render_template(template, &data)
1666 .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))?;
1667
1668 if security_testing_enabled {
1670 script = self.generate_enhanced_script(&script)?;
1671 }
1672
1673 TerminalReporter::print_progress("Validating CRUD flow script...");
1675 let validation_errors = K6ScriptGenerator::validate_script(&script);
1676 if !validation_errors.is_empty() {
1677 TerminalReporter::print_error("CRUD flow script validation failed");
1678 for error in &validation_errors {
1679 eprintln!(" {}", error);
1680 }
1681 return Err(BenchError::Other(format!(
1682 "CRUD flow script validation failed with {} error(s)",
1683 validation_errors.len()
1684 )));
1685 }
1686
1687 TerminalReporter::print_success("CRUD flow script generated");
1688
1689 let script_path = if let Some(output) = &self.script_output {
1691 output.clone()
1692 } else {
1693 self.output.join("k6-crud-flow-script.js")
1694 };
1695
1696 if let Some(parent) = script_path.parent() {
1697 std::fs::create_dir_all(parent)?;
1698 }
1699 std::fs::write(&script_path, &script)?;
1700 TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
1701
1702 if self.generate_only {
1703 println!("\nScript generated successfully. Run it with:");
1704 println!(" k6 run {}", script_path.display());
1705 return Ok(());
1706 }
1707
1708 TerminalReporter::print_progress("Executing CRUD flow test...");
1710 let executor = K6Executor::new()?;
1711 std::fs::create_dir_all(&self.output)?;
1712
1713 let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
1714
1715 let duration_secs = Self::parse_duration(&self.duration)?;
1716 TerminalReporter::print_summary(&results, duration_secs);
1717
1718 Ok(())
1719 }
1720
1721 async fn execute_owasp_test(&self, parser: &SpecParser) -> Result<()> {
1723 TerminalReporter::print_progress("OWASP API Security Top 10 Testing Mode");
1724
1725 let custom_headers = self.parse_headers()?;
1727
1728 let mut config = OwaspApiConfig::new()
1730 .with_auth_header(&self.owasp_auth_header)
1731 .with_verbose(self.verbose)
1732 .with_insecure(self.skip_tls_verify)
1733 .with_concurrency(self.vus as usize)
1734 .with_iterations(self.owasp_iterations as usize)
1735 .with_base_path(self.base_path.clone())
1736 .with_custom_headers(custom_headers);
1737
1738 if let Some(ref token) = self.owasp_auth_token {
1740 config = config.with_valid_auth_token(token);
1741 }
1742
1743 if let Some(ref cats_str) = self.owasp_categories {
1745 let categories: Vec<OwaspCategory> = cats_str
1746 .split(',')
1747 .filter_map(|s| {
1748 let trimmed = s.trim();
1749 match trimmed.parse::<OwaspCategory>() {
1750 Ok(cat) => Some(cat),
1751 Err(e) => {
1752 TerminalReporter::print_warning(&e);
1753 None
1754 }
1755 }
1756 })
1757 .collect();
1758
1759 if !categories.is_empty() {
1760 config = config.with_categories(categories);
1761 }
1762 }
1763
1764 if let Some(ref admin_paths_file) = self.owasp_admin_paths {
1766 config.admin_paths_file = Some(admin_paths_file.clone());
1767 if let Err(e) = config.load_admin_paths() {
1768 TerminalReporter::print_warning(&format!("Failed to load admin paths file: {}", e));
1769 }
1770 }
1771
1772 if let Some(ref id_fields_str) = self.owasp_id_fields {
1774 let id_fields: Vec<String> = id_fields_str
1775 .split(',')
1776 .map(|s| s.trim().to_string())
1777 .filter(|s| !s.is_empty())
1778 .collect();
1779 if !id_fields.is_empty() {
1780 config = config.with_id_fields(id_fields);
1781 }
1782 }
1783
1784 if let Some(ref report_path) = self.owasp_report {
1786 config = config.with_report_path(report_path);
1787 }
1788 if let Ok(format) = self.owasp_report_format.parse::<ReportFormat>() {
1789 config = config.with_report_format(format);
1790 }
1791
1792 let categories = config.categories_to_test();
1794 TerminalReporter::print_success(&format!(
1795 "Testing {} OWASP categories: {}",
1796 categories.len(),
1797 categories.iter().map(|c| c.cli_name()).collect::<Vec<_>>().join(", ")
1798 ));
1799
1800 if config.valid_auth_token.is_some() {
1801 TerminalReporter::print_progress("Using provided auth token for baseline requests");
1802 }
1803
1804 TerminalReporter::print_progress("Generating OWASP security test script...");
1806 let generator = OwaspApiGenerator::new(config, self.target.clone(), parser);
1807
1808 let script = generator.generate()?;
1810 TerminalReporter::print_success("OWASP security test script generated");
1811
1812 let script_path = if let Some(output) = &self.script_output {
1814 output.clone()
1815 } else {
1816 self.output.join("k6-owasp-security-test.js")
1817 };
1818
1819 if let Some(parent) = script_path.parent() {
1820 std::fs::create_dir_all(parent)?;
1821 }
1822 std::fs::write(&script_path, &script)?;
1823 TerminalReporter::print_success(&format!("Script written to: {}", script_path.display()));
1824
1825 if self.generate_only {
1827 println!("\nOWASP security test script generated. Run it with:");
1828 println!(" k6 run {}", script_path.display());
1829 return Ok(());
1830 }
1831
1832 TerminalReporter::print_progress("Executing OWASP security tests...");
1834 let executor = K6Executor::new()?;
1835 std::fs::create_dir_all(&self.output)?;
1836
1837 let results = executor.execute(&script_path, Some(&self.output), self.verbose).await?;
1838
1839 let duration_secs = Self::parse_duration(&self.duration)?;
1840 TerminalReporter::print_summary(&results, duration_secs);
1841
1842 println!("\nOWASP security test results saved to: {}", self.output.display());
1843
1844 Ok(())
1845 }
1846}
1847
1848#[cfg(test)]
1849mod tests {
1850 use super::*;
1851
1852 #[test]
1853 fn test_parse_duration() {
1854 assert_eq!(BenchCommand::parse_duration("30s").unwrap(), 30);
1855 assert_eq!(BenchCommand::parse_duration("5m").unwrap(), 300);
1856 assert_eq!(BenchCommand::parse_duration("1h").unwrap(), 3600);
1857 assert_eq!(BenchCommand::parse_duration("60").unwrap(), 60);
1858 }
1859
1860 #[test]
1861 fn test_parse_duration_invalid() {
1862 assert!(BenchCommand::parse_duration("invalid").is_err());
1863 assert!(BenchCommand::parse_duration("30x").is_err());
1864 }
1865
1866 #[test]
1867 fn test_parse_headers() {
1868 let cmd = BenchCommand {
1869 spec: vec![PathBuf::from("test.yaml")],
1870 spec_dir: None,
1871 merge_conflicts: "error".to_string(),
1872 spec_mode: "merge".to_string(),
1873 dependency_config: None,
1874 target: "http://localhost".to_string(),
1875 base_path: None,
1876 duration: "1m".to_string(),
1877 vus: 10,
1878 scenario: "ramp-up".to_string(),
1879 operations: None,
1880 exclude_operations: None,
1881 auth: None,
1882 headers: Some("X-API-Key:test123,X-Client-ID:client456".to_string()),
1883 output: PathBuf::from("output"),
1884 generate_only: false,
1885 script_output: None,
1886 threshold_percentile: "p(95)".to_string(),
1887 threshold_ms: 500,
1888 max_error_rate: 0.05,
1889 verbose: false,
1890 skip_tls_verify: false,
1891 targets_file: None,
1892 max_concurrency: None,
1893 results_format: "both".to_string(),
1894 params_file: None,
1895 crud_flow: false,
1896 flow_config: None,
1897 extract_fields: None,
1898 parallel_create: None,
1899 data_file: None,
1900 data_distribution: "unique-per-vu".to_string(),
1901 data_mappings: None,
1902 per_uri_control: false,
1903 error_rate: None,
1904 error_types: None,
1905 security_test: false,
1906 security_payloads: None,
1907 security_categories: None,
1908 security_target_fields: None,
1909 wafbench_dir: None,
1910 wafbench_cycle_all: false,
1911 owasp_api_top10: false,
1912 owasp_categories: None,
1913 owasp_auth_header: "Authorization".to_string(),
1914 owasp_auth_token: None,
1915 owasp_admin_paths: None,
1916 owasp_id_fields: None,
1917 owasp_report: None,
1918 owasp_report_format: "json".to_string(),
1919 owasp_iterations: 1,
1920 };
1921
1922 let headers = cmd.parse_headers().unwrap();
1923 assert_eq!(headers.get("X-API-Key"), Some(&"test123".to_string()));
1924 assert_eq!(headers.get("X-Client-ID"), Some(&"client456".to_string()));
1925 }
1926
1927 #[test]
1928 fn test_get_spec_display_name() {
1929 let cmd = BenchCommand {
1930 spec: vec![PathBuf::from("test.yaml")],
1931 spec_dir: None,
1932 merge_conflicts: "error".to_string(),
1933 spec_mode: "merge".to_string(),
1934 dependency_config: None,
1935 target: "http://localhost".to_string(),
1936 base_path: None,
1937 duration: "1m".to_string(),
1938 vus: 10,
1939 scenario: "ramp-up".to_string(),
1940 operations: None,
1941 exclude_operations: None,
1942 auth: None,
1943 headers: None,
1944 output: PathBuf::from("output"),
1945 generate_only: false,
1946 script_output: None,
1947 threshold_percentile: "p(95)".to_string(),
1948 threshold_ms: 500,
1949 max_error_rate: 0.05,
1950 verbose: false,
1951 skip_tls_verify: false,
1952 targets_file: None,
1953 max_concurrency: None,
1954 results_format: "both".to_string(),
1955 params_file: None,
1956 crud_flow: false,
1957 flow_config: None,
1958 extract_fields: None,
1959 parallel_create: None,
1960 data_file: None,
1961 data_distribution: "unique-per-vu".to_string(),
1962 data_mappings: None,
1963 per_uri_control: false,
1964 error_rate: None,
1965 error_types: None,
1966 security_test: false,
1967 security_payloads: None,
1968 security_categories: None,
1969 security_target_fields: None,
1970 wafbench_dir: None,
1971 wafbench_cycle_all: false,
1972 owasp_api_top10: false,
1973 owasp_categories: None,
1974 owasp_auth_header: "Authorization".to_string(),
1975 owasp_auth_token: None,
1976 owasp_admin_paths: None,
1977 owasp_id_fields: None,
1978 owasp_report: None,
1979 owasp_report_format: "json".to_string(),
1980 owasp_iterations: 1,
1981 };
1982
1983 assert_eq!(cmd.get_spec_display_name(), "test.yaml");
1984
1985 let cmd_multi = BenchCommand {
1987 spec: vec![PathBuf::from("a.yaml"), PathBuf::from("b.yaml")],
1988 spec_dir: None,
1989 merge_conflicts: "error".to_string(),
1990 spec_mode: "merge".to_string(),
1991 dependency_config: None,
1992 target: "http://localhost".to_string(),
1993 base_path: None,
1994 duration: "1m".to_string(),
1995 vus: 10,
1996 scenario: "ramp-up".to_string(),
1997 operations: None,
1998 exclude_operations: None,
1999 auth: None,
2000 headers: None,
2001 output: PathBuf::from("output"),
2002 generate_only: false,
2003 script_output: None,
2004 threshold_percentile: "p(95)".to_string(),
2005 threshold_ms: 500,
2006 max_error_rate: 0.05,
2007 verbose: false,
2008 skip_tls_verify: false,
2009 targets_file: None,
2010 max_concurrency: None,
2011 results_format: "both".to_string(),
2012 params_file: None,
2013 crud_flow: false,
2014 flow_config: None,
2015 extract_fields: None,
2016 parallel_create: None,
2017 data_file: None,
2018 data_distribution: "unique-per-vu".to_string(),
2019 data_mappings: None,
2020 per_uri_control: false,
2021 error_rate: None,
2022 error_types: None,
2023 security_test: false,
2024 security_payloads: None,
2025 security_categories: None,
2026 security_target_fields: None,
2027 wafbench_dir: None,
2028 wafbench_cycle_all: false,
2029 owasp_api_top10: false,
2030 owasp_categories: None,
2031 owasp_auth_header: "Authorization".to_string(),
2032 owasp_auth_token: None,
2033 owasp_admin_paths: None,
2034 owasp_id_fields: None,
2035 owasp_report: None,
2036 owasp_report_format: "json".to_string(),
2037 owasp_iterations: 1,
2038 };
2039
2040 assert_eq!(cmd_multi.get_spec_display_name(), "2 spec files");
2041 }
2042}