vibecheck-ai 5.0.1 → 5.1.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.
@@ -23,14 +23,15 @@ __export(scanner_exports, {
23
23
  ALL_ENGINES: () => ALL_ENGINES,
24
24
  RULE_CATALOG: () => RULE_CATALOG,
25
25
  applyFixes: () => applyFixes,
26
+ checkISLCompliance: () => checkISLCompliance,
26
27
  classifyPath: () => classifyPath,
27
28
  fix: () => fix,
28
29
  getRuleOrDefault: () => getRuleOrDefault,
29
30
  scan: () => scan
30
31
  });
31
32
  module.exports = __toCommonJS(scanner_exports);
32
- var import_fs3 = require("fs");
33
- var import_path4 = require("path");
33
+ var import_fs4 = require("fs");
34
+ var import_path5 = require("path");
34
35
  var import_crypto = require("crypto");
35
36
 
36
37
  // src/scanner/classifiers/path-classifier.ts
@@ -865,6 +866,138 @@ var RULE_CATALOG = {
865
866
  fix: "Use a mutex/lock, or restructure to avoid shared mutable state.",
866
867
  tags: ["async", "race-condition", "concurrency"],
867
868
  autoFixable: false
869
+ },
870
+ // ═══════════════════════════════════════════════════════════════════════════
871
+ // FLOW TRACE (FT)
872
+ // ═══════════════════════════════════════════════════════════════════════════
873
+ FT001: {
874
+ ruleId: "FT001",
875
+ name: "SQL Injection via Tainted Input",
876
+ category: "security",
877
+ severity: "critical",
878
+ description: "User-controlled input flows to a SQL query without parameterization",
879
+ why: "Attackers can inject arbitrary SQL through user input, reading, modifying, or deleting your entire database. This is the #1 web vulnerability (OWASP A03).",
880
+ fix: 'Use parameterized queries (prepared statements). Never concatenate user input into SQL strings. Example: db.query("SELECT * FROM users WHERE id = $1", [userId])',
881
+ tags: ["injection", "sql", "taint", "flow-trace"],
882
+ autoFixable: false,
883
+ cwe: "CWE-89"
884
+ },
885
+ FT002: {
886
+ ruleId: "FT002",
887
+ name: "Command Injection via Tainted Input",
888
+ category: "security",
889
+ severity: "critical",
890
+ description: "User-controlled input flows to a shell command without sanitization",
891
+ why: "Attackers can execute arbitrary system commands on your server. A single unsanitized input in exec() can give full server access.",
892
+ fix: "Avoid exec/execSync with user input. Use execFile() or spawn() with argument arrays instead of string concatenation. Validate input against an allowlist.",
893
+ tags: ["injection", "command", "taint", "flow-trace"],
894
+ autoFixable: false,
895
+ cwe: "CWE-78"
896
+ },
897
+ FT003: {
898
+ ruleId: "FT003",
899
+ name: "XSS via Tainted Input",
900
+ category: "security",
901
+ severity: "high",
902
+ description: "User-controlled input flows to HTML output without escaping",
903
+ why: "Attackers can inject scripts that steal cookies, session tokens, or redirect users to malicious sites. innerHTML and dangerouslySetInnerHTML bypass React's XSS protection.",
904
+ fix: "Use textContent instead of innerHTML. In React, avoid dangerouslySetInnerHTML. If HTML rendering is required, use DOMPurify.sanitize() or a similar library.",
905
+ tags: ["xss", "html", "taint", "flow-trace"],
906
+ autoFixable: false,
907
+ cwe: "CWE-79"
908
+ },
909
+ FT004: {
910
+ ruleId: "FT004",
911
+ name: "Path Traversal via Tainted Input",
912
+ category: "security",
913
+ severity: "high",
914
+ description: "User-controlled input flows to filesystem operations without path validation",
915
+ why: 'Attackers can use "../" sequences to read or write files outside the intended directory, accessing /etc/passwd, .env files, or overwriting critical system files.',
916
+ fix: 'Validate that the resolved path starts with the expected base directory. Use path.resolve() and check with startsWith(). Reject paths containing "..".',
917
+ tags: ["path-traversal", "filesystem", "taint", "flow-trace"],
918
+ autoFixable: false,
919
+ cwe: "CWE-22"
920
+ },
921
+ FT005: {
922
+ ruleId: "FT005",
923
+ name: "Open Redirect via Tainted Input",
924
+ category: "security",
925
+ severity: "high",
926
+ description: "User-controlled input flows to a redirect without URL validation",
927
+ why: "Attackers can craft URLs that redirect users to phishing sites while appearing to come from your domain. This is commonly used in credential theft attacks.",
928
+ fix: 'Validate redirect URLs against an allowlist of trusted domains. Only allow relative paths (starting with "/") or check against a set of approved hosts.',
929
+ tags: ["redirect", "phishing", "taint", "flow-trace"],
930
+ autoFixable: false,
931
+ cwe: "CWE-601"
932
+ },
933
+ // ═══════════════════════════════════════════════════════════════════════════
934
+ // ISL COMPLIANCE (ISL)
935
+ // ═══════════════════════════════════════════════════════════════════════════
936
+ ISL001: {
937
+ ruleId: "ISL001",
938
+ name: "Unimplemented Behavior",
939
+ category: "drift",
940
+ severity: "high",
941
+ description: "Behavior declared in spec.isl has no matching route or handler in the codebase",
942
+ why: "Your ISL spec declares this behavior as part of your system contract. If no code implements it, the spec is lying \u2014 the feature doesn't exist. This is spec drift, the equivalent of a ghost feature in your architecture.",
943
+ fix: "Either implement the route/handler for this behavior, or remove it from spec.isl if it's no longer planned.",
944
+ tags: ["isl", "spec-drift", "behavior", "unimplemented"],
945
+ autoFixable: false
946
+ },
947
+ ISL002: {
948
+ ruleId: "ISL002",
949
+ name: "Unimplemented Entity",
950
+ category: "drift",
951
+ severity: "medium",
952
+ description: "Entity declared in spec.isl has no matching type, interface, or model in the codebase",
953
+ why: "Your ISL spec declares this entity as a core data structure. If no type/model matches it in code, your implementation has diverged from your specification. Data model drift causes bugs when teams assume the spec is accurate.",
954
+ fix: "Create a TypeScript interface, database model, or schema that matches this entity definition. Example: interface User { id: string; email: string; ... }",
955
+ tags: ["isl", "spec-drift", "entity", "schema"],
956
+ autoFixable: false
957
+ },
958
+ ISL003: {
959
+ ruleId: "ISL003",
960
+ name: "Unenforced Security Policy",
961
+ category: "security",
962
+ severity: "critical",
963
+ description: "Security policy declared in spec.isl has no matching enforcement in the codebase",
964
+ why: "Your ISL spec declares a security requirement that cannot be found in the code. This means you've documented a security guarantee that isn't actually enforced \u2014 users and auditors may assume this protection exists when it doesn't.",
965
+ fix: "Implement the security enforcement described in the policy. For password hashing: use bcrypt/argon2. For authentication: add auth middleware to protected routes.",
966
+ tags: ["isl", "spec-drift", "security", "unenforced-policy"],
967
+ autoFixable: false
968
+ },
969
+ ISL004: {
970
+ ruleId: "ISL004",
971
+ name: "Missing Rate Limiting",
972
+ category: "security",
973
+ severity: "high",
974
+ description: "Rate limiting policy declared in spec.isl but no rate limiter found in the codebase",
975
+ why: "Your ISL spec declares rate limiting as a requirement, but no rate-limiting middleware or library was detected. Without rate limiting, your endpoints are vulnerable to brute-force attacks and abuse.",
976
+ fix: "Add rate limiting middleware. For Express: use express-rate-limit. For Fastify: use @fastify/rate-limit. Apply to the endpoints specified in the policy.",
977
+ tags: ["isl", "spec-drift", "rate-limit", "security"],
978
+ autoFixable: false
979
+ },
980
+ ISL005: {
981
+ ruleId: "ISL005",
982
+ name: "ISL Spec Parse Error",
983
+ category: "drift",
984
+ severity: "high",
985
+ description: "spec.isl could not be parsed \u2014 the specification file has syntax errors",
986
+ why: "A malformed spec.isl means your system contract is unreadable. No compliance checks can run, and the spec cannot serve as documentation or verification input.",
987
+ fix: 'Fix the syntax errors in spec.isl. Ensure it starts with "domain Name {" and uses valid ISL syntax for entities, behaviors, and policies.',
988
+ tags: ["isl", "spec", "parse-error"],
989
+ autoFixable: false
990
+ },
991
+ QLT006: {
992
+ ruleId: "QLT006",
993
+ name: "Unreachable Code",
994
+ category: "code-quality",
995
+ severity: "medium",
996
+ description: "Code after return/throw statement is unreachable and will never execute",
997
+ why: "Unreachable code is a sign of logic errors or incomplete refactoring. It confuses maintainers and may indicate that important logic was accidentally placed after an early return.",
998
+ fix: "Remove the unreachable code, or restructure the function so the code executes before the return/throw.",
999
+ tags: ["dead-code", "unreachable", "ast-verified"],
1000
+ autoFixable: false
868
1001
  }
