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