ship-safe 4.0.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,224 +1,234 @@
1
- /**
2
- * APIFuzzer Agent
3
- * ================
4
- *
5
- * Static analysis of API endpoints for security anti-patterns.
6
- * Checks authentication, authorization, input validation,
7
- * rate limiting, CORS, error handling, data exposure,
8
- * mass assignment, GraphQL, file uploads, and pagination.
9
- */
10
-
11
- import path from 'path';
12
- import { BaseAgent, createFinding } from './base-agent.js';
13
-
14
- const PATTERNS = [
15
- // ── Missing Authentication ─────────────────────────────────────────────────
16
- {
17
- rule: 'API_NO_AUTH_CHECK',
18
- title: 'API: Route Without Auth Check',
19
- regex: /(?:app|router)\.(?:post|put|patch|delete)\s*\(\s*['"][^'"]+['"]\s*,\s*(?:async\s+)?(?:\(req|function)/g,
20
- severity: 'high',
21
- cwe: 'CWE-306',
22
- owasp: 'A07:2021',
23
- confidence: 'medium',
24
- description: 'State-changing API route without visible auth middleware. Verify authentication is enforced.',
25
- fix: 'Add auth middleware: router.post("/api/data", authMiddleware, handler)',
26
- },
27
-
28
- // ── Input Validation ───────────────────────────────────────────────────────
29
- {
30
- rule: 'API_NO_VALIDATION',
31
- title: 'API: No Input Validation',
32
- regex: /(?:req\.body|request\.body|ctx\.request\.body)\s*(?:;|\))/g,
33
- severity: 'medium',
34
- cwe: 'CWE-20',
35
- owasp: 'A03:2021',
36
- confidence: 'low',
37
- description: 'Request body used without validation. Validate with Zod, Joi, or Yup.',
38
- fix: 'Validate: const data = schema.parse(req.body) using Zod or similar',
39
- },
40
- {
41
- rule: 'API_SPREAD_BODY',
42
- title: 'API: Spread Request Body into Operation',
43
- regex: /\.\.\.\s*(?:req\.body|request\.body|ctx\.request\.body)/g,
44
- severity: 'high',
45
- cwe: 'CWE-915',
46
- owasp: 'A01:2021',
47
- description: 'Spreading request body enables mass assignment. Only use allowed fields.',
48
- fix: 'Destructure specific fields: const { name, email } = req.body',
49
- },
50
-
51
- // ── Error Handling ─────────────────────────────────────────────────────────
52
- {
53
- rule: 'API_STACK_TRACE_RESPONSE',
54
- title: 'API: Stack Trace in Response',
55
- regex: /(?:res\.(?:json|send|status)|ctx\.body)\s*\(\s*(?:\{[^}]*(?:err\.stack|error\.stack|err\.message|error\.message)|err\b|error\b)/g,
56
- severity: 'medium',
57
- cwe: 'CWE-209',
58
- owasp: 'A05:2021',
59
- description: 'Error details sent in API response leak internal information.',
60
- fix: 'Log errors server-side. Return generic message: res.status(500).json({ error: "Internal error" })',
61
- },
62
-
63
- // ── Data Exposure ──────────────────────────────────────────────────────────
64
- {
65
- rule: 'API_EXCESSIVE_DATA',
66
- title: 'API: Returning Full Database Object',
67
- regex: /(?:res\.json|res\.send|ctx\.body)\s*\(\s*(?:user|users|record|result|data|row|document)\s*\)/g,
68
- severity: 'medium',
69
- cwe: 'CWE-200',
70
- owasp: 'A01:2021',
71
- confidence: 'low',
72
- description: 'Returning full DB objects may expose sensitive fields (password, email, etc.).',
73
- fix: 'Select specific fields: res.json({ id: user.id, name: user.name })',
74
- },
75
-
76
- // ── File Upload ────────────────────────────────────────────────────────────
77
- {
78
- rule: 'API_UNRESTRICTED_UPLOAD',
79
- title: 'API: Unrestricted File Upload',
80
- regex: /(?:multer|formidable|busboy|multiparty)\s*\(/g,
81
- severity: 'medium',
82
- cwe: 'CWE-434',
83
- owasp: 'A04:2021',
84
- confidence: 'low',
85
- description: 'File upload handler detected. Ensure file type validation, size limits, and secure storage.',
86
- fix: 'Add: fileFilter, limits: { fileSize: 5*1024*1024 }, and validate MIME type',
87
- },
88
- {
89
- rule: 'API_UPLOAD_NO_TYPE_CHECK',
90
- title: 'API: File Upload Without Type Validation',
91
- regex: /(?:originalname|filename)\s*(?:\)|;)/g,
92
- severity: 'high',
93
- cwe: 'CWE-434',
94
- owasp: 'A04:2021',
95
- confidence: 'low',
96
- description: 'File upload using original filename without type validation.',
97
- fix: 'Validate file extension and MIME type. Generate random filenames for storage.',
98
- },
99
- {
100
- rule: 'API_PATH_IN_FILENAME',
101
- title: 'API: Path Traversal in File Upload',
102
- regex: /path\.join\s*\([^)]*(?:originalname|filename|req\.file|req\.body)/g,
103
- severity: 'critical',
104
- cwe: 'CWE-22',
105
- owasp: 'A01:2021',
106
- description: 'User-supplied filename in path construction enables directory traversal.',
107
- fix: 'Generate random filename: crypto.randomUUID() + path.extname(file.originalname)',
108
- },
109
-
110
- // ── GraphQL Security ───────────────────────────────────────────────────────
111
- {
112
- rule: 'GRAPHQL_INTROSPECTION',
113
- title: 'GraphQL: Introspection Enabled',
114
- regex: /introspection\s*:\s*true/g,
115
- severity: 'medium',
116
- cwe: 'CWE-200',
117
- owasp: 'A05:2021',
118
- description: 'GraphQL introspection enabled. Exposes full schema to attackers in production.',
119
- fix: 'Disable in production: introspection: process.env.NODE_ENV !== "production"',
120
- },
121
- {
122
- rule: 'GRAPHQL_NO_DEPTH_LIMIT',
123
- title: 'GraphQL: No Query Depth Limit',
124
- regex: /(?:ApolloServer|GraphQLServer|createYoga|makeExecutableSchema)\s*\(/g,
125
- severity: 'medium',
126
- cwe: 'CWE-400',
127
- confidence: 'low',
128
- description: 'GraphQL server without query depth limiting. Enables deeply nested DoS queries.',
129
- fix: 'Add depth limiting: graphql-depth-limit or @escape.tech/graphql-armor',
130
- },
131
- {
132
- rule: 'GRAPHQL_NO_COST_ANALYSIS',
133
- title: 'GraphQL: No Query Cost Analysis',
134
- regex: /(?:typeDefs|schema)\s*[:=].*(?:Query|Mutation)\s*\{/g,
135
- severity: 'low',
136
- cwe: 'CWE-400',
137
- confidence: 'low',
138
- description: 'GraphQL schema without query cost analysis. Complex queries can cause DoS.',
139
- fix: 'Add query complexity analysis: graphql-query-complexity or graphql-armor',
140
- },
141
-
142
- // ── API Versioning & Documentation ─────────────────────────────────────────
143
- {
144
- rule: 'API_DEBUG_ENDPOINT',
145
- title: 'API: Debug/Test Endpoint in Code',
146
- regex: /(?:app|router)\.(?:get|post|all)\s*\(\s*['"]\/(?:debug|test|admin|internal|_internal|healthcheck\/debug)/gi,
147
- severity: 'high',
148
- cwe: 'CWE-489',
149
- owasp: 'A05:2021',
150
- description: 'Debug/test/admin endpoint detected. Ensure it is not accessible in production.',
151
- fix: 'Remove debug endpoints or protect with auth + environment check',
152
- },
153
-
154
- // ── Response Headers ───────────────────────────────────────────────────────
155
- {
156
- rule: 'API_NO_SECURITY_HEADERS',
157
- title: 'API: Missing Security Headers (Helmet)',
158
- regex: /app\.(?:use|listen)\s*\(/g,
159
- severity: 'low',
160
- cwe: 'CWE-693',
161
- owasp: 'A05:2021',
162
- confidence: 'low',
163
- description: 'Express app without helmet middleware. Missing security headers (CSP, HSTS, etc.).',
164
- fix: 'Add helmet: app.use(helmet()) for automatic security headers',
165
- },
166
-
167
- // ── Sensitive Data in URL ──────────────────────────────────────────────────
168
- {
169
- rule: 'API_SECRET_IN_URL',
170
- title: 'API: Sensitive Data in URL Parameters',
171
- regex: /(?:app|router)\.(?:get|post)\s*\(\s*['"][^'"]*(?::token|:apiKey|:password|:secret|:key)\b/g,
172
- severity: 'high',
173
- cwe: 'CWE-598',
174
- owasp: 'A04:2021',
175
- description: 'Sensitive data in URL parameters gets logged in server logs, browser history, and proxies.',
176
- fix: 'Move sensitive data to request headers or body',
177
- },
178
-
179
- // ── Server Configuration ───────────────────────────────────────────────────
180
- {
181
- rule: 'API_TRUST_PROXY',
182
- title: 'API: Trust Proxy Not Configured',
183
- regex: /app\.set\s*\(\s*['"]trust proxy['"]\s*,\s*true\s*\)/g,
184
- severity: 'low',
185
- cwe: 'CWE-346',
186
- confidence: 'low',
187
- description: 'trust proxy set to true trusts all proxies. Specify trusted proxy IPs.',
188
- fix: 'Set specific proxy: app.set("trust proxy", "loopback") or IP address',
189
- },
190
-
191
- // ── Denial of Service ──────────────────────────────────────────────────────
192
- {
193
- rule: 'API_LARGE_BODY_NO_LIMIT',
194
- title: 'API: No Request Body Size Limit',
195
- regex: /(?:express\.json|bodyParser\.json)\s*\(\s*\)/g,
196
- severity: 'medium',
197
- cwe: 'CWE-400',
198
- confidence: 'low',
199
- description: 'No body size limit configured. Large payloads can cause memory exhaustion.',
200
- fix: 'Set limit: express.json({ limit: "1mb" })',
201
- },
202
- ];
203
-
204
- export class APIFuzzer extends BaseAgent {
205
- constructor() {
206
- super('APIFuzzer', 'API endpoint security analysis', 'api');
207
- }
208
-
209
- async analyze(context) {
210
- const { files } = context;
211
- const codeFiles = files.filter(f => {
212
- const ext = path.extname(f).toLowerCase();
213
- return ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.rb', '.go'].includes(ext);
214
- });
215
-
216
- let findings = [];
217
- for (const file of codeFiles) {
218
- findings = findings.concat(this.scanFileWithPatterns(file, PATTERNS));
219
- }
220
- return findings;
221
- }
222
- }
223
-
224
- export default APIFuzzer;
1
+ /**
2
+ * APIFuzzer Agent
3
+ * ================
4
+ *
5
+ * Static analysis of API endpoints for security anti-patterns.
6
+ * Checks authentication, authorization, input validation,
7
+ * rate limiting, CORS, error handling, data exposure,
8
+ * mass assignment, GraphQL, file uploads, and pagination.
9
+ */
10
+
11
+ import path from 'path';
12
+ import { BaseAgent, createFinding } from './base-agent.js';
13
+
14
+ const PATTERNS = [
15
+ // ── Missing Authentication ─────────────────────────────────────────────────
16
+ {
17
+ rule: 'API_NO_AUTH_CHECK',
18
+ title: 'API: Route Without Auth Check',
19
+ regex: /(?:app|router)\.(?:post|put|patch|delete)\s*\(\s*['"][^'"]+['"]\s*,\s*(?:async\s+)?(?:\(req|function)/g,
20
+ severity: 'high',
21
+ cwe: 'CWE-306',
22
+ owasp: 'A07:2021',
23
+ confidence: 'medium',
24
+ description: 'State-changing API route without visible auth middleware. Verify authentication is enforced.',
25
+ fix: 'Add auth middleware: router.post("/api/data", authMiddleware, handler)',
26
+ },
27
+
28
+ // ── Input Validation ───────────────────────────────────────────────────────
29
+ {
30
+ rule: 'API_NO_VALIDATION',
31
+ title: 'API: No Input Validation',
32
+ regex: /(?:req\.body|request\.body|ctx\.request\.body)\s*(?:;|\))/g,
33
+ severity: 'medium',
34
+ cwe: 'CWE-20',
35
+ owasp: 'A03:2021',
36
+ confidence: 'low',
37
+ description: 'Request body used without validation. Validate with Zod, Joi, or Yup.',
38
+ fix: 'Validate: const data = schema.parse(req.body) using Zod or similar',
39
+ },
40
+ {
41
+ rule: 'API_SPREAD_BODY',
42
+ title: 'API: Spread Request Body into Operation',
43
+ regex: /\.\.\.\s*(?:req\.body|request\.body|ctx\.request\.body)/g,
44
+ severity: 'high',
45
+ cwe: 'CWE-915',
46
+ owasp: 'A01:2021',
47
+ description: 'Spreading request body enables mass assignment. Only use allowed fields.',
48
+ fix: 'Destructure specific fields: const { name, email } = req.body',
49
+ },
50
+
51
+ // ── Error Handling ─────────────────────────────────────────────────────────
52
+ {
53
+ rule: 'API_STACK_TRACE_RESPONSE',
54
+ title: 'API: Stack Trace in Response',
55
+ regex: /(?:res\.(?:json|send|status)|ctx\.body)\s*\(\s*(?:\{[^}]*(?:err\.stack|error\.stack|err\.message|error\.message)|err\b|error\b)/g,
56
+ severity: 'medium',
57
+ cwe: 'CWE-209',
58
+ owasp: 'A05:2021',
59
+ description: 'Error details sent in API response leak internal information.',
60
+ fix: 'Log errors server-side. Return generic message: res.status(500).json({ error: "Internal error" })',
61
+ },
62
+
63
+ // ── Data Exposure ──────────────────────────────────────────────────────────
64
+ {
65
+ rule: 'API_EXCESSIVE_DATA',
66
+ title: 'API: Returning Full Database Object',
67
+ regex: /(?:res\.json|res\.send|ctx\.body)\s*\(\s*(?:user|users|record|result|data|row|document)\s*\)/g,
68
+ severity: 'medium',
69
+ cwe: 'CWE-200',
70
+ owasp: 'A01:2021',
71
+ confidence: 'low',
72
+ description: 'Returning full DB objects may expose sensitive fields (password, email, etc.).',
73
+ fix: 'Select specific fields: res.json({ id: user.id, name: user.name })',
74
+ },
75
+
76
+ // ── File Upload ────────────────────────────────────────────────────────────
77
+ {
78
+ rule: 'API_UNRESTRICTED_UPLOAD',
79
+ title: 'API: Unrestricted File Upload',
80
+ regex: /(?:multer|formidable|busboy|multiparty)\s*\(/g,
81
+ severity: 'medium',
82
+ cwe: 'CWE-434',
83
+ owasp: 'A04:2021',
84
+ confidence: 'low',
85
+ description: 'File upload handler detected. Ensure file type validation, size limits, and secure storage.',
86
+ fix: 'Add: fileFilter, limits: { fileSize: 5*1024*1024 }, and validate MIME type',
87
+ },
88
+ {
89
+ rule: 'API_UPLOAD_NO_TYPE_CHECK',
90
+ title: 'API: File Upload Without Type Validation',
91
+ regex: /(?:originalname|filename)\s*(?:\)|;)/g,
92
+ severity: 'high',
93
+ cwe: 'CWE-434',
94
+ owasp: 'A04:2021',
95
+ confidence: 'low',
96
+ description: 'File upload using original filename without type validation.',
97
+ fix: 'Validate file extension and MIME type. Generate random filenames for storage.',
98
+ },
99
+ {
100
+ rule: 'API_PATH_IN_FILENAME',
101
+ title: 'API: Path Traversal in File Upload',
102
+ regex: /path\.join\s*\([^)]*(?:originalname|filename|req\.file|req\.body)/g,
103
+ severity: 'critical',
104
+ cwe: 'CWE-22',
105
+ owasp: 'A01:2021',
106
+ description: 'User-supplied filename in path construction enables directory traversal.',
107
+ fix: 'Generate random filename: crypto.randomUUID() + path.extname(file.originalname)',
108
+ },
109
+
110
+ // ── GraphQL Security ───────────────────────────────────────────────────────
111
+ {
112
+ rule: 'GRAPHQL_INTROSPECTION',
113
+ title: 'GraphQL: Introspection Enabled',
114
+ regex: /introspection\s*:\s*true/g,
115
+ severity: 'medium',
116
+ cwe: 'CWE-200',
117
+ owasp: 'A05:2021',
118
+ description: 'GraphQL introspection enabled. Exposes full schema to attackers in production.',
119
+ fix: 'Disable in production: introspection: process.env.NODE_ENV !== "production"',
120
+ },
121
+ {
122
+ rule: 'GRAPHQL_NO_DEPTH_LIMIT',
123
+ title: 'GraphQL: No Query Depth Limit',
124
+ regex: /(?:ApolloServer|GraphQLServer|createYoga|makeExecutableSchema)\s*\(/g,
125
+ severity: 'medium',
126
+ cwe: 'CWE-400',
127
+ confidence: 'low',
128
+ description: 'GraphQL server without query depth limiting. Enables deeply nested DoS queries.',
129
+ fix: 'Add depth limiting: graphql-depth-limit or @escape.tech/graphql-armor',
130
+ },
131
+ {
132
+ rule: 'GRAPHQL_NO_COST_ANALYSIS',
133
+ title: 'GraphQL: No Query Cost Analysis',
134
+ regex: /(?:typeDefs|schema)\s*[:=].*(?:Query|Mutation)\s*\{/g,
135
+ severity: 'low',
136
+ cwe: 'CWE-400',
137
+ confidence: 'low',
138
+ description: 'GraphQL schema without query cost analysis. Complex queries can cause DoS.',
139
+ fix: 'Add query complexity analysis: graphql-query-complexity or graphql-armor',
140
+ },
141
+
142
+ // ── API Versioning & Documentation ─────────────────────────────────────────
143
+ {
144
+ rule: 'API_DEBUG_ENDPOINT',
145
+ title: 'API: Debug/Test Endpoint in Code',
146
+ regex: /(?:app|router)\.(?:get|post|all)\s*\(\s*['"]\/(?:debug|test|admin|internal|_internal|healthcheck\/debug)/gi,
147
+ severity: 'high',
148
+ cwe: 'CWE-489',
149
+ owasp: 'A05:2021',
150
+ description: 'Debug/test/admin endpoint detected. Ensure it is not accessible in production.',
151
+ fix: 'Remove debug endpoints or protect with auth + environment check',
152
+ },
153
+
154
+ // ── Response Headers ───────────────────────────────────────────────────────
155
+ {
156
+ rule: 'API_NO_SECURITY_HEADERS',
157
+ title: 'API: Missing Security Headers (Helmet)',
158
+ regex: /app\.(?:use|listen)\s*\(/g,
159
+ severity: 'low',
160
+ cwe: 'CWE-693',
161
+ owasp: 'A05:2021',
162
+ confidence: 'low',
163
+ description: 'Express app without helmet middleware. Missing security headers (CSP, HSTS, etc.).',
164
+ fix: 'Add helmet: app.use(helmet()) for automatic security headers',
165
+ },
166
+
167
+ // ── Sensitive Data in URL ──────────────────────────────────────────────────
168
+ {
169
+ rule: 'API_KEY_IN_URL',
170
+ title: 'API: Secret in URL Query Parameter',
171
+ regex: /(?:url|endpoint|href)\s*[:=]\s*[`"'][^`"']*\?[^`"']*(?:key|token|secret|password|apiKey|api_key)\s*=/gi,
172
+ severity: 'high',
173
+ cwe: 'CWE-598',
174
+ owasp: 'A02:2021',
175
+ description: 'API key or secret passed in URL query parameter. URLs are logged in server logs, browser history, and proxies.',
176
+ fix: 'Move secrets to request headers (e.g., Authorization, x-api-key) instead of URL parameters.',
177
+ },
178
+ {
179
+ rule: 'API_SECRET_IN_URL',
180
+ title: 'API: Sensitive Data in URL Parameters',
181
+ regex: /(?:app|router)\.(?:get|post)\s*\(\s*['"][^'"]*(?::token|:apiKey|:password|:secret|:key)\b/g,
182
+ severity: 'high',
183
+ cwe: 'CWE-598',
184
+ owasp: 'A04:2021',
185
+ description: 'Sensitive data in URL parameters gets logged in server logs, browser history, and proxies.',
186
+ fix: 'Move sensitive data to request headers or body',
187
+ },
188
+
189
+ // ── Server Configuration ───────────────────────────────────────────────────
190
+ {
191
+ rule: 'API_TRUST_PROXY',
192
+ title: 'API: Trust Proxy Not Configured',
193
+ regex: /app\.set\s*\(\s*['"]trust proxy['"]\s*,\s*true\s*\)/g,
194
+ severity: 'low',
195
+ cwe: 'CWE-346',
196
+ confidence: 'low',
197
+ description: 'trust proxy set to true trusts all proxies. Specify trusted proxy IPs.',
198
+ fix: 'Set specific proxy: app.set("trust proxy", "loopback") or IP address',
199
+ },
200
+
201
+ // ── Denial of Service ──────────────────────────────────────────────────────
202
+ {
203
+ rule: 'API_LARGE_BODY_NO_LIMIT',
204
+ title: 'API: No Request Body Size Limit',
205
+ regex: /(?:express\.json|bodyParser\.json)\s*\(\s*\)/g,
206
+ severity: 'medium',
207
+ cwe: 'CWE-400',
208
+ confidence: 'low',
209
+ description: 'No body size limit configured. Large payloads can cause memory exhaustion.',
210
+ fix: 'Set limit: express.json({ limit: "1mb" })',
211
+ },
212
+ ];
213
+
214
+ export class APIFuzzer extends BaseAgent {
215
+ constructor() {
216
+ super('APIFuzzer', 'API endpoint security analysis', 'api');
217
+ }
218
+
219
+ async analyze(context) {
220
+ const { files } = context;
221
+ const codeFiles = files.filter(f => {
222
+ const ext = path.extname(f).toLowerCase();
223
+ return ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.rb', '.go'].includes(ext);
224
+ });
225
+
226
+ let findings = [];
227
+ for (const file of codeFiles) {
228
+ findings = findings.concat(this.scanFileWithPatterns(file, PATTERNS));
229
+ }
230
+ return findings;
231
+ }
232
+ }
233
+
234
+ export default APIFuzzer;