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.
- package/dist/scanner/index.d.ts +95 -3
- package/dist/scanner/index.js +1167 -15
- package/dist/scanner/index.js.map +1 -1
- package/package.json +1 -1
package/dist/scanner/index.js
CHANGED
|
@@ -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
|
|
33
|
-
var
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
3188
|
-
if (!(0,
|
|
4308
|
+
const fullPath = (0, import_path5.join)(projectRoot, filePath);
|
|
4309
|
+
if (!(0, import_fs4.existsSync)(fullPath)) continue;
|
|
3189
4310
|
try {
|
|
3190
|
-
const content = (0,
|
|
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,
|
|
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,
|