869
1002
  };
870
1003
  function getRuleOrDefault(ruleId) {
@@ -3106,6 +3239,992 @@ ${additions}`, "utf-8");
3106
3239
  };
3107
3240
  }
3108
3241
 
3242
+ // src/scanner/utils/ast-helpers.ts
3243
+ var import_parser = require("@babel/parser");
3244
+ var astCache = /* @__PURE__ */ new Map();
3245
+ function parseToAST(code, filePath) {
3246
+ const cacheKey = filePath;
3247
+ if (astCache.has(cacheKey)) return astCache.get(cacheKey);
3248
+ const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
3249
+ const plugins = ["decorators-legacy", "classProperties", "dynamicImport", "optionalChaining", "nullishCoalescingOperator"];
3250
+ if (["ts", "tsx"].includes(ext)) {
3251
+ plugins.push("typescript");
3252
+ }
3253
+ if (["tsx", "jsx"].includes(ext)) {
3254
+ plugins.push("jsx");
3255
+ }
3256
+ try {
3257
+ const ast = (0, import_parser.parse)(code, {
3258
+ sourceType: "module",
3259
+ plugins,
3260
+ errorRecovery: true,
3261
+ allowImportExportEverywhere: true,
3262
+ allowReturnOutsideFunction: true
3263
+ });
3264
+ astCache.set(cacheKey, ast);
3265
+ return ast;
3266
+ } catch {
3267
+ astCache.set(cacheKey, null);
3268
+ return null;
3269
+ }
3270
+ }
3271
+ function clearASTCache() {
3272
+ astCache.clear();
3273
+ }
3274
+ function walkAST(node, visitor, parent = null) {
3275
+ if (!node || typeof node !== "object") return;
3276
+ visitor(node, parent);
3277
+ const record = node;
3278
+ for (const key of Object.keys(record)) {
3279
+ if (key === "loc" || key === "start" || key === "end" || key === "type" || key.startsWith("_")) continue;
3280
+ const child = record[key];
3281
+ if (Array.isArray(child)) {
3282
+ for (const item of child) {
3283
+ if (item && typeof item === "object" && "type" in item) {
3284
+ walkAST(item, visitor, node);
3285
+ }
3286
+ }
3287
+ } else if (child && typeof child === "object" && "type" in child) {
3288
+ walkAST(child, visitor, node);
3289
+ }
3290
+ }
3291
+ }
3292
+ function getLine(node) {
3293
+ return node.loc?.start?.line ?? 0;
3294
+ }
3295
+ function getCodeSnippet(node, lines) {
3296
+ const line = getLine(node);
3297
+ if (line > 0 && line <= lines.length) {
3298
+ return lines[line - 1].trim();
3299
+ }
3300
+ return "";
3301
+ }
3302
+
3303
+ // src/scanner/engines/ast-analysis.ts
3304
+ function makeFinding2(ruleId, file, node, message, severity, confidence) {
3305
+ const rule = getRuleOrDefault(ruleId);
3306
+ const line = getLine(node);
3307
+ const code = getCodeSnippet(node, file.lines);
3308
+ const finalSeverity = escalateSeverity(severity, file.classification.isCriticalPath);
3309
+ return {
3310
+ id: `${ruleId}-ast-${file.path}-${line}`,
3311
+ ruleId,
3312
+ engine: "ast-analysis",
3313
+ category: rule.category,
3314
+ severity: finalSeverity,
3315
+ confidence: confidence >= 85 ? "certain" : confidence >= 65 ? "likely" : "possible",
3316
+ confidenceScore: confidence,
3317
+ file: file.path,
3318
+ line,
3319
+ code,
3320
+ message,
3321
+ why: rule.why,
3322
+ fix: rule.fix,
3323
+ autoFixable: false,
3324
+ tags: [...rule.tags, "ast-verified"],
3325
+ verified: false,
3326
+ _dedup: `${ruleId}:ast:${file.path}:${line}`
3327
+ };
3328
+ }
3329
+ function isFunctionNode(node) {
3330
+ return node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression" || node.type === "ObjectMethod" || node.type === "ClassMethod";
3331
+ }
3332
+ function getFunctionName(node, parent) {
3333
+ if (node.type === "FunctionDeclaration" && node.id) {
3334
+ return node.id.name;
3335
+ }
3336
+ if (parent?.type === "VariableDeclarator" && parent.id) {
3337
+ return parent.id.name ?? "<anonymous>";
3338
+ }
3339
+ if (node.type === "ClassMethod" || node.type === "ObjectMethod") {
3340
+ const key = node.key;
3341
+ return key?.name ?? "<anonymous>";
3342
+ }
3343
+ return "<anonymous>";
3344
+ }
3345
+ function isAsyncFunction(node) {
3346
+ return !!node.async;
3347
+ }
3348
+ function getFunctionBody(node) {
3349
+ const body = node.body;
3350
+ return body ?? null;
3351
+ }
3352
+ function detectEmptyFunctions(ast, file) {
3353
+ if (!ast) return [];
3354
+ const findings = [];
3355
+ walkAST(ast, (node, parent) => {
3356
+ if (!isFunctionNode(node)) return;
3357
+ const body = getFunctionBody(node);
3358
+ if (!body) return;
3359
+ if (body.type !== "BlockStatement") return;
3360
+ const block = body;
3361
+ if (block.body.length === 0) {
3362
+ const name = getFunctionName(node, parent);
3363
+ if (/noop|passthrough|placeholder|abstract|override/i.test(name)) return;
3364
+ if (file.classification.category === "test") return;
3365
+ findings.push(makeFinding2(
3366
+ "FAKE001",
3367
+ file,
3368
+ node,
3369
+ `Empty function body: '${name}()' has no implementation`,
3370
+ "high",
3371
+ 92
3372
+ ));
3373
+ }
3374
+ });
3375
+ return findings;
3376
+ }
3377
+ function detectAsyncWithoutAwait(ast, file) {
3378
+ if (!ast) return [];
3379
+ const findings = [];
3380
+ walkAST(ast, (node, parent) => {
3381
+ if (!isFunctionNode(node)) return;
3382
+ if (!isAsyncFunction(node)) return;
3383
+ const body = getFunctionBody(node);
3384
+ if (!body || body.type !== "BlockStatement") return;
3385
+ const block = body;
3386
+ if (block.body.length <= 1) return;
3387
+ let hasAwait = false;
3388
+ walkAST(body, (inner) => {
3389
+ if (inner !== body && isFunctionNode(inner)) return;
3390
+ if (inner.type === "AwaitExpression") {
3391
+ hasAwait = true;
3392
+ }
3393
+ });
3394
+ if (!hasAwait) {
3395
+ const name = getFunctionName(node, parent);
3396
+ findings.push(makeFinding2(
3397
+ "RV002",
3398
+ file,
3399
+ node,
3400
+ `Async function '${name}' never uses await (AST-verified)`,
3401
+ "medium",
3402
+ 88
3403
+ ));
3404
+ }
3405
+ });
3406
+ return findings;
3407
+ }
3408
+ function detectSilentCatch(ast, file) {
3409
+ if (!ast) return [];
3410
+ const findings = [];
3411
+ walkAST(ast, (node) => {
3412
+ if (node.type !== "CatchClause") return;
3413
+ if (file.classification.category === "test") return;
3414
+ const catchNode = node;
3415
+ const body = catchNode.body.body;
3416
+ if (body.length === 0) {
3417
+ findings.push(makeFinding2(
3418
+ "FAKE003",
3419
+ file,
3420
+ node,
3421
+ "Empty catch block \u2014 errors silently swallowed (AST-verified)",
3422
+ "high",
3423
+ 95
3424
+ ));
3425
+ return;
3426
+ }
3427
+ if (body.length === 1 && body[0].type === "ReturnStatement") {
3428
+ const ret = body[0];
3429
+ if (ret.argument) {
3430
+ if (ret.argument.type === "BooleanLiteral" && ret.argument.value === true) {
3431
+ findings.push(makeFinding2(
3432
+ "FAKE002",
3433
+ file,
3434
+ node,
3435
+ "Catch block returns true \u2014 error swallowed with fake success (AST-verified)",
3436
+ "critical",
3437
+ 95
3438
+ ));
3439
+ }
3440
+ if (ret.argument.type === "ObjectExpression") {
3441
+ const props = ret.argument.properties;
3442
+ for (const prop of props) {
3443
+ if (prop.type === "ObjectProperty") {
3444
+ const key = prop.key;
3445
+ const value = prop.value;
3446
+ if (key?.name === "success" && value?.type === "BooleanLiteral" && value.value === true) {
3447
+ findings.push(makeFinding2(
3448
+ "FAKE002",
3449
+ file,
3450
+ node,
3451
+ "Catch block returns { success: true } \u2014 error swallowed with fake success (AST-verified)",
3452
+ "critical",
3453
+ 97
3454
+ ));
3455
+ }
3456
+ }
3457
+ }
3458
+ }
3459
+ }
3460
+ }
3461
+ });
3462
+ return findings;
3463
+ }
3464
+ function detectStubReturns(ast, file) {
3465
+ if (!ast) return [];
3466
+ const findings = [];
3467
+ walkAST(ast, (node, parent) => {
3468
+ if (!isFunctionNode(node)) return;
3469
+ if (file.classification.category === "test") return;
3470
+ const body = getFunctionBody(node);
3471
+ if (!body || body.type !== "BlockStatement") return;
3472
+ const block = body;
3473
+ if (block.body.length === 0 || block.body.length > 5) return;
3474
+ const returns = [];
3475
+ let hasNonReturn = false;
3476
+ for (const stmt of block.body) {
3477
+ if (stmt.type === "ReturnStatement") {
3478
+ returns.push(stmt);
3479
+ } else {
3480
+ hasNonReturn = true;
3481
+ }
3482
+ }
3483
+ if (returns.length === 1 && !hasNonReturn) {
3484
+ const ret = returns[0];
3485
+ if (ret.argument) {
3486
+ const arg = ret.argument;
3487
+ const isLiteral = arg.type === "StringLiteral" || arg.type === "NumericLiteral" || arg.type === "BooleanLiteral" || arg.type === "NullLiteral" || arg.type === "ObjectExpression" || arg.type === "ArrayExpression";
3488
+ if (isLiteral) {
3489
+ const name = getFunctionName(node, parent);
3490
+ if (/^(get|default|config|initial|placeholder)/i.test(name)) return;
3491
+ findings.push(makeFinding2(
3492
+ "FAKE002",
3493
+ file,
3494
+ node,
3495
+ `Function '${name}' only returns a hardcoded literal \u2014 likely a stub (AST-verified)`,
3496
+ "medium",
3497
+ 75
3498
+ ));
3499
+ }
3500
+ }
3501
+ }
3502
+ });
3503
+ return findings;
3504
+ }
3505
+ function detectUnreachableCode(ast, file) {
3506
+ if (!ast) return [];
3507
+ const findings = [];
3508
+ walkAST(ast, (node) => {
3509
+ if (node.type !== "BlockStatement") return;
3510
+ const block = node;
3511
+ for (let i = 0; i < block.body.length - 1; i++) {
3512
+ const stmt = block.body[i];
3513
+ if (stmt.type === "ReturnStatement" || stmt.type === "ThrowStatement") {
3514
+ const next = block.body[i + 1];
3515
+ if (next.type === "FunctionDeclaration" || next.type === "VariableDeclaration") continue;
3516
+ findings.push(makeFinding2(
3517
+ "QLT006",
3518
+ file,
3519
+ next,
3520
+ "Unreachable code after return/throw statement (AST-verified)",
3521
+ "medium",
3522
+ 92
3523
+ ));
3524
+ break;
3525
+ }
3526
+ }
3527
+ });
3528
+ return findings;
3529
+ }
3530
+ var astAnalysisEngine = {
3531
+ name: "ast-analysis",
3532
+ description: "Babel-powered AST analysis for scope-aware detection of empty functions, async bugs, and stub code",
3533
+ async scan(files, _options) {
3534
+ const findings = [];
3535
+ clearASTCache();
3536
+ for (const [, file] of files) {
3537
+ if (![".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(file.ext)) continue;
3538
+ if (file.classification.excludeByDefault) continue;
3539
+ const ast = parseToAST(file.content, file.path);
3540
+ if (!ast) continue;
3541
+ findings.push(...detectEmptyFunctions(ast, file));
3542
+ findings.push(...detectAsyncWithoutAwait(ast, file));
3543
+ findings.push(...detectSilentCatch(ast, file));
3544
+ findings.push(...detectStubReturns(ast, file));
3545
+ findings.push(...detectUnreachableCode(ast, file));
3546
+ }
3547
+ return findings;
3548
+ }
3549
+ };
3550
+
3551
+ // src/scanner/engines/flow-trace.ts
3552
+ var TAINT_SOURCES = [
3553
+ // Express / Fastify request objects
3554
+ { pattern: /req\.(?:body|params|query|headers)\b(?:\.\w+|\[['"`]\w+['"`]\])?/, label: "HTTP request input" },
3555
+ { pattern: /request\.(?:body|params|query|headers)\b/, label: "HTTP request input" },
3556
+ { pattern: /ctx\.(?:request|params|query)\b/, label: "Koa context input" },
3557
+ // Next.js
3558
+ { pattern: /searchParams\b(?:\.\w+|\[['"`]\w+['"`]\])?/, label: "URL search params" },
3559
+ { pattern: /useSearchParams\(\)/, label: "URL search params" },
3560
+ // General user input
3561
+ { pattern: /document\.getElementById\(.+\)\.value/, label: "DOM input value" },
3562
+ { pattern: /formData\.get\(.+\)/, label: "Form data input" },
3563
+ { pattern: /event\.target\.value/, label: "Event target value" },
3564
+ // Environment-specific
3565
+ { pattern: /process\.argv\b/, label: "CLI argument" },
3566
+ { pattern: /Deno\.args\b/, label: "CLI argument" }
3567
+ ];
3568
+ var TAINT_SINKS = [
3569
+ // FT001: SQL Injection
3570
+ { pattern: /\.query\s*\(\s*[`'"]\s*(?:SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER)\b/i, ruleId: "FT001", label: "SQL query (string interpolation)", severity: "critical", confidence: 92 },
3571
+ { pattern: /\.query\s*\(\s*`[^`]*\$\{/, ruleId: "FT001", label: "SQL query (template literal)", severity: "critical", confidence: 90 },
3572
+ { pattern: /\.raw\s*\(\s*[`'"]/, ruleId: "FT001", label: "Raw SQL query", severity: "critical", confidence: 88 },
3573
+ { pattern: /db\.exec(?:ute)?\s*\(\s*[`'"]/, ruleId: "FT001", label: "Database execute", severity: "critical", confidence: 88 },
3574
+ // FT002: Command Injection
3575
+ { pattern: /(?:child_process\.)?exec\s*\(/, ruleId: "FT002", label: "Shell exec", severity: "critical", confidence: 90 },
3576
+ { pattern: /(?:child_process\.)?execSync\s*\(/, ruleId: "FT002", label: "Shell execSync", severity: "critical", confidence: 90 },
3577
+ { pattern: /(?:child_process\.)?spawn\s*\(/, ruleId: "FT002", label: "Process spawn", severity: "high", confidence: 75 },
3578
+ { pattern: /Deno\.run\s*\(/, ruleId: "FT002", label: "Deno process run", severity: "critical", confidence: 88 },
3579
+ // FT003: XSS
3580
+ { pattern: /\.innerHTML\s*=/, ruleId: "FT003", label: "innerHTML assignment", severity: "high", confidence: 85 },
3581
+ { pattern: /document\.write\s*\(/, ruleId: "FT003", label: "document.write", severity: "high", confidence: 88 },
3582
+ { pattern: /dangerouslySetInnerHTML/, ruleId: "FT003", label: "dangerouslySetInnerHTML", severity: "high", confidence: 82 },
3583
+ { pattern: /\$\(\s*['"`].*['"`]\s*\)\.html\s*\(/, ruleId: "FT003", label: "jQuery .html()", severity: "high", confidence: 80 },
3584
+ // FT004: Path Traversal
3585
+ { pattern: /(?:readFileSync|readFile|createReadStream|writeFileSync|writeFile|unlink)\s*\(/, ruleId: "FT004", label: "File system operation", severity: "high", confidence: 78 },
3586
+ { pattern: /path\.join\s*\(.*,/, ruleId: "FT004", label: "Path join (check base dir)", severity: "medium", confidence: 65 },
3587
+ // FT005: Unvalidated Redirect
3588
+ { pattern: /res\.redirect\s*\(/, ruleId: "FT005", label: "HTTP redirect", severity: "high", confidence: 80 },
3589
+ { pattern: /window\.location\s*=/, ruleId: "FT005", label: "Window location assignment", severity: "high", confidence: 78 },
3590
+ { pattern: /window\.location\.href\s*=/, ruleId: "FT005", label: "Window location.href assignment", severity: "high", confidence: 78 },
3591
+ { pattern: /router\.push\s*\(/, ruleId: "FT005", label: "Router push", severity: "medium", confidence: 65 }
3592
+ ];
3593
+ var SANITIZERS = [
3594
+ // SQL sanitizers
3595
+ { pattern: /(?:escape|sanitize|parameterize|prepare|placeholder)\s*\(/i, sanitizes: ["FT001"] },
3596
+ { pattern: /\.prepare\s*\(/, sanitizes: ["FT001"] },
3597
+ // Command sanitizers
3598
+ { pattern: /(?:shellEscape|escapeShell|quote)\s*\(/i, sanitizes: ["FT002"] },
3599
+ // XSS sanitizers
3600
+ { pattern: /(?:escape|encode|sanitize|DOMPurify|xss)\s*\(/i, sanitizes: ["FT003"] },
3601
+ { pattern: /encodeURIComponent\s*\(/, sanitizes: ["FT003", "FT005"] },
3602
+ // Path sanitizers
3603
+ { pattern: /path\.(?:normalize|resolve)\s*\(/, sanitizes: ["FT004"] },
3604
+ { pattern: /\.startsWith\s*\(/, sanitizes: ["FT004"] },
3605
+ { pattern: /\.includes\s*\(\s*['"`]\.\.\//i, sanitizes: ["FT004"] },
3606
+ // Redirect sanitizers
3607
+ { pattern: /(?:allowlist|whitelist|allowedUrls|safeUrls)\b/i, sanitizes: ["FT005"] },
3608
+ { pattern: /\.startsWith\s*\(\s*['"`]\//, sanitizes: ["FT005"] },
3609
+ // Generic validators
3610
+ { pattern: /(?:parseInt|parseFloat|Number|Boolean)\s*\(/, sanitizes: ["FT001", "FT002", "FT003", "FT004", "FT005"] },
3611
+ { pattern: /\.(?:match|test)\s*\(\s*\//, sanitizes: ["FT001", "FT002", "FT003", "FT004", "FT005"] },
3612
+ { pattern: /(?:zod|yup|joi|ajv|superstruct).*\.parse\s*\(/i, sanitizes: ["FT001", "FT002", "FT003", "FT004", "FT005"] }
3613
+ ];
3614
+ function traceFlows(file) {
3615
+ const findings = [];
3616
+ const lines = file.lines;
3617
+ const tainted = /* @__PURE__ */ new Map();
3618
+ for (let i = 0; i < lines.length; i++) {
3619
+ const line = lines[i];
3620
+ const trimmed = line.trim();
3621
+ if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) continue;
3622
+ for (const source of TAINT_SOURCES) {
3623
+ if (!source.pattern.test(line)) continue;
3624
+ const assignMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=.*$/);
3625
+ if (assignMatch) {
3626
+ tainted.set(assignMatch[1], {
3627
+ name: assignMatch[1],
3628
+ sourceLine: i + 1,
3629
+ sourceLabel: source.label,
3630
+ sanitizedFor: /* @__PURE__ */ new Set()
3631
+ });
3632
+ continue;
3633
+ }
3634
+ const destructMatch = line.match(/(?:const|let|var)\s+\{\s*([^}]+)\}\s*=/);
3635
+ if (destructMatch) {
3636
+ const vars = destructMatch[1].split(",").map((v) => v.trim().split(":")[0].trim().split("=")[0].trim());
3637
+ for (const v of vars) {
3638
+ if (v && /^\w+$/.test(v)) {
3639
+ tainted.set(v, {
3640
+ name: v,
3641
+ sourceLine: i + 1,
3642
+ sourceLabel: source.label,
3643
+ sanitizedFor: /* @__PURE__ */ new Set()
3644
+ });
3645
+ }
3646
+ }
3647
+ }
3648
+ }
3649
+ for (const [varName, taintInfo] of tainted) {
3650
+ if (!line.includes(varName)) continue;
3651
+ for (const sanitizer of SANITIZERS) {
3652
+ if (sanitizer.pattern.test(line)) {
3653
+ for (const ruleId of sanitizer.sanitizes) {
3654
+ taintInfo.sanitizedFor.add(ruleId);
3655
+ }
3656
+ }
3657
+ }
3658
+ }
3659
+ for (const [varName] of tainted) {
3660
+ const propMatch = line.match(new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=.*\\b${varName}\\b`));
3661
+ if (propMatch && propMatch[1] !== varName) {
3662
+ const existing = tainted.get(varName);
3663
+ tainted.set(propMatch[1], {
3664
+ name: propMatch[1],
3665
+ sourceLine: existing.sourceLine,
3666
+ sourceLabel: existing.sourceLabel,
3667
+ sanitizedFor: new Set(existing.sanitizedFor)
3668
+ });
3669
+ }
3670
+ }
3671
+ for (const sink of TAINT_SINKS) {
3672
+ if (!sink.pattern.test(line)) continue;
3673
+ for (const [varName, taintInfo] of tainted) {
3674
+ if (!line.includes(varName)) continue;
3675
+ if (taintInfo.sanitizedFor.has(sink.ruleId)) continue;
3676
+ const rule = getRuleOrDefault(sink.ruleId);
3677
+ const severity = escalateSeverity(sink.severity, file.classification.isCriticalPath);
3678
+ findings.push({
3679
+ id: `${sink.ruleId}-flow-${file.path}-${i + 1}`,
3680
+ ruleId: sink.ruleId,
3681
+ engine: "flow-trace",
3682
+ category: "security",
3683
+ severity,
3684
+ confidence: sink.confidence >= 85 ? "certain" : sink.confidence >= 65 ? "likely" : "possible",
3685
+ confidenceScore: sink.confidence,
3686
+ file: file.path,
3687
+ line: i + 1,
3688
+ code: trimmed,
3689
+ message: `Tainted data flows to ${sink.label} \u2014 '${varName}' originates from ${taintInfo.sourceLabel} (line ${taintInfo.sourceLine}) without sanitization`,
3690
+ why: rule.why || `User-controlled input ('${varName}' from ${taintInfo.sourceLabel}) reaches a dangerous sink (${sink.label}) without sanitization or validation. This can be exploited by attackers to inject malicious payloads.`,
3691
+ fix: rule.fix || `Validate and sanitize '${varName}' before passing it to ${sink.label}. Use parameterized queries for SQL, escape functions for HTML, and allowlists for redirects.`,
3692
+ autoFixable: false,
3693
+ tags: [...rule.tags || [], "flow-trace", "taint-analysis"],
3694
+ cwe: getCWE(sink.ruleId),
3695
+ verified: false,
3696
+ _dedup: `${sink.ruleId}:flow:${file.path}:${i + 1}:${varName}`
3697
+ });
3698
+ }
3699
+ }
3700
+ }
3701
+ return findings;
3702
+ }
3703
+ function getCWE(ruleId) {
3704
+ switch (ruleId) {
3705
+ case "FT001":
3706
+ return "CWE-89";
3707
+ // SQL Injection
3708
+ case "FT002":
3709
+ return "CWE-78";
3710
+ // OS Command Injection
3711
+ case "FT003":
3712
+ return "CWE-79";
3713
+ // XSS
3714
+ case "FT004":
3715
+ return "CWE-22";
3716
+ // Path Traversal
3717
+ case "FT005":
3718
+ return "CWE-601";
3719
+ // Open Redirect
3720
+ default:
3721
+ return void 0;
3722
+ }
3723
+ }
3724
+ var flowTraceEngine = {
3725
+ name: "flow-trace",
3726
+ description: "Intra-file data-flow tracking: traces user input from sources to dangerous sinks",
3727
+ async scan(files, _options) {
3728
+ const findings = [];
3729
+ for (const [, file] of files) {
3730
+ if (![".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py"].includes(file.ext)) continue;
3731
+ if (file.classification.excludeByDefault) continue;
3732
+ if (file.classification.category === "test") continue;
3733
+ if (file.classification.category === "config") continue;
3734
+ findings.push(...traceFlows(file));
3735
+ }
3736
+ return findings;
3737
+ }
3738
+ };
3739
+
3740
+ // src/scanner/isl-compliance.ts
3741
+ var import_fs3 = require("fs");
3742
+ var import_path4 = require("path");
3743
+ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs"]);
3744
+ var EXCLUDE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".nuxt", "coverage", "__pycache__", ".venv"]);
3745
+ function loadCodeFiles(projectRoot) {
3746
+ const results = [];
3747
+ const maxFiles = 500;
3748
+ const maxFileSize = 256 * 1024;
3749
+ function walk(dir) {
3750
+ if (results.length >= maxFiles) return;
3751
+ let entries;
3752
+ try {
3753
+ entries = (0, import_fs3.readdirSync)(dir);
3754
+ } catch {
3755
+ return;
3756
+ }
3757
+ for (const entry of entries) {
3758
+ if (results.length >= maxFiles) return;
3759
+ if (EXCLUDE_DIRS.has(entry) || entry.startsWith(".")) continue;
3760
+ const fullPath = (0, import_path4.join)(dir, entry);
3761
+ let stat;
3762
+ try {
3763
+ stat = (0, import_fs3.statSync)(fullPath);
3764
+ } catch {
3765
+ continue;
3766
+ }
3767
+ if (stat.isDirectory()) {
3768
+ walk(fullPath);
3769
+ continue;
3770
+ }
3771
+ const ext = (0, import_path4.extname)(entry).toLowerCase();
3772
+ if (!CODE_EXTENSIONS.has(ext)) continue;
3773
+ if (stat.size > maxFileSize) continue;
3774
+ try {
3775
+ const content = (0, import_fs3.readFileSync)(fullPath, "utf-8");
3776
+ results.push({ path: (0, import_path4.relative)(projectRoot, fullPath).replace(/\\/g, "/"), content });
3777
+ } catch {
3778
+ }
3779
+ }
3780
+ }
3781
+ walk(projectRoot);
3782
+ return results;
3783
+ }
3784
+ function extractISLSpec(source) {
3785
+ const domainMatch = source.match(/domain\s+(\w+)/);
3786
+ if (!domainMatch) return null;
3787
+ const spec = {
3788
+ domain: domainMatch[1],
3789
+ behaviors: [],
3790
+ entities: [],
3791
+ policies: []
3792
+ };
3793
+ const versionMatch = source.match(/version\s*:\s*["']([^"']+)["']/);
3794
+ if (versionMatch) spec.version = versionMatch[1];
3795
+ const lines = source.split("\n");
3796
+ let currentBlock = "none";
3797
+ let currentName = "";
3798
+ let currentLine = 0;
3799
+ let braceDepth = 0;
3800
+ let blockBraceStart = 0;
3801
+ let fields = [];
3802
+ let preconditions = [];
3803
+ let postconditions = [];
3804
+ let policyRule = "";
3805
+ let policyEnforce = "";
3806
+ for (let i = 0; i < lines.length; i++) {
3807
+ const line = lines[i];
3808
+ const trimmed = line.trim();
3809
+ if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) continue;
3810
+ for (const ch of trimmed) {
3811
+ if (ch === "{") braceDepth++;
3812
+ if (ch === "}") braceDepth--;
3813
+ }
3814
+ const behaviorMatch = trimmed.match(/^behavior\s+(\w+)\s*\{?/);
3815
+ if (behaviorMatch) {
3816
+ currentBlock = "behavior";
3817
+ currentName = behaviorMatch[1];
3818
+ currentLine = i + 1;
3819
+ blockBraceStart = braceDepth;
3820
+ preconditions = [];
3821
+ postconditions = [];
3822
+ continue;
3823
+ }
3824
+ const entityMatch = trimmed.match(/^entity\s+(\w+)\s*\{?/);
3825
+ if (entityMatch) {
3826
+ currentBlock = "entity";
3827
+ currentName = entityMatch[1];
3828
+ currentLine = i + 1;
3829
+ blockBraceStart = braceDepth;
3830
+ fields = [];
3831
+ continue;
3832
+ }
3833
+ const policyMatch = trimmed.match(/^policy\s+(\w+)\s*\{?/);
3834
+ if (policyMatch) {
3835
+ currentBlock = "policy";
3836
+ currentName = policyMatch[1];
3837
+ currentLine = i + 1;
3838
+ blockBraceStart = braceDepth;
3839
+ policyRule = "";
3840
+ policyEnforce = "";
3841
+ continue;
3842
+ }
3843
+ if (currentBlock === "entity") {
3844
+ const fieldMatch = trimmed.match(/^(\w+)\s*:/);
3845
+ if (fieldMatch && !["meta", "version", "description", "ship_score", "security_level"].includes(fieldMatch[1])) {
3846
+ fields.push(fieldMatch[1]);
3847
+ }
3848
+ }
3849
+ if (currentBlock === "behavior") {
3850
+ const preMatch = trimmed.match(/^pre\s*:\s*\[([^\]]+)\]/);
3851
+ if (preMatch) {
3852
+ preconditions = preMatch[1].split(",").map((s) => s.trim());
3853
+ }
3854
+ const postMatch = trimmed.match(/^post\s*:\s*\[([^\]]+)\]/);
3855
+ if (postMatch) {
3856
+ postconditions = postMatch[1].split(",").map((s) => s.trim());
3857
+ }
3858
+ }
3859
+ if (currentBlock === "policy") {
3860
+ const ruleMatch = trimmed.match(/^rule\s*:\s*["']([^"']+)["']/);
3861
+ if (ruleMatch) policyRule = ruleMatch[1];
3862
+ const enforceMatch = trimmed.match(/^enforce\s*:\s*(.+)/);
3863
+ if (enforceMatch) policyEnforce = enforceMatch[1].trim();
3864
+ }
3865
+ if (trimmed.includes("}") && currentBlock !== "none" && braceDepth <= blockBraceStart - 1) {
3866
+ if (currentBlock === "behavior") {
3867
+ spec.behaviors.push({ name: currentName, line: currentLine, preconditions, postconditions });
3868
+ } else if (currentBlock === "entity") {
3869
+ spec.entities.push({ name: currentName, line: currentLine, fields });
3870
+ } else if (currentBlock === "policy") {
3871
+ spec.policies.push({ name: currentName, line: currentLine, rule: policyRule, enforce: policyEnforce });
3872
+ }
3873
+ currentBlock = "none";
3874
+ }
3875
+ }
3876
+ return spec;
3877
+ }
3878
+ function behaviorToRoutePatterns(name) {
3879
+ const lower = name.toLowerCase();
3880
+ const patterns = [
3881
+ new RegExp(`[/'"]\\/?(?:api\\/)?${lower}['"/]`, "i"),
3882
+ new RegExp(`[/'"]\\/?(?:api\\/)?auth\\/${lower}['"/]`, "i"),
3883
+ new RegExp(`[/'"]\\/?(?:api\\/)?v\\d+\\/${lower}['"/]`, "i")
3884
+ ];
3885
+ const specialRoutes = {
3886
+ login: ["/login", "/auth/login", "/api/auth/login", "/api/login", "/signin", "/sign-in"],
3887
+ register: ["/register", "/auth/register", "/api/auth/register", "/signup", "/sign-up", "/api/register"],
3888
+ resetpassword: ["/reset-password", "/forgot-password", "/auth/reset-password", "/api/auth/reset-password"],
3889
+ logout: ["/logout", "/auth/logout", "/api/auth/logout", "/signout"],
3890
+ getuser: ["/user", "/users", "/api/user", "/api/users", "/me", "/api/me"]
3891
+ };
3892
+ const mapped = specialRoutes[lower];
3893
+ if (mapped) {
3894
+ for (const route of mapped) {
3895
+ patterns.push(new RegExp(route.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i"));
3896
+ }
3897
+ }
3898
+ return patterns;
3899
+ }
3900
+ function behaviorToFunctionPatterns(name) {
3901
+ return [
3902
+ new RegExp(`(?:function|const|let|var|async\\s+function)\\s+${name}\\b`, "i"),
3903
+ new RegExp(`(?:export\\s+(?:async\\s+)?function)\\s+${name}\\b`, "i"),
3904
+ new RegExp(`\\.${name.toLowerCase()}\\s*=`, "i"),
3905
+ new RegExp(`['"]${name.toLowerCase()}['"]\\s*:`, "i")
3906
+ ];
3907
+ }
3908
+ function entityToCodePatterns(name) {
3909
+ return [
3910
+ new RegExp(`(?:interface|type|class|model)\\s+${name}\\b`, "i"),
3911
+ new RegExp(`(?:schema|Schema)\\s*\\(\\s*['"\`]${name}['"\`]`, "i"),
3912
+ new RegExp(`createTable\\s*\\(\\s*['"\`]${name.toLowerCase()}s?['"\`]`, "i"),
3913
+ new RegExp(`model\\s+${name}\\s*\\{`, "i"),
3914
+ // Prisma
3915
+ new RegExp(`@Entity\\s*\\(.*['"\`]${name}['"\`]`, "i")
3916
+ // TypeORM
3917
+ ];
3918
+ }
3919
+ function securityEnforcePatterns(enforce, policyName) {
3920
+ const patterns = [];
3921
+ const lower = enforce.toLowerCase();
3922
+ if (lower.includes("bcrypt") || lower.includes("hash")) {
3923
+ patterns.push(/bcrypt/i, /argon2/i, /scrypt/i, /hash\s*\(/i, /hashPassword/i);
3924
+ }
3925
+ if (lower.includes("jwt") || lower.includes("token")) {
3926
+ patterns.push(/jwt\.sign/i, /jsonwebtoken/i, /jose/i);
3927
+ }
3928
+ if (lower.includes("rate_limit") || policyName.toLowerCase().includes("rate")) {
3929
+ patterns.push(/rate[-_]?limit/i, /rateLimit/i, /express-rate-limit/i, /throttle/i, /@fastify\/rate-limit/i);
3930
+ }
3931
+ if (lower.includes("auth") || lower.includes("authenticated")) {
3932
+ patterns.push(/(?:isAuthenticated|requireAuth|auth\s*\(|middleware.*auth|verifyToken|getServerSession)/i);
3933
+ }
3934
+ if (lower.includes("encrypt")) {
3935
+ patterns.push(/encrypt/i, /crypto\.createCipher/i, /AES/i);
3936
+ }
3937
+ return patterns;
3938
+ }
3939
+ function checkISLCompliance(projectRoot, files) {
3940
+ const specPath = (0, import_path4.join)(projectRoot, "spec.isl");
3941
+ const findings = [];
3942
+ if (!(0, import_fs3.existsSync)(specPath)) {
3943
+ return {
3944
+ result: {
3945
+ specFound: false,
3946
+ specPath,
3947
+ domain: "",
3948
+ parseSuccess: false,
3949
+ parseErrors: [],
3950
+ checks: [],
3951
+ score: 100,
3952
+ // No spec = no compliance requirements
3953
+ summary: { totalChecks: 0, passed: 0, failed: 0, warnings: 0 }
3954
+ },
3955
+ findings: []
3956
+ };
3957
+ }
3958
+ let source;
3959
+ try {
3960
+ source = (0, import_fs3.readFileSync)(specPath, "utf-8");
3961
+ } catch {
3962
+ return {
3963
+ result: {
3964
+ specFound: true,
3965
+ specPath,
3966
+ domain: "",
3967
+ parseSuccess: false,
3968
+ parseErrors: ["Failed to read spec.isl"],
3969
+ checks: [],
3970
+ score: 0,
3971
+ summary: { totalChecks: 0, passed: 0, failed: 0, warnings: 0 }
3972
+ },
3973
+ findings: [{
3974
+ id: "ISL005-parse-error",
3975
+ ruleId: "ISL005",
3976
+ engine: "isl-compliance",
3977
+ category: "drift",
3978
+ severity: "high",
3979
+ confidence: "certain",
3980
+ confidenceScore: 100,
3981
+ file: "spec.isl",
3982
+ line: 1,
3983
+ code: "",
3984
+ message: "Failed to read spec.isl",
3985
+ why: getRuleOrDefault("ISL005").why,
3986
+ fix: getRuleOrDefault("ISL005").fix,
3987
+ autoFixable: false,
3988
+ tags: ["isl", "spec", "parse-error"],
3989
+ verified: false
3990
+ }]
3991
+ };
3992
+ }
3993
+ const spec = extractISLSpec(source);
3994
+ if (!spec) {
3995
+ const rule = getRuleOrDefault("ISL005");
3996
+ return {
3997
+ result: {
3998
+ specFound: true,
3999
+ specPath,
4000
+ domain: "",
4001
+ parseSuccess: false,
4002
+ parseErrors: ["Could not parse domain declaration in spec.isl"],
4003
+ checks: [],
4004
+ score: 0,
4005
+ summary: { totalChecks: 0, passed: 0, failed: 0, warnings: 0 }
4006
+ },
4007
+ findings: [{
4008
+ id: "ISL005-no-domain",
4009
+ ruleId: "ISL005",
4010
+ engine: "isl-compliance",
4011
+ category: "drift",
4012
+ severity: "high",
4013
+ confidence: "certain",
4014
+ confidenceScore: 100,
4015
+ file: "spec.isl",
4016
+ line: 1,
4017
+ code: source.split("\n")[0] ?? "",
4018
+ message: "spec.isl has no valid domain declaration",
4019
+ why: rule.why,
4020
+ fix: rule.fix,
4021
+ autoFixable: false,
4022
+ tags: ["isl", "spec", "parse-error"],
4023
+ verified: false
4024
+ }]
4025
+ };
4026
+ }
4027
+ let fileContents;
4028
+ if (files.size > 0) {
4029
+ fileContents = [];
4030
+ for (const [, file] of files) {
4031
+ if (file.classification.excludeByDefault) continue;
4032
+ fileContents.push({ path: file.path, content: file.content });
4033
+ }
4034
+ } else {
4035
+ fileContents = loadCodeFiles(projectRoot);
4036
+ }
4037
+ const checks = [];
4038
+ for (const behavior of spec.behaviors) {
4039
+ const routePatterns = behaviorToRoutePatterns(behavior.name);
4040
+ const funcPatterns = behaviorToFunctionPatterns(behavior.name);
4041
+ const allPatterns = [...routePatterns, ...funcPatterns];
4042
+ const matchedFiles = [];
4043
+ for (const file of fileContents) {
4044
+ for (const pattern of allPatterns) {
4045
+ if (pattern.test(file.content)) {
4046
+ matchedFiles.push(file.path);
4047
+ break;
4048
+ }
4049
+ }
4050
+ }
4051
+ if (matchedFiles.length > 0) {
4052
+ checks.push({
4053
+ type: "behavior",
4054
+ name: behavior.name,
4055
+ status: "pass",
4056
+ message: `Behavior '${behavior.name}' has matching implementation`,
4057
+ specLine: behavior.line,
4058
+ matchedFiles
4059
+ });
4060
+ } else {
4061
+ checks.push({
4062
+ type: "behavior",
4063
+ name: behavior.name,
4064
+ status: "fail",
4065
+ message: `Behavior '${behavior.name}' declared in spec.isl but no matching route or handler found in codebase`,
4066
+ specLine: behavior.line
4067
+ });
4068
+ const rule = getRuleOrDefault("ISL001");
4069
+ findings.push({
4070
+ id: `ISL001-${behavior.name}`,
4071
+ ruleId: "ISL001",
4072
+ engine: "isl-compliance",
4073
+ category: "drift",
4074
+ severity: "high",
4075
+ confidence: "likely",
4076
+ confidenceScore: 78,
4077
+ file: "spec.isl",
4078
+ line: behavior.line,
4079
+ code: `behavior ${behavior.name} { ... }`,
4080
+ message: `Behavior '${behavior.name}' declared in spec but no matching route/handler found`,
4081
+ why: rule.why,
4082
+ fix: rule.fix,
4083
+ autoFixable: false,
4084
+ tags: ["isl", "spec-drift", "unimplemented-behavior"],
4085
+ verified: false,
4086
+ _dedup: `ISL001:${behavior.name}`
4087
+ });
4088
+ }
4089
+ }
4090
+ for (const entity of spec.entities) {
4091
+ const patterns = entityToCodePatterns(entity.name);
4092
+ const matchedFiles = [];
4093
+ for (const file of fileContents) {
4094
+ for (const pattern of patterns) {
4095
+ if (pattern.test(file.content)) {
4096
+ matchedFiles.push(file.path);
4097
+ break;
4098
+ }
4099
+ }
4100
+ }
4101
+ if (matchedFiles.length > 0) {
4102
+ checks.push({
4103
+ type: "entity",
4104
+ name: entity.name,
4105
+ status: "pass",
4106
+ message: `Entity '${entity.name}' has matching type/model definition`,
4107
+ specLine: entity.line,
4108
+ matchedFiles
4109
+ });
4110
+ } else {
4111
+ checks.push({
4112
+ type: "entity",
4113
+ name: entity.name,
4114
+ status: "fail",
4115
+ message: `Entity '${entity.name}' declared in spec.isl but no matching type/model/schema found`,
4116
+ specLine: entity.line
4117
+ });
4118
+ const rule = getRuleOrDefault("ISL002");
4119
+ findings.push({
4120
+ id: `ISL002-${entity.name}`,
4121
+ ruleId: "ISL002",
4122
+ engine: "isl-compliance",
4123
+ category: "drift",
4124
+ severity: "medium",
4125
+ confidence: "likely",
4126
+ confidenceScore: 72,
4127
+ file: "spec.isl",
4128
+ line: entity.line,
4129
+ code: `entity ${entity.name} { ${entity.fields.join(", ")} }`,
4130
+ message: `Entity '${entity.name}' declared in spec but no matching type/model found`,
4131
+ why: rule.why,
4132
+ fix: rule.fix,
4133
+ autoFixable: false,
4134
+ tags: ["isl", "spec-drift", "unimplemented-entity"],
4135
+ verified: false,
4136
+ _dedup: `ISL002:${entity.name}`
4137
+ });
4138
+ }
4139
+ }
4140
+ for (const policy of spec.policies) {
4141
+ const patterns = securityEnforcePatterns(policy.enforce, policy.name);
4142
+ if (patterns.length === 0) {
4143
+ checks.push({
4144
+ type: "policy",
4145
+ name: policy.name,
4146
+ status: "warn",
4147
+ message: `Policy '${policy.name}' could not be automatically verified (unrecognized enforce pattern)`,
4148
+ specLine: policy.line
4149
+ });
4150
+ continue;
4151
+ }
4152
+ const matchedFiles = [];
4153
+ for (const file of fileContents) {
4154
+ for (const pattern of patterns) {
4155
+ if (pattern.test(file.content)) {
4156
+ matchedFiles.push(file.path);
4157
+ break;
4158
+ }
4159
+ }
4160
+ }
4161
+ if (matchedFiles.length > 0) {
4162
+ checks.push({
4163
+ type: "policy",
4164
+ name: policy.name,
4165
+ status: "pass",
4166
+ message: `Policy '${policy.name}' has matching enforcement in codebase`,
4167
+ specLine: policy.line,
4168
+ matchedFiles
4169
+ });
4170
+ } else {
4171
+ const isRateLimit = policy.name.toLowerCase().includes("rate") || policy.enforce.toLowerCase().includes("rate_limit");
4172
+ const ruleId = isRateLimit ? "ISL004" : "ISL003";
4173
+ const rule = getRuleOrDefault(ruleId);
4174
+ const severity = isRateLimit ? "high" : "critical";
4175
+ checks.push({
4176
+ type: "policy",
4177
+ name: policy.name,
4178
+ status: "fail",
4179
+ message: `Policy '${policy.name}' (${policy.rule}) has no matching enforcement found`,
4180
+ specLine: policy.line
4181
+ });
4182
+ findings.push({
4183
+ id: `${ruleId}-${policy.name}`,
4184
+ ruleId,
4185
+ engine: "isl-compliance",
4186
+ category: "security",
4187
+ severity,
4188
+ confidence: "likely",
4189
+ confidenceScore: 75,
4190
+ file: "spec.isl",
4191
+ line: policy.line,
4192
+ code: `policy ${policy.name} { enforce: ${policy.enforce} }`,
4193
+ message: `Policy '${policy.name}' declared in spec but no enforcement found: ${policy.rule}`,
4194
+ why: rule.why,
4195
+ fix: rule.fix,
4196
+ autoFixable: false,
4197
+ tags: ["isl", "spec-drift", "unenforced-policy"],
4198
+ verified: false,
4199
+ _dedup: `${ruleId}:${policy.name}`
4200
+ });
4201
+ }
4202
+ }
4203
+ const passed = checks.filter((c) => c.status === "pass").length;
4204
+ const failed = checks.filter((c) => c.status === "fail").length;
4205
+ const warned = checks.filter((c) => c.status === "warn").length;
4206
+ const total = checks.length;
4207
+ const score = total > 0 ? Math.round(passed / total * 100) : 100;
4208
+ return {
4209
+ result: {
4210
+ specFound: true,
4211
+ specPath,
4212
+ domain: spec.domain,
4213
+ parseSuccess: true,
4214
+ parseErrors: [],
4215
+ checks,
4216
+ score,
4217
+ summary: {
4218
+ totalChecks: total,
4219
+ passed,
4220
+ failed,
4221
+ warnings: warned
4222
+ }
4223
+ },
4224
+ findings
4225
+ };
4226
+ }
4227
+
3109
4228
  // src/scanner/index.ts
3110
4229
  var ALL_ENGINES = [
3111
4230
  credentialsEngine,
@@ -3115,7 +4234,9 @@ var ALL_ENGINES = [
3115
4234
  deadUIEngine,
3116
4235
  codeQualityEngine,
3117
4236
  importGraphEngine,
3118
- runtimeVerifyEngine
4237
+ runtimeVerifyEngine,
4238
+ astAnalysisEngine,
4239
+ flowTraceEngine
3119
4240
  ];
3120
4241
  var DEFAULT_EXCLUDE = [
3121
4242
  "node_modules",
@@ -3137,22 +4258,22 @@ function loadFiles(projectRoot, options) {
3137
4258
  function walk(dir) {
3138
4259
  let entries;
3139
4260
  try {
3140
- entries = (0, import_fs3.readdirSync)(dir);
4261
+ entries = (0, import_fs4.readdirSync)(dir);
3141
4262
  } catch {
3142
4263
  return;
3143
4264
  }
3144
4265
  for (const entry of entries) {
3145
- const fullPath = (0, import_path4.join)(dir, entry);
4266
+ const fullPath = (0, import_path5.join)(dir, entry);
3146
4267
  if (exclude.some((e) => entry === e || entry.startsWith("."))) {
3147
4268
  try {
3148
- if ((0, import_fs3.statSync)(fullPath).isDirectory()) continue;
4269
+ if ((0, import_fs4.statSync)(fullPath).isDirectory()) continue;
3149
4270
  } catch {
3150
4271
  continue;
3151
4272
  }
3152
4273
  }
3153
4274
  let stat;
3154
4275
  try {
3155
- stat = (0, import_fs3.statSync)(fullPath);
4276
+ stat = (0, import_fs4.statSync)(fullPath);
3156
4277
  } catch {
3157
4278
  continue;
3158
4279
  }
@@ -3162,19 +4283,19 @@ function loadFiles(projectRoot, options) {
3162
4283
  }
3163
4284
  if (!isCodeFile(fullPath)) continue;
3164
4285
  if (stat.size > maxFileSize) continue;
3165
- const relativePath = (0, import_path4.relative)(projectRoot, fullPath);
4286
+ const relativePath = (0, import_path5.relative)(projectRoot, fullPath);
3166
4287
  const classification = classifyPath(relativePath);
3167
4288
  if (classification.excludeByDefault && classification.category !== "test") continue;
3168
4289
  if (classification.category === "test" && !options.includeTests) continue;
3169
4290
  try {
3170
- const content = (0, import_fs3.readFileSync)(fullPath, "utf-8");
4291
+ const content = (0, import_fs4.readFileSync)(fullPath, "utf-8");
3171
4292
  const hash = (0, import_crypto.createHash)("md5").update(content).digest("hex");
3172
4293
  files.set(relativePath, {
3173
4294
  path: relativePath,
3174
4295
  absolutePath: fullPath,
3175
4296
  content,
3176
4297
  lines: content.split("\n"),
3177
- ext: (0, import_path4.extname)(fullPath).toLowerCase(),
4298
+ ext: (0, import_path5.extname)(fullPath).toLowerCase(),
3178
4299
  hash,
3179
4300
  classification
3180
4301
  });
@@ -3184,10 +4305,10 @@ function loadFiles(projectRoot, options) {
3184
4305
  }
3185
4306
  if (options.files && options.files.length > 0) {
3186
4307
  for (const filePath of options.files) {
3187
- const fullPath = (0, import_path4.join)(projectRoot, filePath);
3188
- if (!(0, import_fs3.existsSync)(fullPath)) continue;
4308
+ const fullPath = (0, import_path5.join)(projectRoot, filePath);
4309
+ if (!(0, import_fs4.existsSync)(fullPath)) continue;
3189
4310
  try {
3190
- const content = (0, import_fs3.readFileSync)(fullPath, "utf-8");
4311
+ const content = (0, import_fs4.readFileSync)(fullPath, "utf-8");
3191
4312
  const hash = (0, import_crypto.createHash)("md5").update(content).digest("hex");
3192
4313
  const classification = classifyPath(filePath);
3193
4314
  files.set(filePath, {
@@ -3195,7 +4316,7 @@ function loadFiles(projectRoot, options) {
3195
4316
  absolutePath: fullPath,
3196
4317
  content,
3197
4318
  lines: content.split("\n"),
3198
- ext: (0, import_path4.extname)(fullPath).toLowerCase(),
4319
+ ext: (0, import_path5.extname)(fullPath).toLowerCase(),
3199
4320
  hash,
3200
4321
  classification
3201
4322
  });
@@ -3320,6 +4441,35 @@ async function scan(options) {
3320
4441
  elapsedMs: Date.now() - startTime
3321
4442
  });
3322
4443
  const { deduplicated, suppressedCount } = deduplicateFindings(allFindings);
4444
+ let specCompliance;
4445
+ const islStart = Date.now();
4446
+ try {
4447
+ const { result: islResult, findings: islFindings } = checkISLCompliance(options.projectRoot, files);
4448
+ if (islResult.specFound) {
4449
+ specCompliance = {
4450
+ specFound: islResult.specFound,
4451
+ domain: islResult.domain,
4452
+ score: islResult.score,
4453
+ summary: islResult.summary,
4454
+ checks: islResult.checks
4455
+ };
4456
+ deduplicated.push(...islFindings);
4457
+ engineResults.push({
4458
+ engine: "isl-compliance",
4459
+ findings: islFindings.length,
4460
+ durationMs: Date.now() - islStart,
4461
+ success: true
4462
+ });
4463
+ }
4464
+ } catch {
4465
+ engineResults.push({
4466
+ engine: "isl-compliance",
4467
+ findings: 0,
4468
+ durationMs: Date.now() - islStart,
4469
+ success: false,
4470
+ error: "ISL compliance check failed"
4471
+ });
4472
+ }
3323
4473
  const severityOrder = ["low", "medium", "high", "critical"];
3324
4474
  const minSeverityIndex = severityOrder.indexOf(options.severityThreshold ?? "low");
3325
4475
  const filtered = deduplicated.filter(
@@ -3370,7 +4520,8 @@ async function scan(options) {
3370
4520
  durationMs,
3371
4521
  filesPerSecond: files.size > 0 ? Math.round(files.size / durationMs * 1e3) : 0,
3372
4522
  engineTimings
3373
- }
4523
+ },
4524
+ specCompliance
3374
4525
  };
3375
4526
  }
3376
4527
  async function fix(options) {
@@ -3387,6 +4538,7 @@ async function fix(options) {
3387
4538
  ALL_ENGINES,
3388
4539
  RULE_CATALOG,
3389
4540
  applyFixes,
4541
+ checkISLCompliance,
3390
4542
  classifyPath,
3391
4543
  fix,
3392
4544
  getRuleOrDefault,