tryassay 0.34.0 → 0.35.1
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/bayesian/__tests__/bas-calculator.test.d.ts +1 -0
- package/dist/bayesian/__tests__/bas-calculator.test.js +63 -0
- package/dist/bayesian/__tests__/bas-calculator.test.js.map +1 -0
- package/dist/bayesian/__tests__/structural-entropy.test.d.ts +1 -0
- package/dist/bayesian/__tests__/structural-entropy.test.js +21 -0
- package/dist/bayesian/__tests__/structural-entropy.test.js.map +1 -0
- package/dist/bayesian/bas-calculator.d.ts +41 -0
- package/dist/bayesian/bas-calculator.js +198 -0
- package/dist/bayesian/bas-calculator.js.map +1 -0
- package/dist/bayesian/index.d.ts +3 -0
- package/dist/bayesian/index.js +3 -0
- package/dist/bayesian/index.js.map +1 -0
- package/dist/bayesian/structural-entropy.d.ts +12 -0
- package/dist/bayesian/structural-entropy.js +37 -0
- package/dist/bayesian/structural-entropy.js.map +1 -0
- package/dist/bayesian/types.d.ts +37 -0
- package/dist/bayesian/types.js +6 -0
- package/dist/bayesian/types.js.map +1 -0
- package/dist/cli.js +28 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/__tests__/assess-formal.test.d.ts +1 -0
- package/dist/commands/__tests__/assess-formal.test.js +72 -0
- package/dist/commands/__tests__/assess-formal.test.js.map +1 -0
- package/dist/commands/activate.d.ts +1 -0
- package/dist/commands/activate.js +48 -0
- package/dist/commands/activate.js.map +1 -0
- package/dist/commands/assess.js +100 -5
- package/dist/commands/assess.js.map +1 -1
- package/dist/commands/bas-score.d.ts +13 -0
- package/dist/commands/bas-score.js +310 -0
- package/dist/commands/bas-score.js.map +1 -0
- package/dist/commands/bounty-watch.js.map +1 -1
- package/dist/commands/hunt.js +32 -0
- package/dist/commands/hunt.js.map +1 -1
- package/dist/commands/runtime.js +11 -10
- package/dist/commands/runtime.js.map +1 -1
- package/dist/commands/stream-verify.d.ts +16 -0
- package/dist/commands/stream-verify.js +228 -0
- package/dist/commands/stream-verify.js.map +1 -0
- package/dist/commands/watch.js.map +1 -1
- package/dist/hunt/__tests__/deep-dive.test.js.map +1 -1
- package/dist/hunt/__tests__/e2e.test.js.map +1 -1
- package/dist/hunt/__tests__/finding-to-template.test.js +10 -1
- package/dist/hunt/__tests__/finding-to-template.test.js.map +1 -1
- package/dist/hunt/__tests__/orchestrator.test.js.map +1 -1
- package/dist/hunt/__tests__/templates.test.js +2 -2
- package/dist/hunt/__tests__/triage.test.js.map +1 -1
- package/dist/hunt/deep-dive.js +7 -7
- package/dist/hunt/deep-dive.js.map +1 -1
- package/dist/hunt/parse-utils.d.ts +1 -1
- package/dist/hunt/state.js.map +1 -1
- package/dist/hunt/templates/injection.js +1 -1
- package/dist/hunt/templates/injection.js.map +1 -1
- package/dist/hunt/triage.js +5 -5
- package/dist/hunt/triage.js.map +1 -1
- package/dist/lib/__tests__/arithmetic-quick-test.js +10 -9
- package/dist/lib/__tests__/arithmetic-quick-test.js.map +1 -1
- package/dist/lib/__tests__/arithmetic-real-llm-test.js +8 -8
- package/dist/lib/__tests__/arithmetic-real-llm-test.js.map +1 -1
- package/dist/lib/__tests__/formal-verifier-api-misuse.test.js +4 -3
- package/dist/lib/__tests__/formal-verifier-api-misuse.test.js.map +1 -1
- package/dist/lib/__tests__/formal-verifier-behavioral.test.d.ts +18 -0
- package/dist/lib/__tests__/formal-verifier-behavioral.test.js +576 -0
- package/dist/lib/__tests__/formal-verifier-behavioral.test.js.map +1 -0
- package/dist/lib/__tests__/formal-verifier-claimless-async.test.d.ts +1 -0
- package/dist/lib/__tests__/formal-verifier-claimless-async.test.js +154 -0
- package/dist/lib/__tests__/formal-verifier-claimless-async.test.js.map +1 -0
- package/dist/lib/__tests__/formal-verifier-claimless-quality.test.d.ts +1 -0
- package/dist/lib/__tests__/formal-verifier-claimless-quality.test.js +121 -0
- package/dist/lib/__tests__/formal-verifier-claimless-quality.test.js.map +1 -0
- package/dist/lib/__tests__/formal-verifier-claimless-realworld.test.d.ts +1 -0
- package/dist/lib/__tests__/formal-verifier-claimless-realworld.test.js +119 -0
- package/dist/lib/__tests__/formal-verifier-claimless-realworld.test.js.map +1 -0
- package/dist/lib/__tests__/formal-verifier-claimless.test.d.ts +1 -0
- package/dist/lib/__tests__/formal-verifier-claimless.test.js +667 -0
- package/dist/lib/__tests__/formal-verifier-claimless.test.js.map +1 -0
- package/dist/lib/__tests__/mr-gsm8k-benchmark.js +6 -6
- package/dist/lib/__tests__/mr-gsm8k-benchmark.js.map +1 -1
- package/dist/lib/__tests__/pr-harvester.test.js.map +1 -1
- package/dist/lib/assessment-reporter.d.ts +1 -1
- package/dist/lib/assessment-reporter.js +2 -1
- package/dist/lib/assessment-reporter.js.map +1 -1
- package/dist/lib/chain-analyzer.d.ts +4 -3
- package/dist/lib/chain-analyzer.js.map +1 -1
- package/dist/lib/formal-verifier.d.ts +20 -1
- package/dist/lib/formal-verifier.js +1182 -24
- package/dist/lib/formal-verifier.js.map +1 -1
- package/dist/lib/issue-reporter.d.ts +2 -1
- package/dist/lib/issue-reporter.js.map +1 -1
- package/dist/lib/remediation-generator.js.map +1 -1
- package/dist/lib/report-generator.js.map +1 -1
- package/dist/lib/rule-harvester/ground-truth.js +13 -2
- package/dist/lib/rule-harvester/ground-truth.js.map +1 -1
- package/dist/lib/rule-harvester/scanner.d.ts +1 -1
- package/dist/lib/user-config.d.ts +1 -0
- package/dist/lib/user-config.js.map +1 -1
- package/dist/realtime/__tests__/entropy-detector.test.d.ts +1 -0
- package/dist/realtime/__tests__/entropy-detector.test.js +200 -0
- package/dist/realtime/__tests__/entropy-detector.test.js.map +1 -0
- package/dist/realtime/__tests__/entropy-live-demo.d.ts +1 -0
- package/dist/realtime/__tests__/entropy-live-demo.js +103 -0
- package/dist/realtime/__tests__/entropy-live-demo.js.map +1 -0
- package/dist/realtime/__tests__/entropy-live.d.ts +8 -0
- package/dist/realtime/__tests__/entropy-live.js +114 -0
- package/dist/realtime/__tests__/entropy-live.js.map +1 -0
- package/dist/realtime/__tests__/streaming-checks.test.js +3 -4
- package/dist/realtime/__tests__/streaming-checks.test.js.map +1 -1
- package/dist/realtime/entropy-detector.d.ts +143 -0
- package/dist/realtime/entropy-detector.js +504 -0
- package/dist/realtime/entropy-detector.js.map +1 -0
- package/dist/realtime/mcp-server.d.ts +7 -1
- package/dist/realtime/mcp-server.js +378 -2
- package/dist/realtime/mcp-server.js.map +1 -1
- package/dist/realtime/stream-interceptor.d.ts +28 -0
- package/dist/realtime/stream-interceptor.js +204 -0
- package/dist/realtime/stream-interceptor.js.map +1 -1
- package/dist/realtime/streaming-checks.js +28 -0
- package/dist/realtime/streaming-checks.js.map +1 -1
- package/dist/realtime/streaming-verifier.d.ts +45 -0
- package/dist/realtime/streaming-verifier.js +98 -5
- package/dist/realtime/streaming-verifier.js.map +1 -1
- package/dist/realtime/types.d.ts +56 -0
- package/dist/runtime/agents/research-agent.js +10 -1
- package/dist/runtime/agents/research-agent.js.map +1 -1
- package/dist/runtime/agents/test-agent.js +10 -7
- package/dist/runtime/agents/test-agent.js.map +1 -1
- package/dist/runtime/composition-verifier.js +13 -3
- package/dist/runtime/composition-verifier.js.map +1 -1
- package/dist/runtime/fs-helpers.js.map +1 -1
- package/dist/runtime/prompt-safety-analyzer.js.map +1 -1
- package/dist/sdk/verified-generate.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/package.json +3 -2
|
@@ -175,7 +175,8 @@ function preprocessCode(code) {
|
|
|
175
175
|
const KNOWN_BAD_APIS = {
|
|
176
176
|
// JavaScript/TypeScript — non-existent array methods
|
|
177
177
|
'.flatten(': { languages: ['js', 'ts', 'jsx', 'tsx', 'javascript', 'typescript'], correct: '.flat(' },
|
|
178
|
-
|
|
178
|
+
// NOTE: .contains() is NOT a bad API — it exists on Node, DOMTokenList, DOMStringList,
|
|
179
|
+
// and Supabase query builders. Removed after real-world audit showed 100% false positive rate.
|
|
179
180
|
// Non-existent Object methods
|
|
180
181
|
'Object.fromPairs(': { languages: ['js', 'ts', 'jsx', 'tsx', 'javascript', 'typescript'], correct: 'Object.fromEntries(' },
|
|
181
182
|
'Object.pairs(': { languages: ['js', 'ts', 'jsx', 'tsx', 'javascript', 'typescript'], correct: 'Object.entries(' },
|
|
@@ -223,7 +224,7 @@ const PY_BUILTINS = new Set([
|
|
|
223
224
|
'functools', 'itertools', 'typing', 'pathlib', 'unittest', 'pytest',
|
|
224
225
|
]);
|
|
225
226
|
// ── Claim Classification ─────────────────────────────────────
|
|
226
|
-
function classifyClaim(claim) {
|
|
227
|
+
export function classifyClaim(claim) {
|
|
227
228
|
const text = `${claim.description} ${claim.assertion}`.toLowerCase();
|
|
228
229
|
const originalText = `${claim.description} ${claim.assertion}`;
|
|
229
230
|
const funcExistsMatch = originalText.match(/(?:function|method|class|constructor)\s+['"`]?(\w+)['"`]?\s+(?:exists?|is\s+defined|is\s+declared|should\s+exist)/i);
|
|
@@ -254,30 +255,43 @@ function classifyClaim(claim) {
|
|
|
254
255
|
if (/(?:parameter|argument)\s+['"`]?(\w+)['"`]?\s+(?:is|of\s+type)/i.test(text)) {
|
|
255
256
|
return { claim, formallyVerifiable: true, checkType: 'parameter_check' };
|
|
256
257
|
}
|
|
257
|
-
if (
|
|
258
|
-
/(?:
|
|
258
|
+
if (/(?:handles?\s+(?:errors?|exceptions?|failures?)|try[\s-]catch|error\s+handling|catches?\s+(?:errors?|exceptions?)|wraps?\s+in\s+try)/i.test(text) ||
|
|
259
|
+
/(?:catch(?:es)?\s+rejected\s+promises?|network\s+failures?\s+(?:are\s+)?caught)/i.test(text) ||
|
|
260
|
+
/(?:graceful(?:ly)?\s+(?:handle|fail|degrade|recover))/i.test(text)) {
|
|
259
261
|
return { claim, formallyVerifiable: true, checkType: 'error_handling' };
|
|
260
262
|
}
|
|
261
263
|
if (/(?:null|undefined|nil|none)\s+(?:check|guard|validation|handling|safety)/i.test(text) ||
|
|
262
264
|
/(?:checks?\s+(?:for\s+)?(?:null|undefined|nil|none))|(?:handles?\s+(?:null|undefined|nil|none))/i.test(text) ||
|
|
263
|
-
/(?:validates?\s+(?:that\s+)?(?:input|parameter|argument)\s+(?:is\s+not\s+)?(?:null|undefined|nil|none))/i.test(text)
|
|
265
|
+
/(?:validates?\s+(?:that\s+)?(?:input|parameter|argument)\s+(?:is\s+not\s+)?(?:null|undefined|nil|none))/i.test(text) ||
|
|
266
|
+
/(?:undefined|null)\s+(?:\w+\s+){0,3}(?:handled|checked|guarded|safe)/i.test(text) ||
|
|
267
|
+
/returns?\s+null\b/i.test(text)) {
|
|
264
268
|
return { claim, formallyVerifiable: true, checkType: 'null_check' };
|
|
265
269
|
}
|
|
266
|
-
if (
|
|
267
|
-
/(?:
|
|
270
|
+
if (/(?:type\s+annot|typed\s+(?:as|return)|return\s+type|type-safe|strongly\s+typed)/i.test(text) ||
|
|
271
|
+
/(?:typescript\s+type|have\s+(?:typescript\s+)?type\s+annotations?)/i.test(text) ||
|
|
272
|
+
/(?:return\s+types?\s+(?:are\s+)?(?:explicitly\s+)?declared)/i.test(text)) {
|
|
268
273
|
return { claim, formallyVerifiable: true, checkType: 'type_annotation' };
|
|
269
274
|
}
|
|
270
|
-
if (
|
|
271
|
-
/(?:sql\s+injection|parameterized\s+quer|prepared\s+statement|sql\s+sanitiz|query\s+parameteriz)/i.test(text)) {
|
|
275
|
+
if (/(?:sql\s+injection|parameterized\s+quer|prepared\s+statement|sql\s+sanitiz|query\s+parameteriz)/i.test(text)) {
|
|
272
276
|
return { claim, formallyVerifiable: true, checkType: 'sql_parameterized' };
|
|
273
277
|
}
|
|
274
|
-
if (/(?:validates?\s+(?:user\s+)?input|input\s+validation|sanitizes?\s+input|validates?\s+(?:before|prior))/i.test(text)
|
|
278
|
+
if (/(?:validates?\s+(?:user\s+)?input|input\s+validation|sanitizes?\s+input|validates?\s+(?:before|prior))/i.test(text) ||
|
|
279
|
+
/(?:(?:email|phone|url|format|field|body|parameter)\s+(?:is\s+)?(?:validated?|sanitized?|checked))/i.test(text) ||
|
|
280
|
+
/(?:(?:validated?|sanitized?|checked)\s+(?:on\s+the\s+)?(?:server|backend|api))/i.test(text) ||
|
|
281
|
+
/(?:request\s+body\s+(?:fields?\s+)?(?:are\s+)?(?:validated?|sanitized?|checked))/i.test(text)) {
|
|
275
282
|
return { claim, formallyVerifiable: true, checkType: 'input_validation' };
|
|
276
283
|
}
|
|
284
|
+
// Undefined reference checks — must come before arithmetic to avoid "for calculations" matching arithmetic
|
|
285
|
+
if (/(?:references?|uses?|accesses?)\s+(?:variable|function|import|module|identifier)\s+['"`]?(\w+)['"`]?/i.test(text) ||
|
|
286
|
+
/(?:variable|function|import)\s+['"`]?(\w+)['"`]?\s+(?:is\s+)?(?:defined|declared|imported|available)/i.test(text)) {
|
|
287
|
+
return { claim, formallyVerifiable: true, checkType: 'undefined_reference' };
|
|
288
|
+
}
|
|
277
289
|
// Arithmetic correctness checks
|
|
278
|
-
if (/(?:calculates?|computes?|evaluates?\s+to|sums?|totals?|multiplies?|divides?|averages?|rounds?)/i.test(text) ||
|
|
290
|
+
if (/(?:calculates?|calculation|computes?|computation|evaluates?\s+to|sums?|totals?|multiplies?|divides?|averages?|rounds?)/i.test(text) ||
|
|
279
291
|
/(?:returns?\s+(?:the\s+)?(?:sum|product|average|total|difference|ratio|percentage|remainder|quotient))/i.test(text) ||
|
|
280
|
-
/(?:result\s+(?:is|equals?|should\s+be)\s+\d)/i.test(text)
|
|
292
|
+
/(?:result\s+(?:is|equals?|should\s+be)\s+\d)/i.test(text) ||
|
|
293
|
+
/(?:\d+\s*[+\-*/]\s*\d+\s*=\s*\d)/i.test(text) ||
|
|
294
|
+
/(?:percentage|percent|ratio)\s*:\s*\d/i.test(text)) {
|
|
281
295
|
return { claim, formallyVerifiable: true, checkType: 'arithmetic_correctness' };
|
|
282
296
|
}
|
|
283
297
|
// API misuse checks
|
|
@@ -285,10 +299,100 @@ function classifyClaim(claim) {
|
|
|
285
299
|
/(?:calls?\s+\w+\.\w+)|(?:uses?\s+\w+\.\w+\s*\()/i.test(originalText)) {
|
|
286
300
|
return { claim, formallyVerifiable: true, checkType: 'api_misuse' };
|
|
287
301
|
}
|
|
288
|
-
//
|
|
289
|
-
if (/(?:
|
|
290
|
-
/(?:
|
|
291
|
-
return { claim, formallyVerifiable: true, checkType: '
|
|
302
|
+
// Capacity/limit checks — "supports up to N", "limit of N", "maximum N"
|
|
303
|
+
if (/(?:supports?\s+up\s+to|limit\s+(?:of\s+)?\d|maximum\s+(?:of\s+)?\d|up\s+to\s+\d|capacity\s+(?:of\s+)?\d)/i.test(text) ||
|
|
304
|
+
/(?:allows?\s+up\s+to|can\s+(?:hold|handle|store)\s+up\s+to)\s+\d/i.test(text)) {
|
|
305
|
+
return { claim, formallyVerifiable: true, checkType: 'capacity_limit' };
|
|
306
|
+
}
|
|
307
|
+
// Security control checks — encryption, authentication, authorization
|
|
308
|
+
if (/(?:encrypt|decrypt|hashing|hash(?:ed|es|ing)?|ssl|tls|https|cipher)/i.test(text) ||
|
|
309
|
+
/(?:authenticat|two-factor|2fa|mfa|multi-factor|single\s+sign|sso|oauth|saml|jwt)/i.test(text) ||
|
|
310
|
+
/(?:authoriz|access\s+control|permission|role-based|rbac|acl)/i.test(text) ||
|
|
311
|
+
/(?:api\s+key|token|credential|secret|certificate)/i.test(text)) {
|
|
312
|
+
return { claim, formallyVerifiable: true, checkType: 'security_control' };
|
|
313
|
+
}
|
|
314
|
+
// Compliance checks — regulatory compliance references
|
|
315
|
+
if (/(?:compl(?:y|ies|iant|iance)\s+with|gdpr|ccpa|hipaa|soc\s*2|pci[\s-]dss|iso\s*2700|ferpa|coppa)/i.test(text) ||
|
|
316
|
+
/(?:data\s+(?:protection|privacy|retention|deletion)|right\s+to\s+(?:be\s+forgotten|erasure|delete))/i.test(text)) {
|
|
317
|
+
return { claim, formallyVerifiable: true, checkType: 'compliance_check' };
|
|
318
|
+
}
|
|
319
|
+
// Connectivity checks — connections, integrations, webhooks
|
|
320
|
+
if (/(?:connects?\s+(?:to|via|using|with)|websocket|persistent\s+connection|real-time\s+(?:connection|communication))/i.test(text) ||
|
|
321
|
+
/(?:webhook|callback\s+(?:url|endpoint)|event[\s-]driven|pub[\s/]?sub|message\s+(?:queue|broker))/i.test(text) ||
|
|
322
|
+
/(?:integrat(?:es?|ion)\s+with|third[\s-]party\s+(?:api|service|integration))/i.test(text)) {
|
|
323
|
+
return { claim, formallyVerifiable: true, checkType: 'connectivity_check' };
|
|
324
|
+
}
|
|
325
|
+
// Configuration checks — configurable, customizable, settings
|
|
326
|
+
if (/(?:configur(?:able|ation|ed)|customiz(?:able|ation|ed|e)|setting|preference|option(?:al|s)?)/i.test(text) ||
|
|
327
|
+
/(?:adjustable|tuneable|parameteriz|user[\s-]defined|admin[\s-]defined)/i.test(text)) {
|
|
328
|
+
return { claim, formallyVerifiable: true, checkType: 'configuration_check' };
|
|
329
|
+
}
|
|
330
|
+
// Data handling checks — stores, processes, handles, manages data
|
|
331
|
+
if (/(?:stores?|storage|persist(?:s|ed|ent|ence)?|saves?|cach(?:es?|ing|ed))/i.test(text) ||
|
|
332
|
+
/(?:process(?:es|ed|ing)?|transform(?:s|ed|ing)?|convert(?:s|ed|ing)?|pars(?:es?|ing|ed))/i.test(text) ||
|
|
333
|
+
/(?:handles?\s+(?:data|files?|upload|download|request|response|message|event))/i.test(text) ||
|
|
334
|
+
/(?:manages?\s+(?:data|files?|state|session|queue|pool|connection))/i.test(text) ||
|
|
335
|
+
/(?:back(?:up|ed\s+up)|replicate[sd]?|mirror(?:s|ed|ing)?|(?:daily|hourly|weekly)\s+(?:backup|snapshot))/i.test(text) ||
|
|
336
|
+
/(?:retain(?:ed|s)?|retention|ttl|expir(?:es?|ation|y)|purg(?:es?|ing|ed))/i.test(text) ||
|
|
337
|
+
/(?:export(?:able|ed|s)?|delet(?:able|ed?|ion|ing)|remov(?:able|ed?|al|ing))/i.test(text)) {
|
|
338
|
+
return { claim, formallyVerifiable: true, checkType: 'data_handling' };
|
|
339
|
+
}
|
|
340
|
+
// Role/access checks — user roles, permissions, access levels
|
|
341
|
+
// Must come before pricing_billing to avoid "billing" in "excluding billing" matching pricing
|
|
342
|
+
if (/(?:(?:owner|admin|moderator|viewer|editor|manager|operator|developer)\s+role)/i.test(text) ||
|
|
343
|
+
/(?:role\s+(?:has|provides?|grants?|allows?|restricts?))/i.test(text) ||
|
|
344
|
+
/(?:full\s+access|read[\s-]?only\s+(?:access|dashboard)|restricted?\s+(?:to|access))/i.test(text) ||
|
|
345
|
+
/(?:permission|privilege|authorization|access\s+level|scoped?\s+to)/i.test(text)) {
|
|
346
|
+
return { claim, formallyVerifiable: true, checkType: 'role_access' };
|
|
347
|
+
}
|
|
348
|
+
// Pricing/billing checks — costs, prices, subscriptions, billing, refunds
|
|
349
|
+
if (/(?:costs?\s+\$|price[sd]?\s+(?:at|is|are)?\s*\$|\$\d|per\s+month|per\s+year|\/month|\/year|monthly|annually)/i.test(text) ||
|
|
350
|
+
/(?:billed?|billing|subscription|invoice|payment|refund|pro[\s-]rata|trial|free\s+trial)/i.test(text) ||
|
|
351
|
+
/(?:credit\s+card|debit\s+card|ach\s+transfer|usd|dollars?)/i.test(text) ||
|
|
352
|
+
/(?:cancel(?:lation)?|terminate|downgrade|upgrade)\s+(?:their\s+)?(?:subscription|plan|account)/i.test(text)) {
|
|
353
|
+
return { claim, formallyVerifiable: true, checkType: 'pricing_billing' };
|
|
354
|
+
}
|
|
355
|
+
// Media processing checks — image/video/photo processing, resizing, optimization
|
|
356
|
+
if (/(?:resiz(?:e[sd]?|ing)|optimiz(?:e[sd]?|ing|ation)|compress(?:ed|ion|ing)?|thumbnail)/i.test(text) ||
|
|
357
|
+
/(?:exif|gps\s+(?:data|coordinates?)|metadata|orientation|resolution|1080p|4k|hdr)/i.test(text) ||
|
|
358
|
+
/(?:(?:photo|image|video|media)\s+(?:process|analyz|transform|convert|generat|strip))/i.test(text) ||
|
|
359
|
+
/(?:ai\s+(?:moderation|analysis|detection|recognition)|content\s+(?:moderation|analysis|detection|filter))/i.test(text) ||
|
|
360
|
+
/(?:nudity|violence|hate\s+symbols?|inappropriate|offensive)\s+(?:detection|content|categor)/i.test(text) ||
|
|
361
|
+
/(?:confidence\s+score|false\s+positive|false\s+negative|sensitivity\s+threshold)/i.test(text)) {
|
|
362
|
+
return { claim, formallyVerifiable: true, checkType: 'media_processing' };
|
|
363
|
+
}
|
|
364
|
+
// Subjective quality checks — vague/subjective claims about code quality, UX
|
|
365
|
+
if (/(?:highly\s+(?:performant|available|reliable|scalable|secure))/i.test(text) ||
|
|
366
|
+
/(?:intuitive|user[\s-]?friendly|well[\s-]?(?:tested|documented|structured|designed))/i.test(text) ||
|
|
367
|
+
/(?:best\s+practices?|clean\s+code|maintainable|readable)/i.test(text) ||
|
|
368
|
+
/(?:correctly\s+handles?\s+all\s+(?:edge\s+)?cases?|reliable\s+(?:and|under))/i.test(text)) {
|
|
369
|
+
return { claim, formallyVerifiable: true, checkType: 'subjective_quality' };
|
|
370
|
+
}
|
|
371
|
+
// Requirement/constraint checks — must, requires, limited to, within N days
|
|
372
|
+
if (/(?:must\s+(?:be|have|create|meet|complete|provide|verify|include|use))/i.test(text) ||
|
|
373
|
+
/(?:requires?\s+(?:a\s+)?(?:valid|proof|minimum|at\s+least))/i.test(text) ||
|
|
374
|
+
/(?:limited\s+to|restricted\s+to|capped\s+at|within\s+\d+\s+(?:days?|hours?|minutes?|seconds?|months?))/i.test(text) ||
|
|
375
|
+
/(?:(?:is|are)\s+not\s+(?:eligible|allowed|permitted|available))/i.test(text) ||
|
|
376
|
+
/(?:do\s+not\s+require|no\s+(?:credit\s+card|account|login|installation)\s+(?:is\s+)?required)/i.test(text) ||
|
|
377
|
+
/(?:requires?\s+no\s+(?:app|account|login|installation|registration|download))/i.test(text) ||
|
|
378
|
+
/(?:(?:is|are)\s+not\s+(?:sold|shared|distributed|disclosed|transferred))/i.test(text) ||
|
|
379
|
+
/(?:will\s+be\s+(?:disconnected|disabled|suspended|terminated|revoked|removed))/i.test(text) ||
|
|
380
|
+
/(?:after\s+(?:the\s+)?(?:grace|trial|billing)\s+period)/i.test(text)) {
|
|
381
|
+
return { claim, formallyVerifiable: true, checkType: 'requirement_constraint' };
|
|
382
|
+
}
|
|
383
|
+
// Feature presence checks — broadest category, catch-all for "provides", "supports", "includes"
|
|
384
|
+
if (/(?:provides?|supports?|includes?|offers?|enables?|delivers?|features?)/i.test(text) ||
|
|
385
|
+
/(?:allows?\s+(?:users?|customers?|clients?|admins?|operators?)?\s*(?:to\s+)?)/i.test(text) ||
|
|
386
|
+
/(?:(?:users?|customers?|clients?|playlists?|devices?|events?|accounts?)\s+(?:\w+\s+){0,4}(?:may|can|are\s+able\s+to|have\s+the\s+ability|(?:may\s+)?be\s+(?:assigned|created|managed|configured|organized)))/i.test(text) ||
|
|
387
|
+
/(?:accessible|available|capable\s+of|equipped\s+with)/i.test(text) ||
|
|
388
|
+
/(?:receive[sd]?\s+(?:real[\s-]?time|push|email|sms|notification))/i.test(text) ||
|
|
389
|
+
/(?:display[sd]?\s+(?:content|data|information|photos?|images?|video))/i.test(text) ||
|
|
390
|
+
/(?:branded?\s+with|customiz(?:ed?|able)\s+with)/i.test(text) ||
|
|
391
|
+
/(?:have\s+(?:unique|dedicated|custom)\s+(?:\w+\s+)?(?:urls?|qr|codes?|links?|endpoints?))/i.test(text) ||
|
|
392
|
+
/(?:(?:is|are)\s+(?:a\s+)?(?:cloud|web|mobile|desktop|saas|platform))/i.test(text) ||
|
|
393
|
+
/(?:recurring|automatic(?:ally)?|real[\s-]?time|continuous)/i.test(text) ||
|
|
394
|
+
/(?:(?:may|can)\s+(?:submit|manage|restrict|access|control|create|modify|delete|view|export))/i.test(text)) {
|
|
395
|
+
return { claim, formallyVerifiable: true, checkType: 'feature_presence' };
|
|
292
396
|
}
|
|
293
397
|
return { claim, formallyVerifiable: false };
|
|
294
398
|
}
|
|
@@ -355,9 +459,28 @@ function checkErrorHandling(code, language) {
|
|
|
355
459
|
evidence = 'Error handling found: try/except block';
|
|
356
460
|
}
|
|
357
461
|
else if (['go', 'golang'].includes(language)) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
462
|
+
const goPatterns = [
|
|
463
|
+
/if\s+err\s*!=\s*nil/.test(clean),
|
|
464
|
+
/errors\.(?:New|Wrap|Is|As|Unwrap)\s*\(/.test(clean),
|
|
465
|
+
/fmt\.Errorf\s*\(/.test(clean),
|
|
466
|
+
/panic\s*\(/.test(clean),
|
|
467
|
+
/recover\s*\(/.test(clean),
|
|
468
|
+
];
|
|
469
|
+
hasErrorHandling = goPatterns.some(Boolean);
|
|
470
|
+
if (hasErrorHandling) {
|
|
471
|
+
const mechanisms = [];
|
|
472
|
+
if (goPatterns[0])
|
|
473
|
+
mechanisms.push('if err != nil');
|
|
474
|
+
if (goPatterns[1])
|
|
475
|
+
mechanisms.push('errors package');
|
|
476
|
+
if (goPatterns[2])
|
|
477
|
+
mechanisms.push('fmt.Errorf');
|
|
478
|
+
if (goPatterns[3])
|
|
479
|
+
mechanisms.push('panic');
|
|
480
|
+
if (goPatterns[4])
|
|
481
|
+
mechanisms.push('recover');
|
|
482
|
+
evidence = `Error handling found: ${mechanisms.join(', ')}`;
|
|
483
|
+
}
|
|
361
484
|
}
|
|
362
485
|
else if (['java', 'kotlin', 'scala'].includes(language)) {
|
|
363
486
|
hasErrorHandling = /try\s*\{/.test(clean) && /catch\s*\(/.test(clean);
|
|
@@ -365,9 +488,34 @@ function checkErrorHandling(code, language) {
|
|
|
365
488
|
evidence = 'Error handling found: try/catch block';
|
|
366
489
|
}
|
|
367
490
|
else if (['rust', 'rs'].includes(language)) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
491
|
+
const rustPatterns = [
|
|
492
|
+
/\?\s*[;,)]/.test(clean),
|
|
493
|
+
/\.unwrap_or(?:_else|_default)?\s*\(/.test(clean),
|
|
494
|
+
/match\s+.*\{[^}]*Err/.test(clean),
|
|
495
|
+
/\.map_err\s*\(/.test(clean),
|
|
496
|
+
/\.ok\(\)\s*\?/.test(clean),
|
|
497
|
+
/\.expect\s*\(/.test(clean),
|
|
498
|
+
/if\s+let\s+(?:Ok|Err|Some|None)\s*\(/.test(clean),
|
|
499
|
+
];
|
|
500
|
+
hasErrorHandling = rustPatterns.some(Boolean);
|
|
501
|
+
if (hasErrorHandling) {
|
|
502
|
+
const mechanisms = [];
|
|
503
|
+
if (rustPatterns[0])
|
|
504
|
+
mechanisms.push('? operator');
|
|
505
|
+
if (rustPatterns[1])
|
|
506
|
+
mechanisms.push('.unwrap_or*()');
|
|
507
|
+
if (rustPatterns[2])
|
|
508
|
+
mechanisms.push('match on Err');
|
|
509
|
+
if (rustPatterns[3])
|
|
510
|
+
mechanisms.push('.map_err()');
|
|
511
|
+
if (rustPatterns[4])
|
|
512
|
+
mechanisms.push('.ok()?');
|
|
513
|
+
if (rustPatterns[5])
|
|
514
|
+
mechanisms.push('.expect()');
|
|
515
|
+
if (rustPatterns[6])
|
|
516
|
+
mechanisms.push('if let Ok/Err/Some/None');
|
|
517
|
+
evidence = `Error handling found: ${mechanisms.join(', ')}`;
|
|
518
|
+
}
|
|
371
519
|
}
|
|
372
520
|
else {
|
|
373
521
|
hasErrorHandling = /try\s*[{:]/.test(clean) && /catch|except|rescue/.test(clean);
|
|
@@ -421,6 +569,36 @@ function checkNullHandling(code, language) {
|
|
|
421
569
|
if (hasNullCheck)
|
|
422
570
|
evidence = 'Null handling found: nil check';
|
|
423
571
|
}
|
|
572
|
+
else if (['rust', 'rs'].includes(language)) {
|
|
573
|
+
const rustPatterns = [
|
|
574
|
+
/\.is_some\(\)/.test(clean),
|
|
575
|
+
/\.is_none\(\)/.test(clean),
|
|
576
|
+
/\.unwrap_or/.test(clean),
|
|
577
|
+
/if\s+let\s+Some\s*\(/.test(clean),
|
|
578
|
+
/match\s+.*\{[^}]*None/.test(clean),
|
|
579
|
+
/\.map\s*\(/.test(clean) && /Option/.test(clean),
|
|
580
|
+
/\.ok_or/.test(clean),
|
|
581
|
+
];
|
|
582
|
+
hasNullCheck = rustPatterns.some(Boolean);
|
|
583
|
+
if (hasNullCheck) {
|
|
584
|
+
const mechanisms = [];
|
|
585
|
+
if (rustPatterns[0])
|
|
586
|
+
mechanisms.push('.is_some()');
|
|
587
|
+
if (rustPatterns[1])
|
|
588
|
+
mechanisms.push('.is_none()');
|
|
589
|
+
if (rustPatterns[2])
|
|
590
|
+
mechanisms.push('.unwrap_or*()');
|
|
591
|
+
if (rustPatterns[3])
|
|
592
|
+
mechanisms.push('if let Some');
|
|
593
|
+
if (rustPatterns[4])
|
|
594
|
+
mechanisms.push('match on None');
|
|
595
|
+
if (rustPatterns[5])
|
|
596
|
+
mechanisms.push('Option.map()');
|
|
597
|
+
if (rustPatterns[6])
|
|
598
|
+
mechanisms.push('.ok_or*()');
|
|
599
|
+
evidence = `Null handling found: ${mechanisms.join(', ')}`;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
424
602
|
else {
|
|
425
603
|
hasNullCheck = /(?:null|nil|None|undefined|NULL)\s*[!=]/.test(clean) || /[!=]\s*(?:null|nil|None|undefined|NULL)/.test(clean);
|
|
426
604
|
if (hasNullCheck)
|
|
@@ -467,6 +645,60 @@ function checkTypeAnnotations(code, language) {
|
|
|
467
645
|
evidence = `Type safety found: ${details.join(', ')}`;
|
|
468
646
|
}
|
|
469
647
|
}
|
|
648
|
+
else if (['go', 'golang'].includes(language)) {
|
|
649
|
+
const funcTypes = /func\s+\w+\s*\([^)]*\w+\s+\w+/.test(clean);
|
|
650
|
+
const returnTypes = /\)\s*(?:\(.*\)|[\w*]+)\s*\{/.test(clean);
|
|
651
|
+
const structFields = /\w+\s+(?:string|int|float|bool|byte|error|interface|map|func|chan|\[\]|\*\w)/.test(clean);
|
|
652
|
+
hasTypes = funcTypes || returnTypes || structFields;
|
|
653
|
+
if (hasTypes) {
|
|
654
|
+
const details = [];
|
|
655
|
+
if (funcTypes)
|
|
656
|
+
details.push('typed function parameters');
|
|
657
|
+
if (returnTypes)
|
|
658
|
+
details.push('return type declarations');
|
|
659
|
+
if (structFields)
|
|
660
|
+
details.push('typed struct fields');
|
|
661
|
+
evidence = `Type safety found: ${details.join(', ')}`;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
else if (['rust', 'rs'].includes(language)) {
|
|
665
|
+
const fnTypes = /fn\s+\w+\s*[(<].*->/.test(clean);
|
|
666
|
+
const letTypes = /let\s+(?:mut\s+)?\w+\s*:\s*\w+/.test(clean);
|
|
667
|
+
const structTypes = /struct\s+\w+/.test(clean);
|
|
668
|
+
const traitTypes = /(?:trait|impl)\s+\w+/.test(clean);
|
|
669
|
+
const genericTypes = /<\w+(?:\s*:\s*\w+)?>/.test(clean);
|
|
670
|
+
hasTypes = fnTypes || letTypes || structTypes || traitTypes || genericTypes;
|
|
671
|
+
if (hasTypes) {
|
|
672
|
+
const details = [];
|
|
673
|
+
if (fnTypes)
|
|
674
|
+
details.push('typed function signatures');
|
|
675
|
+
if (letTypes)
|
|
676
|
+
details.push('typed let bindings');
|
|
677
|
+
if (structTypes)
|
|
678
|
+
details.push('struct definitions');
|
|
679
|
+
if (traitTypes)
|
|
680
|
+
details.push('trait/impl definitions');
|
|
681
|
+
if (genericTypes)
|
|
682
|
+
details.push('generic types');
|
|
683
|
+
evidence = `Type safety found: ${details.join(', ')}`;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
else if (['java', 'kotlin', 'scala'].includes(language)) {
|
|
687
|
+
const classTypes = /class\s+\w+/.test(clean);
|
|
688
|
+
const methodTypes = /(?:public|private|protected|static)\s+\w+\s+\w+\s*\(/.test(clean);
|
|
689
|
+
const varTypes = /(?:int|long|double|float|boolean|String|List|Map|Set|char|byte|void)\s+\w+/.test(clean);
|
|
690
|
+
hasTypes = classTypes || methodTypes || varTypes;
|
|
691
|
+
if (hasTypes) {
|
|
692
|
+
const details = [];
|
|
693
|
+
if (classTypes)
|
|
694
|
+
details.push('class definitions');
|
|
695
|
+
if (methodTypes)
|
|
696
|
+
details.push('typed method signatures');
|
|
697
|
+
if (varTypes)
|
|
698
|
+
details.push('typed variable declarations');
|
|
699
|
+
evidence = `Type safety found: ${details.join(', ')}`;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
470
702
|
else {
|
|
471
703
|
hasTypes = /:\s*\w+/.test(clean) || /\w+\s+\w+\s*[=(]/.test(clean);
|
|
472
704
|
evidence = hasTypes ? 'Type annotations detected' : 'No type annotations found';
|
|
@@ -553,8 +785,18 @@ function checkSQLParameterized(code, language) {
|
|
|
553
785
|
confidence: 1,
|
|
554
786
|
};
|
|
555
787
|
}
|
|
788
|
+
// Tagged template SQL libraries (postgres.js, slonik, pglite, sql-template-strings/tag)
|
|
789
|
+
// use ${} as parameterization — not string concatenation. Suppress template literal checks.
|
|
790
|
+
const usesTaggedTemplateSql = /from\s+['"]postgres['"]/.test(codeForSQL) ||
|
|
791
|
+
/require\s*\(\s*['"]postgres['"]\s*\)/.test(codeForSQL) ||
|
|
792
|
+
/from\s+['"]slonik['"]/.test(codeForSQL) ||
|
|
793
|
+
/from\s+['"]@electric-sql\/pglite['"]/.test(codeForSQL) ||
|
|
794
|
+
/from\s+['"]sql-template-strings['"]/.test(codeForSQL) ||
|
|
795
|
+
/from\s+['"]sql-template-tag['"]/.test(codeForSQL) ||
|
|
796
|
+
/postgres\.Sql\b/.test(codeForSQL);
|
|
797
|
+
const templateLiteralConcat = /`[^`]*(?:SELECT|INSERT|UPDATE|DELETE)[^`]*\$\{(?![\d])/i.test(codeForSQL);
|
|
556
798
|
const stringConcat = /(?:SELECT|INSERT|UPDATE|DELETE)[^'"]*\+\s*\w+/i.test(codeForSQL) ||
|
|
557
|
-
|
|
799
|
+
(templateLiteralConcat && !usesTaggedTemplateSql) || // suppress if tagged template lib
|
|
558
800
|
/(?:SELECT|INSERT|UPDATE|DELETE)[^'"]*%s/i.test(codeForSQL) ||
|
|
559
801
|
/(?:SELECT|INSERT|UPDATE|DELETE)[^'"]*\.format\s*\(/i.test(codeForSQL) ||
|
|
560
802
|
/f['"][^'"]*(?:SELECT|INSERT|UPDATE|DELETE)/i.test(codeForSQL);
|
|
@@ -564,7 +806,8 @@ function checkSQLParameterized(code, language) {
|
|
|
564
806
|
/:\w+/.test(codeForSQL) ||
|
|
565
807
|
/\.prepare\s*\(/.test(clean) ||
|
|
566
808
|
/(?:prisma|drizzle|typeorm|sequelize)\./i.test(clean) ||
|
|
567
|
-
/\.query\s*\([^,]+,\s*\[/.test(clean)
|
|
809
|
+
/\.query\s*\([^,]+,\s*\[/.test(clean) ||
|
|
810
|
+
usesTaggedTemplateSql; // tagged template libs ARE parameterized
|
|
568
811
|
const safe = parameterized && !stringConcat;
|
|
569
812
|
return {
|
|
570
813
|
claimId: '',
|
|
@@ -589,6 +832,8 @@ function checkInputValidation(code, language) {
|
|
|
589
832
|
/if\s*\(\s*!?\s*\w+\s*\|\|\s*typeof/.test(clean),
|
|
590
833
|
/throw\s+new\s+(?:Error|TypeError|ValidationError|BadRequest)/i.test(clean),
|
|
591
834
|
/(?:required|minLength|maxLength|min|max|pattern|regex)\s*[:(=]/i.test(clean),
|
|
835
|
+
/\.test\s*\(\s*\w+\s*\)/.test(clean),
|
|
836
|
+
/\.match\s*\(\s*\//.test(clean),
|
|
592
837
|
];
|
|
593
838
|
const hasValidation = patterns.some(Boolean);
|
|
594
839
|
const mechanisms = [];
|
|
@@ -606,6 +851,10 @@ function checkInputValidation(code, language) {
|
|
|
606
851
|
mechanisms.push('validation error throwing');
|
|
607
852
|
if (patterns[7])
|
|
608
853
|
mechanisms.push('constraint definitions');
|
|
854
|
+
if (patterns[8])
|
|
855
|
+
mechanisms.push('regex .test() validation');
|
|
856
|
+
if (patterns[9])
|
|
857
|
+
mechanisms.push('regex .match() validation');
|
|
609
858
|
return {
|
|
610
859
|
claimId: '',
|
|
611
860
|
checkType: 'input_validation',
|
|
@@ -844,6 +1093,299 @@ function checkUndefinedReference(code, language, claim) {
|
|
|
844
1093
|
confidence: 1,
|
|
845
1094
|
};
|
|
846
1095
|
}
|
|
1096
|
+
// ── Behavioral Claim Verifiers ───────────────────────────────
|
|
1097
|
+
// These verify claims about system behavior by checking code for evidence
|
|
1098
|
+
// of the described functionality. They look for structural patterns that
|
|
1099
|
+
// indicate whether the claimed behavior is implemented.
|
|
1100
|
+
function checkFeaturePresence(code, language, claim) {
|
|
1101
|
+
const clean = preprocessCode(code);
|
|
1102
|
+
const text = `${claim.description} ${claim.assertion}`.toLowerCase();
|
|
1103
|
+
// Extract key nouns/features from the claim text
|
|
1104
|
+
const featureWords = text
|
|
1105
|
+
.replace(/(?:provides?|supports?|includes?|offers?|enables?|delivers?|features?|allows?|may|can|the|a|an|is|are|with|for|to|of|and|or|in|on|by|via|from|has|have|be|been|that|this|their|its|each|all|any|users?|customers?|clients?|service|system|application|platform|app)\b/gi, '')
|
|
1106
|
+
.trim()
|
|
1107
|
+
.split(/\s+/)
|
|
1108
|
+
.filter(w => w.length > 3);
|
|
1109
|
+
// Check if at least some of the feature-related words appear in the code
|
|
1110
|
+
// Use substring match (not word boundary) to catch camelCase like getAnalytics
|
|
1111
|
+
const matchCount = featureWords.filter(w => {
|
|
1112
|
+
try {
|
|
1113
|
+
return new RegExp(w.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'i').test(clean);
|
|
1114
|
+
}
|
|
1115
|
+
catch {
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
}).length;
|
|
1119
|
+
const matchRatio = featureWords.length > 0 ? matchCount / featureWords.length : 0;
|
|
1120
|
+
// If we find enough related terms in the code, the feature is likely present
|
|
1121
|
+
const hasEvidence = matchRatio >= 0.3 || matchCount >= 2;
|
|
1122
|
+
return {
|
|
1123
|
+
claimId: claim.id,
|
|
1124
|
+
checkType: 'feature_presence',
|
|
1125
|
+
verdict: hasEvidence ? 'PASS' : 'FAIL',
|
|
1126
|
+
evidence: hasEvidence
|
|
1127
|
+
? `Feature-related terms found in source (${matchCount}/${featureWords.length} terms match)`
|
|
1128
|
+
: `Feature-related terms not found in source (${matchCount}/${featureWords.length} terms match)`,
|
|
1129
|
+
confidence: 1,
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
function checkDataHandling(code, language, claim) {
|
|
1133
|
+
const clean = preprocessCode(code);
|
|
1134
|
+
const text = `${claim.description} ${claim.assertion}`.toLowerCase();
|
|
1135
|
+
const patterns = [
|
|
1136
|
+
{ test: /(?:fs\.|readFile|writeFile|createReadStream|createWriteStream)/i.test(clean), label: 'file I/O operations' },
|
|
1137
|
+
{ test: /(?:localStorage|sessionStorage|indexedDB|\.setItem|\.getItem)/i.test(clean), label: 'browser storage' },
|
|
1138
|
+
{ test: /(?:database|db\.|\.query|\.insert|\.update|\.delete|\.find|\.save|mongoose|prisma|drizzle|knex|sequelize)/i.test(clean), label: 'database operations' },
|
|
1139
|
+
{ test: /(?:cache|redis|memcache|lru|\.set\s*\(|\.get\s*\()/i.test(clean), label: 'caching layer' },
|
|
1140
|
+
{ test: /(?:upload|download|multer|formidable|busboy|multipart)/i.test(clean), label: 'file upload/download' },
|
|
1141
|
+
{ test: /(?:JSON\.parse|JSON\.stringify|serialize|deserialize|marshal|unmarshal)/i.test(clean), label: 'data serialization' },
|
|
1142
|
+
{ test: /(?:transform|convert|map|reduce|filter|pipe|stream)/i.test(clean), label: 'data transformation' },
|
|
1143
|
+
{ test: /(?:backup|snapshot|replicate|mirror|clone|copy)/i.test(clean), label: 'data backup/replication' },
|
|
1144
|
+
{ test: /(?:encrypt|decrypt|hash|hmac|cipher)/i.test(clean), label: 'data encryption' },
|
|
1145
|
+
{ test: /(?:queue|worker|job|task|bull|agenda|rabbitmq|kafka)/i.test(clean), label: 'message queue/worker' },
|
|
1146
|
+
];
|
|
1147
|
+
const found = patterns.filter(p => p.test);
|
|
1148
|
+
const hasDataHandling = found.length > 0;
|
|
1149
|
+
return {
|
|
1150
|
+
claimId: claim.id,
|
|
1151
|
+
checkType: 'data_handling',
|
|
1152
|
+
verdict: hasDataHandling ? 'PASS' : 'FAIL',
|
|
1153
|
+
evidence: hasDataHandling
|
|
1154
|
+
? `Data handling found: ${found.map(f => f.label).join(', ')}`
|
|
1155
|
+
: 'No data handling patterns found in source code',
|
|
1156
|
+
confidence: 1,
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
function checkConfiguration(code, language, claim) {
|
|
1160
|
+
const clean = preprocessCode(code);
|
|
1161
|
+
const patterns = [
|
|
1162
|
+
{ test: /(?:config|configuration|settings|preferences|options)\s*[=:{]/i.test(clean), label: 'configuration object' },
|
|
1163
|
+
{ test: /(?:\.env|process\.env|import\.meta\.env|os\.environ)/i.test(clean), label: 'environment variables' },
|
|
1164
|
+
{ test: /(?:default[A-Z]\w*|DEFAULT_|DEFAULTS)\s*[=:]/i.test(clean), label: 'default values' },
|
|
1165
|
+
{ test: /(?:\.config\.|\.settings\.|\.options\.|\.preferences\.)/i.test(clean), label: 'config accessors' },
|
|
1166
|
+
{ test: /(?:configurable|customizable|adjustable)/i.test(clean), label: 'configurability markers' },
|
|
1167
|
+
{ test: /(?:theme|color|font|style|layout|display)\s*[=:{]/i.test(clean), label: 'display configuration' },
|
|
1168
|
+
{ test: /(?:enable|disable|toggle|switch|flag)\s*[=:(]/i.test(clean), label: 'feature flags' },
|
|
1169
|
+
];
|
|
1170
|
+
const found = patterns.filter(p => p.test);
|
|
1171
|
+
const hasConfig = found.length > 0;
|
|
1172
|
+
return {
|
|
1173
|
+
claimId: claim.id,
|
|
1174
|
+
checkType: 'configuration_check',
|
|
1175
|
+
verdict: hasConfig ? 'PASS' : 'FAIL',
|
|
1176
|
+
evidence: hasConfig
|
|
1177
|
+
? `Configuration patterns found: ${found.map(f => f.label).join(', ')}`
|
|
1178
|
+
: 'No configuration patterns found in source code',
|
|
1179
|
+
confidence: 1,
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
function checkCapacityLimit(code, language, claim) {
|
|
1183
|
+
const clean = preprocessCode(code);
|
|
1184
|
+
const text = `${claim.description} ${claim.assertion}`;
|
|
1185
|
+
// Extract the numeric limit from the claim
|
|
1186
|
+
const limitMatch = text.match(/(?:up\s+to|limit\s+(?:of\s+)?|maximum\s+(?:of\s+)?|capacity\s+(?:of\s+)?)(\d[\d,]*)/i);
|
|
1187
|
+
const limitValue = limitMatch ? limitMatch[1].replace(/,/g, '') : null;
|
|
1188
|
+
const patterns = [
|
|
1189
|
+
{ test: /(?:MAX_|LIMIT_|CAPACITY_|MAX[A-Z])\w*\s*[=:]/i.test(clean), label: 'limit constants' },
|
|
1190
|
+
{ test: /(?:\.length\s*[<>=]|\.size\s*[<>=]|\.count\s*[<>=])/i.test(clean), label: 'size comparisons' },
|
|
1191
|
+
{ test: /(?:quota|throttle|rate[\s_-]?limit|max[\s_-]?(?:size|count|items|users|connections))/i.test(clean), label: 'capacity controls' },
|
|
1192
|
+
{ test: limitValue !== null && new RegExp(`\\b${limitValue}\\b`).test(clean), label: `limit value ${limitValue} in code` },
|
|
1193
|
+
];
|
|
1194
|
+
const found = patterns.filter(p => p.test);
|
|
1195
|
+
const hasLimit = found.length > 0;
|
|
1196
|
+
return {
|
|
1197
|
+
claimId: claim.id,
|
|
1198
|
+
checkType: 'capacity_limit',
|
|
1199
|
+
verdict: hasLimit ? 'PASS' : 'FAIL',
|
|
1200
|
+
evidence: hasLimit
|
|
1201
|
+
? `Capacity/limit patterns found: ${found.map(f => f.label).join(', ')}`
|
|
1202
|
+
: 'No capacity limit patterns found in source code',
|
|
1203
|
+
confidence: 1,
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
function checkSecurityControl(code, language, claim) {
|
|
1207
|
+
const clean = preprocessCode(code);
|
|
1208
|
+
const text = `${claim.description} ${claim.assertion}`.toLowerCase();
|
|
1209
|
+
const patterns = [
|
|
1210
|
+
{ test: /(?:crypto|bcrypt|argon2|scrypt|pbkdf2|sha256|sha512|hmac)/i.test(clean), label: 'cryptographic operations' },
|
|
1211
|
+
{ test: /(?:encrypt|decrypt|cipher|aes|rsa)/i.test(clean), label: 'encryption/decryption' },
|
|
1212
|
+
{ test: /(?:jwt|jsonwebtoken|passport|oauth|openid)/i.test(clean), label: 'authentication library' },
|
|
1213
|
+
{ test: /(?:authorize|permission|role|grant|deny|allow|forbid|access[\s_-]?control)/i.test(clean), label: 'authorization' },
|
|
1214
|
+
{ test: /(?:token|bearer|session|cookie|csrf|xsrf)/i.test(clean), label: 'session/token management' },
|
|
1215
|
+
{ test: /(?:ssl|tls|https|certificate|cert)/i.test(clean), label: 'TLS/SSL' },
|
|
1216
|
+
{ test: /(?:hash|salt|pepper|digest)/i.test(clean), label: 'hashing' },
|
|
1217
|
+
{ test: /(?:api[\s_-]?key|secret[\s_-]?key|credentials?)/i.test(clean), label: 'credential management' },
|
|
1218
|
+
{ test: /(?:mfa|two[\s_-]?factor|2fa|totp|hotp|otp)/i.test(clean), label: 'multi-factor auth' },
|
|
1219
|
+
{ test: /(?:cors|csp|helmet|content[\s_-]?security[\s_-]?policy)/i.test(clean), label: 'security headers' },
|
|
1220
|
+
];
|
|
1221
|
+
const found = patterns.filter(p => p.test);
|
|
1222
|
+
const hasSecurityControl = found.length > 0;
|
|
1223
|
+
return {
|
|
1224
|
+
claimId: claim.id,
|
|
1225
|
+
checkType: 'security_control',
|
|
1226
|
+
verdict: hasSecurityControl ? 'PASS' : 'FAIL',
|
|
1227
|
+
evidence: hasSecurityControl
|
|
1228
|
+
? `Security controls found: ${found.map(f => f.label).join(', ')}`
|
|
1229
|
+
: 'No security control patterns found in source code',
|
|
1230
|
+
confidence: 1,
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
function checkCompliance(code, language, claim) {
|
|
1234
|
+
const clean = preprocessCode(code);
|
|
1235
|
+
const text = `${claim.description} ${claim.assertion}`.toLowerCase();
|
|
1236
|
+
const patterns = [
|
|
1237
|
+
{ test: /(?:gdpr|data[\s_-]?protection|privacy[\s_-]?policy)/i.test(clean), label: 'GDPR/data protection' },
|
|
1238
|
+
{ test: /(?:consent|opt[\s_-]?(?:in|out)|cookie[\s_-]?(?:consent|banner|notice))/i.test(clean), label: 'consent management' },
|
|
1239
|
+
{ test: /(?:delete[\s_-]?(?:account|data|user)|erase|purge|anonymize|pseudonymize)/i.test(clean), label: 'data deletion/anonymization' },
|
|
1240
|
+
{ test: /(?:audit[\s_-]?(?:log|trail)|logging|tracking|compliance)/i.test(clean), label: 'audit logging' },
|
|
1241
|
+
{ test: /(?:retention|expir(?:y|ation)|ttl|data[\s_-]?lifecycle)/i.test(clean), label: 'data retention' },
|
|
1242
|
+
{ test: /(?:encrypt|hash|mask|redact|sensitive)/i.test(clean), label: 'data protection measures' },
|
|
1243
|
+
{ test: /(?:access[\s_-]?(?:log|control)|rbac|permission|role)/i.test(clean), label: 'access controls' },
|
|
1244
|
+
];
|
|
1245
|
+
const found = patterns.filter(p => p.test);
|
|
1246
|
+
const hasCompliance = found.length > 0;
|
|
1247
|
+
return {
|
|
1248
|
+
claimId: claim.id,
|
|
1249
|
+
checkType: 'compliance_check',
|
|
1250
|
+
verdict: hasCompliance ? 'PASS' : 'FAIL',
|
|
1251
|
+
evidence: hasCompliance
|
|
1252
|
+
? `Compliance-related patterns found: ${found.map(f => f.label).join(', ')}`
|
|
1253
|
+
: 'No compliance-related patterns found in source code',
|
|
1254
|
+
confidence: 1,
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
function checkConnectivity(code, language, claim) {
|
|
1258
|
+
const clean = preprocessCode(code);
|
|
1259
|
+
const patterns = [
|
|
1260
|
+
{ test: /(?:websocket|ws\.|wss:|socket\.io|\.on\s*\(\s*['"](?:connect|message|open|close))/i.test(clean), label: 'WebSocket' },
|
|
1261
|
+
{ test: /(?:webhook|callback[\s_-]?url|event[\s_-]?(?:handler|listener|emitter))/i.test(clean), label: 'webhook/event handling' },
|
|
1262
|
+
{ test: /(?:fetch|axios|got|superagent|request|http\.(?:get|post|put|delete)|XMLHttpRequest)/i.test(clean), label: 'HTTP client' },
|
|
1263
|
+
{ test: /(?:mqtt|amqp|rabbitmq|kafka|redis\.(?:pub|sub)|pubsub|nats)/i.test(clean), label: 'message broker' },
|
|
1264
|
+
{ test: /(?:grpc|protobuf|graphql|rest[\s_-]?api|api[\s_-]?endpoint)/i.test(clean), label: 'API protocol' },
|
|
1265
|
+
{ test: /(?:sse|server[\s_-]?sent|EventSource|text\/event-stream)/i.test(clean), label: 'server-sent events' },
|
|
1266
|
+
{ test: /(?:\.connect\s*\(|\.listen\s*\(|createServer|createConnection)/i.test(clean), label: 'connection management' },
|
|
1267
|
+
];
|
|
1268
|
+
const found = patterns.filter(p => p.test);
|
|
1269
|
+
const hasConnectivity = found.length > 0;
|
|
1270
|
+
return {
|
|
1271
|
+
claimId: claim.id,
|
|
1272
|
+
checkType: 'connectivity_check',
|
|
1273
|
+
verdict: hasConnectivity ? 'PASS' : 'FAIL',
|
|
1274
|
+
evidence: hasConnectivity
|
|
1275
|
+
? `Connectivity patterns found: ${found.map(f => f.label).join(', ')}`
|
|
1276
|
+
: 'No connectivity patterns found in source code',
|
|
1277
|
+
confidence: 1,
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
function checkPricingBilling(code, language, claim) {
|
|
1281
|
+
const clean = preprocessCode(code);
|
|
1282
|
+
const text = `${claim.description} ${claim.assertion}`;
|
|
1283
|
+
// Extract price from claim
|
|
1284
|
+
const priceMatch = text.match(/\$\s*([\d,]+(?:\.\d{2})?)/);
|
|
1285
|
+
const priceValue = priceMatch ? priceMatch[1].replace(/,/g, '') : null;
|
|
1286
|
+
const patterns = [
|
|
1287
|
+
{ test: /(?:price|cost|amount|rate|fee|charge)\s*[=:]/i.test(clean), label: 'price definitions' },
|
|
1288
|
+
{ test: /(?:stripe|paypal|braintree|paddle|lemon[\s_-]?squeezy|billing)/i.test(clean), label: 'payment provider' },
|
|
1289
|
+
{ test: /(?:subscription|plan|tier|pricing)/i.test(clean), label: 'subscription logic' },
|
|
1290
|
+
{ test: /(?:invoice|receipt|charge|refund|credit)/i.test(clean), label: 'billing operations' },
|
|
1291
|
+
{ test: /(?:trial|free[\s_-]?tier|freemium)/i.test(clean), label: 'trial/free tier' },
|
|
1292
|
+
{ test: priceValue !== null && new RegExp(`\\b${priceValue}\\b`).test(clean), label: `price value $${priceValue} in code` },
|
|
1293
|
+
];
|
|
1294
|
+
const found = patterns.filter(p => p.test);
|
|
1295
|
+
const hasPricing = found.length > 0;
|
|
1296
|
+
return {
|
|
1297
|
+
claimId: claim.id,
|
|
1298
|
+
checkType: 'pricing_billing',
|
|
1299
|
+
verdict: hasPricing ? 'PASS' : 'FAIL',
|
|
1300
|
+
evidence: hasPricing
|
|
1301
|
+
? `Pricing/billing patterns found: ${found.map(f => f.label).join(', ')}`
|
|
1302
|
+
: 'No pricing/billing patterns found in source code',
|
|
1303
|
+
confidence: 1,
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
function checkRoleAccess(code, language, claim) {
|
|
1307
|
+
const clean = preprocessCode(code);
|
|
1308
|
+
const patterns = [
|
|
1309
|
+
{ test: /(?:role|permission|privilege|access[\s_-]?level)/i.test(clean), label: 'role/permission definitions' },
|
|
1310
|
+
{ test: /(?:admin|moderator|viewer|editor|owner|manager|operator)/i.test(clean), label: 'role names' },
|
|
1311
|
+
{ test: /(?:can(?:Access|Edit|Delete|View|Manage)|has(?:Permission|Role|Access))/i.test(clean), label: 'permission checks' },
|
|
1312
|
+
{ test: /(?:authorize|isAuthorized|checkPermission|requireRole|guardRole)/i.test(clean), label: 'authorization logic' },
|
|
1313
|
+
{ test: /(?:rbac|acl|policy|grant|deny|allow|forbid)/i.test(clean), label: 'access control' },
|
|
1314
|
+
];
|
|
1315
|
+
const found = patterns.filter(p => p.test);
|
|
1316
|
+
const hasRoleAccess = found.length > 0;
|
|
1317
|
+
return {
|
|
1318
|
+
claimId: claim.id,
|
|
1319
|
+
checkType: 'role_access',
|
|
1320
|
+
verdict: hasRoleAccess ? 'PASS' : 'FAIL',
|
|
1321
|
+
evidence: hasRoleAccess
|
|
1322
|
+
? `Role/access patterns found: ${found.map(f => f.label).join(', ')}`
|
|
1323
|
+
: 'No role/access patterns found in source code',
|
|
1324
|
+
confidence: 1,
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
function checkMediaProcessing(code, language, claim) {
|
|
1328
|
+
const clean = preprocessCode(code);
|
|
1329
|
+
const patterns = [
|
|
1330
|
+
{ test: /(?:sharp|jimp|canvas|imagemagick|gm|pillow|opencv)/i.test(clean), label: 'image processing library' },
|
|
1331
|
+
{ test: /(?:resize|crop|rotate|flip|thumbnail|scale)/i.test(clean), label: 'image transformation' },
|
|
1332
|
+
{ test: /(?:exif|metadata|orientation|gps|geolocation)/i.test(clean), label: 'metadata handling' },
|
|
1333
|
+
{ test: /(?:rekognition|vision|detect|moderate|classify|label)/i.test(clean), label: 'AI/ML analysis' },
|
|
1334
|
+
{ test: /(?:confidence|score|threshold|probability)/i.test(clean), label: 'confidence scoring' },
|
|
1335
|
+
{ test: /(?:upload|download|stream|buffer|blob|file)/i.test(clean), label: 'file handling' },
|
|
1336
|
+
{ test: /(?:compress|optimize|quality|format|convert)/i.test(clean), label: 'media optimization' },
|
|
1337
|
+
{ test: /(?:ffmpeg|video|audio|transcode|encode|decode)/i.test(clean), label: 'video/audio processing' },
|
|
1338
|
+
];
|
|
1339
|
+
const found = patterns.filter(p => p.test);
|
|
1340
|
+
const hasMediaProcessing = found.length > 0;
|
|
1341
|
+
return {
|
|
1342
|
+
claimId: claim.id,
|
|
1343
|
+
checkType: 'media_processing',
|
|
1344
|
+
verdict: hasMediaProcessing ? 'PASS' : 'FAIL',
|
|
1345
|
+
evidence: hasMediaProcessing
|
|
1346
|
+
? `Media processing patterns found: ${found.map(f => f.label).join(', ')}`
|
|
1347
|
+
: 'No media processing patterns found in source code',
|
|
1348
|
+
confidence: 1,
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
function checkSubjectiveQuality(code, language, claim) {
|
|
1352
|
+
// Subjective claims cannot be formally verified with high confidence.
|
|
1353
|
+
// We classify them to route them to formal checking, but always PASS
|
|
1354
|
+
// with a note that they require human/LLM judgment.
|
|
1355
|
+
return {
|
|
1356
|
+
claimId: claim.id,
|
|
1357
|
+
checkType: 'subjective_quality',
|
|
1358
|
+
verdict: 'PASS',
|
|
1359
|
+
evidence: 'Subjective quality claim — formal check defers to code review and testing',
|
|
1360
|
+
confidence: 1,
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
function checkRequirementConstraint(code, language, claim) {
|
|
1364
|
+
const clean = preprocessCode(code);
|
|
1365
|
+
const text = `${claim.description} ${claim.assertion}`;
|
|
1366
|
+
// Extract numeric constraints from the claim
|
|
1367
|
+
const numericMatch = text.match(/(\d+)\s+(?:characters?|days?|hours?|minutes?|months?|items?|devices?|users?|members?)/i);
|
|
1368
|
+
const numericValue = numericMatch ? numericMatch[1] : null;
|
|
1369
|
+
const patterns = [
|
|
1370
|
+
{ test: /(?:require|mandatory|must|shall|enforce)/i.test(clean), label: 'requirement enforcement' },
|
|
1371
|
+
{ test: /(?:validate|check|verify|assert|guard)/i.test(clean), label: 'validation logic' },
|
|
1372
|
+
{ test: /(?:minimum|maximum|min\b|max\b|limit|constraint|threshold)/i.test(clean), label: 'constraint definitions' },
|
|
1373
|
+
{ test: /(?:if\s*\(|throw|reject|deny|forbid|prevent)/i.test(clean), label: 'constraint enforcement' },
|
|
1374
|
+
{ test: numericValue !== null && new RegExp(`\\b${numericValue}\\b`).test(clean), label: `constraint value ${numericValue} in code` },
|
|
1375
|
+
{ test: /(?:regex|pattern|match|test\s*\()/i.test(clean), label: 'pattern matching' },
|
|
1376
|
+
];
|
|
1377
|
+
const found = patterns.filter(p => p.test);
|
|
1378
|
+
const hasConstraint = found.length > 0;
|
|
1379
|
+
return {
|
|
1380
|
+
claimId: claim.id,
|
|
1381
|
+
checkType: 'requirement_constraint',
|
|
1382
|
+
verdict: hasConstraint ? 'PASS' : 'FAIL',
|
|
1383
|
+
evidence: hasConstraint
|
|
1384
|
+
? `Requirement/constraint patterns found: ${found.map(f => f.label).join(', ')}`
|
|
1385
|
+
: 'No requirement/constraint patterns found in source code',
|
|
1386
|
+
confidence: 1,
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
847
1389
|
// ── Main Entry Point ─────────────────────────────────────────
|
|
848
1390
|
export function runFormalVerification(code, language, claims, llmVerifications) {
|
|
849
1391
|
const classified = claims.map(c => classifyClaim(c));
|
|
@@ -885,6 +1427,42 @@ export function runFormalVerification(code, language, claims, llmVerifications)
|
|
|
885
1427
|
case 'undefined_reference':
|
|
886
1428
|
result = checkUndefinedReference(code, language, c.claim);
|
|
887
1429
|
break;
|
|
1430
|
+
case 'feature_presence':
|
|
1431
|
+
result = checkFeaturePresence(code, language, c.claim);
|
|
1432
|
+
break;
|
|
1433
|
+
case 'data_handling':
|
|
1434
|
+
result = checkDataHandling(code, language, c.claim);
|
|
1435
|
+
break;
|
|
1436
|
+
case 'configuration_check':
|
|
1437
|
+
result = checkConfiguration(code, language, c.claim);
|
|
1438
|
+
break;
|
|
1439
|
+
case 'capacity_limit':
|
|
1440
|
+
result = checkCapacityLimit(code, language, c.claim);
|
|
1441
|
+
break;
|
|
1442
|
+
case 'security_control':
|
|
1443
|
+
result = checkSecurityControl(code, language, c.claim);
|
|
1444
|
+
break;
|
|
1445
|
+
case 'compliance_check':
|
|
1446
|
+
result = checkCompliance(code, language, c.claim);
|
|
1447
|
+
break;
|
|
1448
|
+
case 'connectivity_check':
|
|
1449
|
+
result = checkConnectivity(code, language, c.claim);
|
|
1450
|
+
break;
|
|
1451
|
+
case 'pricing_billing':
|
|
1452
|
+
result = checkPricingBilling(code, language, c.claim);
|
|
1453
|
+
break;
|
|
1454
|
+
case 'role_access':
|
|
1455
|
+
result = checkRoleAccess(code, language, c.claim);
|
|
1456
|
+
break;
|
|
1457
|
+
case 'media_processing':
|
|
1458
|
+
result = checkMediaProcessing(code, language, c.claim);
|
|
1459
|
+
break;
|
|
1460
|
+
case 'subjective_quality':
|
|
1461
|
+
result = checkSubjectiveQuality(code, language, c.claim);
|
|
1462
|
+
break;
|
|
1463
|
+
case 'requirement_constraint':
|
|
1464
|
+
result = checkRequirementConstraint(code, language, c.claim);
|
|
1465
|
+
break;
|
|
888
1466
|
default:
|
|
889
1467
|
continue;
|
|
890
1468
|
}
|
|
@@ -932,4 +1510,584 @@ export function runFormalVerification(code, language, claims, llmVerifications)
|
|
|
932
1510
|
},
|
|
933
1511
|
};
|
|
934
1512
|
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Find async function line ranges by tracking brace depth.
|
|
1515
|
+
* Returns array of [startLine, endLine] (1-indexed).
|
|
1516
|
+
*/
|
|
1517
|
+
function findAsyncFunctionRanges(lines) {
|
|
1518
|
+
const ranges = [];
|
|
1519
|
+
let i = 0;
|
|
1520
|
+
while (i < lines.length) {
|
|
1521
|
+
const line = lines[i];
|
|
1522
|
+
// Match async function declarations or async arrow functions assigned to const/let/var
|
|
1523
|
+
if (/\basync\s+function\b/.test(line) || /\basync\s*\(/.test(line) || /\basync\s+\w+\s*=>/.test(line)) {
|
|
1524
|
+
// Find the opening brace on this line or the next few lines
|
|
1525
|
+
let braceStart = -1;
|
|
1526
|
+
let searchLine = i;
|
|
1527
|
+
while (searchLine < Math.min(i + 5, lines.length)) {
|
|
1528
|
+
if (lines[searchLine].includes('{')) {
|
|
1529
|
+
braceStart = searchLine;
|
|
1530
|
+
break;
|
|
1531
|
+
}
|
|
1532
|
+
searchLine++;
|
|
1533
|
+
}
|
|
1534
|
+
if (braceStart === -1) {
|
|
1535
|
+
i++;
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
// Count braces to find the closing brace
|
|
1539
|
+
let depth = 0;
|
|
1540
|
+
let j = braceStart;
|
|
1541
|
+
while (j < lines.length) {
|
|
1542
|
+
for (const ch of lines[j]) {
|
|
1543
|
+
if (ch === '{')
|
|
1544
|
+
depth++;
|
|
1545
|
+
else if (ch === '}') {
|
|
1546
|
+
depth--;
|
|
1547
|
+
if (depth === 0) {
|
|
1548
|
+
ranges.push([i + 1, j + 1]); // 1-indexed
|
|
1549
|
+
break;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
if (depth === 0)
|
|
1554
|
+
break;
|
|
1555
|
+
j++;
|
|
1556
|
+
}
|
|
1557
|
+
i = j + 1;
|
|
1558
|
+
}
|
|
1559
|
+
else {
|
|
1560
|
+
i++;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return ranges;
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Scan code line-by-line for issues without requiring a claim object.
|
|
1567
|
+
* Suitable for development-time hooks (pre-commit, editor integration).
|
|
1568
|
+
*/
|
|
1569
|
+
export function runClaimlessChecks(code, language) {
|
|
1570
|
+
const findings = [];
|
|
1571
|
+
const lines = code.split('\n');
|
|
1572
|
+
const langLower = language.toLowerCase();
|
|
1573
|
+
const isJsTs = ['js', 'ts', 'jsx', 'tsx', 'javascript', 'typescript'].includes(langLower);
|
|
1574
|
+
// Preprocess code for pattern matching (strips comments/strings)
|
|
1575
|
+
const clean = preprocessCode(code);
|
|
1576
|
+
const cleanLines = clean.split('\n');
|
|
1577
|
+
// ── 1. SQL Injection (string concatenation in SQL context) ───
|
|
1578
|
+
if (isJsTs) {
|
|
1579
|
+
// Look for string concatenation where the string STARTS with a SQL keyword.
|
|
1580
|
+
// This avoids false positives on display strings like "Updated " + count + " records from..."
|
|
1581
|
+
// We check the original line for a string literal starting with SELECT/INSERT/UPDATE/DELETE.
|
|
1582
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
1583
|
+
const cl = cleanLines[i];
|
|
1584
|
+
// Must have string concatenation
|
|
1585
|
+
if (/"__STR__"\s*\+\s*\w/.test(cl) ||
|
|
1586
|
+
/\w\s*\+\s*"__STR__"/.test(cl) ||
|
|
1587
|
+
/`__STR__`\s*\+\s*\w/.test(cl) ||
|
|
1588
|
+
/\w\s*\+\s*`__STR__`/.test(cl)) {
|
|
1589
|
+
// Check if the original line has a string that STARTS with a SQL command
|
|
1590
|
+
// e.g., "SELECT * FROM users WHERE id = " + userId
|
|
1591
|
+
const origLine = lines[i] ?? '';
|
|
1592
|
+
if (/['"`]\s*(SELECT|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i.test(origLine)) {
|
|
1593
|
+
findings.push({
|
|
1594
|
+
checkType: 'sql_parameterized',
|
|
1595
|
+
line: i + 1,
|
|
1596
|
+
evidence: `SQL query built with string concatenation on line ${i + 1} — use parameterized queries instead`,
|
|
1597
|
+
severity: 'critical',
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
// ── 2. API Misuse (KNOWN_BAD_APIS) ───────────────────────────
|
|
1604
|
+
for (const [badApi, info] of Object.entries(KNOWN_BAD_APIS)) {
|
|
1605
|
+
if (info.languages.length === 0)
|
|
1606
|
+
continue;
|
|
1607
|
+
if (!info.languages.includes(langLower))
|
|
1608
|
+
continue;
|
|
1609
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
1610
|
+
if (cleanLines[i].includes(badApi)) {
|
|
1611
|
+
findings.push({
|
|
1612
|
+
checkType: 'api_misuse',
|
|
1613
|
+
line: i + 1,
|
|
1614
|
+
evidence: `Line ${i + 1} uses '${badApi.replace(/[()]/g, '')}' which does not exist in ${language}. Use '${info.correct}' instead.`,
|
|
1615
|
+
severity: 'critical',
|
|
1616
|
+
});
|
|
1617
|
+
break; // One finding per bad API pattern
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
// ── 3. Error Handling (async without try/catch) ──────────────
|
|
1622
|
+
if (isJsTs) {
|
|
1623
|
+
const asyncRanges = findAsyncFunctionRanges(lines);
|
|
1624
|
+
for (const [startLine, endLine] of asyncRanges) {
|
|
1625
|
+
// Extract the function body (1-indexed → 0-indexed slice)
|
|
1626
|
+
const bodyLines = cleanLines.slice(startLine - 1, endLine);
|
|
1627
|
+
const bodyText = bodyLines.join('\n');
|
|
1628
|
+
const hasAwait = /\bawait\b/.test(bodyText);
|
|
1629
|
+
const hasTryCatch = /\btry\s*\{/.test(bodyText) && /\}\s*catch\s*\(/.test(bodyText);
|
|
1630
|
+
const hasDotCatch = /\.catch\s*\(/.test(bodyText);
|
|
1631
|
+
if (hasAwait && !hasTryCatch && !hasDotCatch) {
|
|
1632
|
+
findings.push({
|
|
1633
|
+
checkType: 'error_handling',
|
|
1634
|
+
line: startLine,
|
|
1635
|
+
evidence: `Async function starting at line ${startLine} uses 'await' without try/catch error handling`,
|
|
1636
|
+
severity: 'high',
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
// ── 4. Input Validation (unvalidated req.body/params/query) ──
|
|
1642
|
+
if (isJsTs) {
|
|
1643
|
+
// Detect direct property access from req.body / req.params / req.query
|
|
1644
|
+
// without any validation (zod, joi, express-validator, manual checks)
|
|
1645
|
+
const hasValidation = /(?:z\.|joi\.|Joi\.|validate\(|schema\.|\.parse\(|\.safeParse\(|\.check\(|\.validate\()/.test(clean);
|
|
1646
|
+
if (!hasValidation) {
|
|
1647
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
1648
|
+
const cl = cleanLines[i];
|
|
1649
|
+
if (/\breq\.(body|params|query)\b/.test(cl)) {
|
|
1650
|
+
findings.push({
|
|
1651
|
+
checkType: 'input_validation',
|
|
1652
|
+
line: i + 1,
|
|
1653
|
+
evidence: `Line ${i + 1} accesses req.body/params/query without apparent input validation (no zod/joi/validate pattern found)`,
|
|
1654
|
+
severity: 'high',
|
|
1655
|
+
});
|
|
1656
|
+
break; // One finding per file for this pattern
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
// ── 5. Null Safety (non-null assertions without guard) ───────
|
|
1662
|
+
if (isJsTs) {
|
|
1663
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
1664
|
+
const cl = cleanLines[i];
|
|
1665
|
+
// TypeScript non-null assertion !. without a preceding null check
|
|
1666
|
+
if (/\w+!\.\w+/.test(cl)) {
|
|
1667
|
+
// Skip if line also has a null check
|
|
1668
|
+
if (!/(?:\?\?|&&|\?\.)/.test(cl)) {
|
|
1669
|
+
findings.push({
|
|
1670
|
+
checkType: 'null_check',
|
|
1671
|
+
line: i + 1,
|
|
1672
|
+
evidence: `Line ${i + 1} uses non-null assertion (!.) without a null guard — consider optional chaining (?.) instead`,
|
|
1673
|
+
severity: 'medium',
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
// ── 6. Security Controls (eval, innerHTML assignment, hardcoded secrets) ──
|
|
1680
|
+
if (isJsTs) {
|
|
1681
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
1682
|
+
const cl = cleanLines[i];
|
|
1683
|
+
const orig = lines[i] ?? '';
|
|
1684
|
+
// eval() usage
|
|
1685
|
+
if (/\beval\s*\(/.test(cl)) {
|
|
1686
|
+
findings.push({
|
|
1687
|
+
checkType: 'security_control',
|
|
1688
|
+
line: i + 1,
|
|
1689
|
+
evidence: `Line ${i + 1} uses eval() — arbitrary code execution risk`,
|
|
1690
|
+
severity: 'critical',
|
|
1691
|
+
});
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
// innerHTML assignment (write, not read)
|
|
1695
|
+
if (/\.innerHTML\s*=/.test(cl)) {
|
|
1696
|
+
findings.push({
|
|
1697
|
+
checkType: 'security_control',
|
|
1698
|
+
line: i + 1,
|
|
1699
|
+
evidence: `Line ${i + 1} assigns to innerHTML — XSS risk, use textContent or a sanitizer instead`,
|
|
1700
|
+
severity: 'critical',
|
|
1701
|
+
});
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1704
|
+
// Hardcoded secrets — check original line (preprocessCode strips string contents)
|
|
1705
|
+
// Pattern: assignment of a value starting with known secret prefixes, or key=value with secret keyword
|
|
1706
|
+
if (/\bsk_live_[A-Za-z0-9]+/.test(orig) ||
|
|
1707
|
+
/\bsk_test_[A-Za-z0-9]+/.test(orig) ||
|
|
1708
|
+
/(?:api_key|password|secret)\s*[=:]\s*["'`][^"'`]{8,}/.test(orig)) {
|
|
1709
|
+
findings.push({
|
|
1710
|
+
checkType: 'security_control',
|
|
1711
|
+
line: i + 1,
|
|
1712
|
+
evidence: `Line ${i + 1} appears to contain a hardcoded secret or API key — use environment variables instead`,
|
|
1713
|
+
severity: 'critical',
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
// ── 7. Data Handling (JSON.parse without try/catch) ───────────
|
|
1719
|
+
if (isJsTs) {
|
|
1720
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
1721
|
+
if (/\bJSON\.parse\s*\(/.test(cleanLines[i])) {
|
|
1722
|
+
// Check up to 10 lines above for an enclosing try {
|
|
1723
|
+
const windowStart = Math.max(0, i - 10);
|
|
1724
|
+
const context = cleanLines.slice(windowStart, i + 1).join('\n');
|
|
1725
|
+
if (!/\btry\s*\{/.test(context)) {
|
|
1726
|
+
findings.push({
|
|
1727
|
+
checkType: 'data_handling',
|
|
1728
|
+
line: i + 1,
|
|
1729
|
+
evidence: `Line ${i + 1} calls JSON.parse() without a surrounding try/catch — throws on invalid JSON`,
|
|
1730
|
+
severity: 'high',
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
// ── 8. Configuration Check (process.env without fallback) ─────
|
|
1737
|
+
if (isJsTs) {
|
|
1738
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
1739
|
+
const cl = cleanLines[i];
|
|
1740
|
+
if (/\bprocess\.env\.\w+/.test(cl)) {
|
|
1741
|
+
// Passes if line has ?? or || or ternary (? ... :)
|
|
1742
|
+
if (!/(?:\?\?|\|\||\?\s*[^?])/.test(cl)) {
|
|
1743
|
+
findings.push({
|
|
1744
|
+
checkType: 'configuration_check',
|
|
1745
|
+
line: i + 1,
|
|
1746
|
+
evidence: `Line ${i + 1} reads process.env without a fallback value — may be undefined at runtime`,
|
|
1747
|
+
severity: 'high',
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
// ── 9. Undefined References (deprecated package imports) ──────
|
|
1754
|
+
if (isJsTs) {
|
|
1755
|
+
const DEPRECATED_IMPORTS = ['request', 'request-promise', 'node-fetch@2'];
|
|
1756
|
+
for (const pkg of DEPRECATED_IMPORTS) {
|
|
1757
|
+
// Match exact package name in from 'pkg' or require('pkg') — not subpaths like 'request/...'
|
|
1758
|
+
const escapedPkg = pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1759
|
+
const importPattern = new RegExp(`(?:from\\s+['"]${escapedPkg}['"]|require\\s*\\(\\s*['"]${escapedPkg}['"]\\s*\\))`);
|
|
1760
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1761
|
+
if (importPattern.test(lines[i])) {
|
|
1762
|
+
findings.push({
|
|
1763
|
+
checkType: 'undefined_reference',
|
|
1764
|
+
line: i + 1,
|
|
1765
|
+
evidence: `Line ${i + 1} imports deprecated package '${pkg}' — use a maintained alternative (e.g., node-fetch v3, got, or native fetch)`,
|
|
1766
|
+
severity: 'high',
|
|
1767
|
+
});
|
|
1768
|
+
break; // One finding per deprecated package
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
// ── 10. Type Annotations (exported functions missing return type) ─
|
|
1774
|
+
// Skip .tsx files entirely — React components conventionally omit return types
|
|
1775
|
+
if (langLower === 'typescript' || langLower === 'ts') {
|
|
1776
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1777
|
+
const orig = lines[i];
|
|
1778
|
+
// Match `export function name(` or `export async function name(`
|
|
1779
|
+
// Skip `export default function` — common React/Next.js pattern that omits return type
|
|
1780
|
+
// Skip Next.js route handlers (GET, POST, PUT, DELETE, PATCH) — conventionally untyped
|
|
1781
|
+
if (/export\s+(?:async\s+)?function\s+\w+\s*\(/.test(orig) &&
|
|
1782
|
+
!/export\s+default\s+(?:async\s+)?function/.test(orig) &&
|
|
1783
|
+
!/export\s+(?:async\s+)?function\s+(?:GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s*\(/.test(orig)) {
|
|
1784
|
+
// Extract the portion from the opening paren to end of line
|
|
1785
|
+
// A return type annotation looks like `): SomeType` after the closing paren
|
|
1786
|
+
// We check if the line (or up to 2 continuation lines) contains `): `
|
|
1787
|
+
const snippet = lines.slice(i, Math.min(i + 3, lines.length)).join(' ');
|
|
1788
|
+
if (!/\)\s*:\s*\w/.test(snippet)) {
|
|
1789
|
+
findings.push({
|
|
1790
|
+
checkType: 'type_annotation',
|
|
1791
|
+
line: i + 1,
|
|
1792
|
+
evidence: `Line ${i + 1} exports a function without an explicit return type annotation`,
|
|
1793
|
+
severity: 'medium',
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
// ── 11. Connectivity Check (fetch/axios without timeout) ──────
|
|
1800
|
+
// Use original lines (not cleanLines) because preprocessCode mangles URLs with // in them
|
|
1801
|
+
if (isJsTs) {
|
|
1802
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1803
|
+
const orig = lines[i];
|
|
1804
|
+
if (/\bfetch\s*\(/.test(orig) || /\baxios\b/.test(orig)) {
|
|
1805
|
+
// Check this line plus the next 2 lines for AbortSignal / timeout / signal
|
|
1806
|
+
const window = lines.slice(i, Math.min(i + 3, lines.length)).join('\n');
|
|
1807
|
+
if (!/(?:AbortSignal|timeout|signal)/.test(window)) {
|
|
1808
|
+
findings.push({
|
|
1809
|
+
checkType: 'connectivity_check',
|
|
1810
|
+
line: i + 1,
|
|
1811
|
+
evidence: `Line ${i + 1} makes a network request without a timeout or AbortSignal — may hang indefinitely`,
|
|
1812
|
+
severity: 'medium',
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
// ── 12. Async forEach (silently drops promises) ──────────────
|
|
1819
|
+
if (isJsTs) {
|
|
1820
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
1821
|
+
if (/\.forEach\s*\(\s*async\b/.test(cleanLines[i])) {
|
|
1822
|
+
findings.push({
|
|
1823
|
+
checkType: 'error_handling',
|
|
1824
|
+
line: i + 1,
|
|
1825
|
+
evidence: `Line ${i + 1} passes async callback to forEach — promises are silently dropped. Use for...of with await instead`,
|
|
1826
|
+
severity: 'critical',
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
// ── 13. Console.log/debug in production code ───────────────
|
|
1832
|
+
if (isJsTs) {
|
|
1833
|
+
// Count total console.log/debug calls in the file
|
|
1834
|
+
// If >5, the file is likely a CLI tool, logger, or test helper — skip entirely
|
|
1835
|
+
const consoleLogCount = cleanLines.filter(l => /\bconsole\.(log|debug)\s*\(/.test(l)).length;
|
|
1836
|
+
if (consoleLogCount <= 5) {
|
|
1837
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
1838
|
+
if (/\bconsole\.(log|debug)\s*\(/.test(cleanLines[i])) {
|
|
1839
|
+
findings.push({
|
|
1840
|
+
checkType: 'debug_artifact',
|
|
1841
|
+
line: i + 1,
|
|
1842
|
+
evidence: `Line ${i + 1} has console.log/debug statement — remove before production`,
|
|
1843
|
+
severity: 'medium',
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
// ── 14. Loose equality (== null, != undefined) ─────────────
|
|
1850
|
+
if (isJsTs) {
|
|
1851
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
1852
|
+
const cl = cleanLines[i];
|
|
1853
|
+
// Skip lines that are entirely comments (preprocessCode empties them)
|
|
1854
|
+
if (/^\s*$/.test(cl))
|
|
1855
|
+
continue;
|
|
1856
|
+
// Use cleanLines to avoid matching inside comments/strings
|
|
1857
|
+
if (/[^!=]==\s*(null|undefined)\b/.test(cl) && !/===\s*(null|undefined)\b/.test(cl)) {
|
|
1858
|
+
findings.push({
|
|
1859
|
+
checkType: 'equality_check',
|
|
1860
|
+
line: i + 1,
|
|
1861
|
+
evidence: `Line ${i + 1} uses loose equality (==) with null/undefined — use strict equality (===) instead`,
|
|
1862
|
+
severity: 'medium',
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
// Check for != (but not !==)
|
|
1866
|
+
if (/(?<!=)!=\s*(null|undefined)\b/.test(cl) && !/!==\s*(null|undefined)\b/.test(cl)) {
|
|
1867
|
+
findings.push({
|
|
1868
|
+
checkType: 'equality_check',
|
|
1869
|
+
line: i + 1,
|
|
1870
|
+
evidence: `Line ${i + 1} uses loose inequality (!=) with null/undefined — use strict inequality (!==) instead`,
|
|
1871
|
+
severity: 'medium',
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
// ── 15. RegExp injection (user input to new RegExp) ────────
|
|
1877
|
+
if (isJsTs) {
|
|
1878
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
1879
|
+
const cl = cleanLines[i];
|
|
1880
|
+
// After preprocessCode, string literals become "__STR__"
|
|
1881
|
+
// Flag: new RegExp(variable) where arg is NOT a string literal
|
|
1882
|
+
if (/\bnew\s+RegExp\s*\(/.test(cl) && !/\bnew\s+RegExp\s*\(\s*"__STR__"/.test(cl)) {
|
|
1883
|
+
findings.push({
|
|
1884
|
+
checkType: 'security_control',
|
|
1885
|
+
line: i + 1,
|
|
1886
|
+
evidence: `Line ${i + 1} passes a variable to new RegExp() — user-controlled patterns can cause ReDoS. Escape input or use a literal regex`,
|
|
1887
|
+
severity: 'critical',
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
// ── 16. Promise.all without error handling ─────────────────
|
|
1893
|
+
if (isJsTs) {
|
|
1894
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
1895
|
+
if (/\bPromise\.all\s*\(/.test(cleanLines[i])) {
|
|
1896
|
+
const windowStart = Math.max(0, i - 5);
|
|
1897
|
+
const windowEnd = Math.min(cleanLines.length, i + 6);
|
|
1898
|
+
const context = cleanLines.slice(windowStart, windowEnd).join('\n');
|
|
1899
|
+
const hasCatch = /\.catch\s*\(/.test(context);
|
|
1900
|
+
const hasTryCatch = /\btry\s*\{/.test(context) && /\}\s*catch\s*\(/.test(context);
|
|
1901
|
+
if (!hasCatch && !hasTryCatch) {
|
|
1902
|
+
findings.push({
|
|
1903
|
+
checkType: 'error_handling',
|
|
1904
|
+
line: i + 1,
|
|
1905
|
+
evidence: `Line ${i + 1} uses Promise.all() without error handling — one rejection crashes all. Consider Promise.allSettled() or add .catch()`,
|
|
1906
|
+
severity: 'high',
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
// ── 17. parseInt without radix ─────────────────────────────
|
|
1913
|
+
if (isJsTs) {
|
|
1914
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
1915
|
+
if (/\bparseInt\s*\(\s*\w[^,)]*\)/.test(cleanLines[i])) {
|
|
1916
|
+
findings.push({
|
|
1917
|
+
checkType: 'api_misuse',
|
|
1918
|
+
line: i + 1,
|
|
1919
|
+
evidence: `Line ${i + 1} calls parseInt() without radix parameter — may produce unexpected results with leading zeros`,
|
|
1920
|
+
severity: 'medium',
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
// ── 18. Fetch body on GET/HEAD ─────────────────────────────
|
|
1926
|
+
if (isJsTs) {
|
|
1927
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1928
|
+
// Use original lines since preprocessCode strips string contents
|
|
1929
|
+
if (/\bfetch\s*\(/.test(lines[i])) {
|
|
1930
|
+
const windowEnd = Math.min(lines.length, i + 10);
|
|
1931
|
+
const block = lines.slice(i, windowEnd).join('\n');
|
|
1932
|
+
const hasGetOrHead = /method\s*:\s*['"](?:GET|HEAD)['"]/i.test(block);
|
|
1933
|
+
const hasBody = /\bbody\s*:/.test(block);
|
|
1934
|
+
if (hasGetOrHead && hasBody) {
|
|
1935
|
+
findings.push({
|
|
1936
|
+
checkType: 'api_misuse',
|
|
1937
|
+
line: i + 1,
|
|
1938
|
+
evidence: `Line ${i + 1} sends a body with GET/HEAD request — body is ignored by most servers`,
|
|
1939
|
+
severity: 'high',
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
// ── 19. addEventListener without cleanup in useEffect ──────
|
|
1946
|
+
if (isJsTs) {
|
|
1947
|
+
const codeText = cleanLines.join('\n');
|
|
1948
|
+
if (/\buseEffect\b/.test(codeText) && /\baddEventListener\b/.test(codeText)) {
|
|
1949
|
+
const addCount = (codeText.match(/\baddEventListener\b/g) || []).length;
|
|
1950
|
+
const removeCount = (codeText.match(/\bremoveEventListener\b/g) || []).length;
|
|
1951
|
+
if (addCount > removeCount) {
|
|
1952
|
+
// Find the first addEventListener line for the line number
|
|
1953
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
1954
|
+
if (/\baddEventListener\b/.test(cleanLines[i])) {
|
|
1955
|
+
findings.push({
|
|
1956
|
+
checkType: 'resource_leak',
|
|
1957
|
+
line: i + 1,
|
|
1958
|
+
evidence: `Line ${i + 1} has addEventListener in useEffect without matching removeEventListener — memory leak`,
|
|
1959
|
+
severity: 'high',
|
|
1960
|
+
});
|
|
1961
|
+
break;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
// ── 20. Empty catch block ──────────────────────────────────
|
|
1968
|
+
if (isJsTs) {
|
|
1969
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
1970
|
+
const cl = cleanLines[i];
|
|
1971
|
+
// catch (e) {} or catch {} on same line
|
|
1972
|
+
if (/\bcatch\s*(?:\(\s*\w*\s*\))?\s*\{\s*\}/.test(cl)) {
|
|
1973
|
+
findings.push({
|
|
1974
|
+
checkType: 'error_handling',
|
|
1975
|
+
line: i + 1,
|
|
1976
|
+
evidence: `Line ${i + 1} has empty catch block — swallows errors silently. At minimum log the error`,
|
|
1977
|
+
severity: 'high',
|
|
1978
|
+
});
|
|
1979
|
+
continue;
|
|
1980
|
+
}
|
|
1981
|
+
// catch block where { is at end of line and } is on the next line (with only whitespace)
|
|
1982
|
+
if (/\bcatch\s*(?:\(\s*\w*\s*\))?\s*\{\s*$/.test(cl)) {
|
|
1983
|
+
const nextLine = cleanLines[i + 1];
|
|
1984
|
+
if (nextLine !== undefined && /^\s*\}\s*$/.test(nextLine)) {
|
|
1985
|
+
findings.push({
|
|
1986
|
+
checkType: 'error_handling',
|
|
1987
|
+
line: i + 1,
|
|
1988
|
+
evidence: `Line ${i + 1} has empty catch block — swallows errors silently. At minimum log the error`,
|
|
1989
|
+
severity: 'high',
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
// ── 21. dangerouslySetInnerHTML ────────────────────────────
|
|
1996
|
+
if (isJsTs) {
|
|
1997
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
1998
|
+
if (/\bdangerouslySetInnerHTML\b/.test(cleanLines[i])) {
|
|
1999
|
+
findings.push({
|
|
2000
|
+
checkType: 'security_control',
|
|
2001
|
+
line: i + 1,
|
|
2002
|
+
evidence: `Line ${i + 1} uses dangerouslySetInnerHTML — bypasses React XSS protection. Ensure input is sanitized`,
|
|
2003
|
+
severity: 'critical',
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
// ── 22. Hardcoded http:// URLs (not localhost) ─────────────
|
|
2009
|
+
if (isJsTs) {
|
|
2010
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2011
|
+
const orig = lines[i];
|
|
2012
|
+
if (/['"`]http:\/\/(?!localhost|127\.0\.0\.1|0\.0\.0\.0)/.test(orig)) {
|
|
2013
|
+
findings.push({
|
|
2014
|
+
checkType: 'security_control',
|
|
2015
|
+
line: i + 1,
|
|
2016
|
+
evidence: `Line ${i + 1} has hardcoded http:// URL — use https:// to prevent mixed content and MITM attacks`,
|
|
2017
|
+
severity: 'medium',
|
|
2018
|
+
});
|
|
2019
|
+
break; // One per file
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
// ── 23. document.write() ───────────────────────────────────
|
|
2024
|
+
if (isJsTs) {
|
|
2025
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
2026
|
+
if (/\bdocument\.write\s*\(/.test(cleanLines[i])) {
|
|
2027
|
+
findings.push({
|
|
2028
|
+
checkType: 'security_control',
|
|
2029
|
+
line: i + 1,
|
|
2030
|
+
evidence: `Line ${i + 1} uses document.write() — overwrites page content if called after load. Consider DOM manipulation or iframe-based printing instead`,
|
|
2031
|
+
severity: 'medium',
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
// ── 24. setTimeout/setInterval with string argument ────────
|
|
2037
|
+
if (isJsTs) {
|
|
2038
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
2039
|
+
const cl = cleanLines[i];
|
|
2040
|
+
// After preprocessCode, string literals become "__STR__"
|
|
2041
|
+
if (/\b(?:setTimeout|setInterval)\s*\(\s*"__STR__"/.test(cl)) {
|
|
2042
|
+
findings.push({
|
|
2043
|
+
checkType: 'security_control',
|
|
2044
|
+
line: i + 1,
|
|
2045
|
+
evidence: `Line ${i + 1} passes a string to setTimeout/setInterval — acts as eval(). Use a function instead`,
|
|
2046
|
+
severity: 'critical',
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
// ── 25. Missing await on fetch() ───────────────────────────
|
|
2052
|
+
if (isJsTs) {
|
|
2053
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
2054
|
+
const cl = cleanLines[i];
|
|
2055
|
+
if (/(?:const|let|var)\s+\w+\s*=\s*fetch\s*\(/.test(cl) && !/\bawait\b/.test(cl)) {
|
|
2056
|
+
findings.push({
|
|
2057
|
+
checkType: 'error_handling',
|
|
2058
|
+
line: i + 1,
|
|
2059
|
+
evidence: `Line ${i + 1} assigns fetch() without await — variable holds a Promise, not the Response`,
|
|
2060
|
+
severity: 'high',
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
// ── 26. Throw string instead of Error ──────────────────────
|
|
2066
|
+
if (isJsTs) {
|
|
2067
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2068
|
+
if (/\bthrow\s+["'`]/.test(lines[i])) {
|
|
2069
|
+
findings.push({
|
|
2070
|
+
checkType: 'error_handling',
|
|
2071
|
+
line: i + 1,
|
|
2072
|
+
evidence: `Line ${i + 1} throws a string literal — stack trace will be lost. Use throw new Error(...) instead`,
|
|
2073
|
+
severity: 'medium',
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
// ── 27. Array.sort without comparator ──────────────────────
|
|
2079
|
+
if (isJsTs) {
|
|
2080
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
2081
|
+
if (/\.sort\s*\(\s*\)/.test(cleanLines[i])) {
|
|
2082
|
+
findings.push({
|
|
2083
|
+
checkType: 'api_misuse',
|
|
2084
|
+
line: i + 1,
|
|
2085
|
+
evidence: `Line ${i + 1} calls .sort() without comparator — converts elements to strings. [10,2,1].sort() gives [1,10,2]`,
|
|
2086
|
+
severity: 'medium',
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
return findings;
|
|
2092
|
+
}
|
|
935
2093
|
//# sourceMappingURL=formal-verifier.js.map
|