@securecode-ai/mcp 0.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/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/api/client.d.ts +22 -0
- package/dist/api/client.js +141 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/types.d.ts +129 -0
- package/dist/api/types.js +3 -0
- package/dist/api/types.js.map +1 -0
- package/dist/approval/auditLog.d.ts +3 -0
- package/dist/approval/auditLog.js +67 -0
- package/dist/approval/auditLog.js.map +1 -0
- package/dist/approval/broker.d.ts +11 -0
- package/dist/approval/broker.js +252 -0
- package/dist/approval/broker.js.map +1 -0
- package/dist/approval/types.d.ts +26 -0
- package/dist/approval/types.js +58 -0
- package/dist/approval/types.js.map +1 -0
- package/dist/attack/agentLoop.d.ts +21 -0
- package/dist/attack/agentLoop.js +148 -0
- package/dist/attack/agentLoop.js.map +1 -0
- package/dist/attack/executor.d.ts +17 -0
- package/dist/attack/executor.js +162 -0
- package/dist/attack/executor.js.map +1 -0
- package/dist/attack/policy.d.ts +26 -0
- package/dist/attack/policy.js +73 -0
- package/dist/attack/policy.js.map +1 -0
- package/dist/attack/protocol.d.ts +64 -0
- package/dist/attack/protocol.js +3 -0
- package/dist/attack/protocol.js.map +1 -0
- package/dist/attack/report.d.ts +16 -0
- package/dist/attack/report.js +54 -0
- package/dist/attack/report.js.map +1 -0
- package/dist/auth/credentialStore.d.ts +20 -0
- package/dist/auth/credentialStore.js +148 -0
- package/dist/auth/credentialStore.js.map +1 -0
- package/dist/auth/keychain.d.ts +14 -0
- package/dist/auth/keychain.js +267 -0
- package/dist/auth/keychain.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +170 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +183 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +2 -0
- package/dist/mcp/tools.js +120 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mcp/types.d.ts +26 -0
- package/dist/mcp/types.js +3 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/project-map/astHelpers.d.ts +89 -0
- package/dist/project-map/astHelpers.js +468 -0
- package/dist/project-map/astHelpers.js.map +1 -0
- package/dist/project-map/cache.d.ts +11 -0
- package/dist/project-map/cache.js +86 -0
- package/dist/project-map/cache.js.map +1 -0
- package/dist/project-map/confidence.d.ts +50 -0
- package/dist/project-map/confidence.js +87 -0
- package/dist/project-map/confidence.js.map +1 -0
- package/dist/project-map/crossFile.d.ts +73 -0
- package/dist/project-map/crossFile.js +33 -0
- package/dist/project-map/crossFile.js.map +1 -0
- package/dist/project-map/detectors.d.ts +83 -0
- package/dist/project-map/detectors.js +376 -0
- package/dist/project-map/detectors.js.map +1 -0
- package/dist/project-map/layer1.d.ts +74 -0
- package/dist/project-map/layer1.js +928 -0
- package/dist/project-map/layer1.js.map +1 -0
- package/dist/project-map/layer2.d.ts +30 -0
- package/dist/project-map/layer2.js +249 -0
- package/dist/project-map/layer2.js.map +1 -0
- package/dist/project-map/mapBuilder.d.ts +17 -0
- package/dist/project-map/mapBuilder.js +164 -0
- package/dist/project-map/mapBuilder.js.map +1 -0
- package/dist/project-map/parserLoader.d.ts +61 -0
- package/dist/project-map/parserLoader.js +175 -0
- package/dist/project-map/parserLoader.js.map +1 -0
- package/dist/project-map/schemas.d.ts +28 -0
- package/dist/project-map/schemas.js +131 -0
- package/dist/project-map/schemas.js.map +1 -0
- package/dist/project-map/types.d.ts +232 -0
- package/dist/project-map/types.js +61 -0
- package/dist/project-map/types.js.map +1 -0
- package/dist/tools/attack.d.ts +2 -0
- package/dist/tools/attack.js +80 -0
- package/dist/tools/attack.js.map +1 -0
- package/dist/tools/fix.d.ts +2 -0
- package/dist/tools/fix.js +60 -0
- package/dist/tools/fix.js.map +1 -0
- package/dist/tools/map.d.ts +2 -0
- package/dist/tools/map.js +46 -0
- package/dist/tools/map.js.map +1 -0
- package/dist/tools/scan.d.ts +2 -0
- package/dist/tools/scan.js +55 -0
- package/dist/tools/scan.js.map +1 -0
- package/dist/utils/files.d.ts +8 -0
- package/dist/utils/files.js +79 -0
- package/dist/utils/files.js.map +1 -0
- package/package.json +53 -0
- package/resources/tree-sitter/tree-sitter-javascript.wasm +0 -0
- package/resources/tree-sitter/tree-sitter-python.wasm +0 -0
- package/resources/tree-sitter/tree-sitter-tsx.wasm +0 -0
- package/resources/tree-sitter/tree-sitter-typescript.wasm +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 4 — confidence scoring + runtime-confirmed upgrade.
|
|
4
|
+
*
|
|
5
|
+
* Centralizes the README's confidence model so Layer 1, Layer 2, the
|
|
6
|
+
* runtime trace merger, and the cache all share one definition.
|
|
7
|
+
*
|
|
8
|
+
* 1.0 — STATIC_LITERAL (string-literal path, directly-registered middleware)
|
|
9
|
+
* 0.8 — RUNTIME_CONFIRMED (trace hook observed the relationship)
|
|
10
|
+
* 0.6 — INTRA_FILE_DATAFLOW (resolved from a same-file const/variable)
|
|
11
|
+
* 0.2 — UNRESOLVED (could not statically resolve; not yet traced)
|
|
12
|
+
* 0.15 — META_PROGRAMMING (new Function / vm.runIn* / dynamic require)
|
|
13
|
+
* 0.1 — FULLY_UNRESOLVED (no usable evidence)
|
|
14
|
+
* 0.0 — DYNAMIC_PATTERN (D1-D10 flagged but un-confirmed)
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.aggregateConfidence = aggregateConfidence;
|
|
18
|
+
exports.patternMetaConfidence = patternMetaConfidence;
|
|
19
|
+
exports.applyRuntimeConfirmation = applyRuntimeConfirmation;
|
|
20
|
+
exports.markUnresolved = markUnresolved;
|
|
21
|
+
const types_1 = require("./types");
|
|
22
|
+
/**
|
|
23
|
+
* Pick the lowest confidence among the evidence gathered for the record.
|
|
24
|
+
*
|
|
25
|
+
* `pathConfidence` counts as evidence: a route registered at a string-literal
|
|
26
|
+
* path is well-understood even if it has no middleware, params or calls.
|
|
27
|
+
*
|
|
28
|
+
* A record with NO evidence at all — nothing resolved about the path and no
|
|
29
|
+
* relationships — scores FULLY_UNRESOLVED. Scoring it STATIC_LITERAL would
|
|
30
|
+
* report maximum certainty about an endpoint we know nothing about, and this
|
|
31
|
+
* value travels into EndpointContext and on to the AI scan stages.
|
|
32
|
+
*/
|
|
33
|
+
function aggregateConfidence(record) {
|
|
34
|
+
const cs = [];
|
|
35
|
+
if (typeof record.pathConfidence === 'number')
|
|
36
|
+
cs.push(record.pathConfidence);
|
|
37
|
+
for (const m of record.middleware)
|
|
38
|
+
cs.push(m.confidence);
|
|
39
|
+
for (const p of record.params)
|
|
40
|
+
cs.push(p.confidence);
|
|
41
|
+
for (const c of record.callGraph)
|
|
42
|
+
cs.push(c.confidence);
|
|
43
|
+
if (cs.length === 0)
|
|
44
|
+
return types_1.Confidence.FULLY_UNRESOLVED;
|
|
45
|
+
return Math.min(...cs);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Map a Layer 2 pattern type to the META_PROGRAMMING confidence the
|
|
49
|
+
* record gets if that pattern is the only evidence for a relationship.
|
|
50
|
+
* (D1-D10 all start at 0.0; once a runtime trace confirms them, they
|
|
51
|
+
* jump to 0.8.)
|
|
52
|
+
*/
|
|
53
|
+
function patternMetaConfidence(_patternType) {
|
|
54
|
+
return types_1.Confidence.META_PROGRAMMING;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Upgrade a record's confidence after a runtime trace confirms it.
|
|
58
|
+
* Per the README: confirmed-but-previously-unresolved entries go 0.2 -> 0.8.
|
|
59
|
+
* Records that were META_PROGRAMMING (0.15) also upgrade to 0.8 on confirmation.
|
|
60
|
+
*
|
|
61
|
+
* Absence of evidence (trace ran, endpoint not hit) does NOT downgrade —
|
|
62
|
+
* the spec is explicit: we leave confidence low, we don't mark "absent".
|
|
63
|
+
*/
|
|
64
|
+
function applyRuntimeConfirmation(record) {
|
|
65
|
+
const upgraded = {
|
|
66
|
+
...record,
|
|
67
|
+
runtimeConfirmed: true,
|
|
68
|
+
middleware: record.middleware.map(m => m.confidence === types_1.Confidence.UNRESOLVED || m.confidence === types_1.Confidence.META_PROGRAMMING
|
|
69
|
+
? { ...m, confidence: types_1.Confidence.RUNTIME_CONFIRMED }
|
|
70
|
+
: m),
|
|
71
|
+
};
|
|
72
|
+
upgraded.confidence = aggregateConfidence(upgraded);
|
|
73
|
+
return upgraded;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Cross-file middleware resolution: given a middleware entry whose
|
|
77
|
+
* sourceFile is unknown (because the import was dynamic or unresolvable),
|
|
78
|
+
* set it to UNRESOLVED confidence. Used by the builder when an import
|
|
79
|
+
* could not be resolved to a file.
|
|
80
|
+
*/
|
|
81
|
+
function markUnresolved(entry) {
|
|
82
|
+
if (entry.sourceFile === '' || entry.sourceFile === '?') {
|
|
83
|
+
return { ...entry, sourceFile: '', confidence: types_1.Confidence.UNRESOLVED };
|
|
84
|
+
}
|
|
85
|
+
return entry;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=confidence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"confidence.js","sourceRoot":"","sources":["../../src/project-map/confidence.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;GAaG;;AAeH,kDAQC;AAQD,sDAEC;AAUD,4DAYC;AAQD,wCAKC;AAlED,mCAAsE;AAEtE;;;;;;;;;;GAUG;AACH,SAAgB,mBAAmB,CAAC,MAAsB;IACtD,MAAM,EAAE,GAAa,EAAE,CAAC;IACxB,IAAI,OAAO,MAAM,CAAC,cAAc,KAAK,QAAQ;QAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IAC9E,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,UAAU;QAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IACzD,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM;QAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IACrD,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,SAAS;QAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IACxD,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,kBAAU,CAAC,gBAAgB,CAAC;IACxD,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC;AAC3B,CAAC;AAED;;;;;GAKG;AACH,SAAgB,qBAAqB,CAAC,YAAoB;IACtD,OAAO,kBAAU,CAAC,gBAAgB,CAAC;AACvC,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,wBAAwB,CAAC,MAAsB;IAC3D,MAAM,QAAQ,GAAmB;QAC7B,GAAG,MAAM;QACT,gBAAgB,EAAE,IAAI;QACtB,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAClC,CAAC,CAAC,UAAU,KAAK,kBAAU,CAAC,UAAU,IAAI,CAAC,CAAC,UAAU,KAAK,kBAAU,CAAC,gBAAgB;YAClF,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,kBAAU,CAAC,iBAAiB,EAAE;YACpD,CAAC,CAAC,CAAC,CACV;KACJ,CAAC;IACF,QAAQ,CAAC,UAAU,GAAG,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IACpD,OAAO,QAAQ,CAAC;AACpB,CAAC;AAED;;;;;GAKG;AACH,SAAgB,cAAc,CAAC,KAAsB;IACjD,IAAI,KAAK,CAAC,UAAU,KAAK,EAAE,IAAI,KAAK,CAAC,UAAU,KAAK,GAAG,EAAE,CAAC;QACtD,OAAO,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,UAAU,EAAE,kBAAU,CAAC,UAAU,EAAE,CAAC;IAC3E,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4 — the facts Layer 1 needs from OUTSIDE the file it is extracting.
|
|
3
|
+
*
|
|
4
|
+
* Three questions cannot be answered from one file, and all three change the
|
|
5
|
+
* security reading of an endpoint:
|
|
6
|
+
*
|
|
7
|
+
* 1. What does this middleware actually check? `requireApiKey` is just a
|
|
8
|
+
* name until the module defining it is read.
|
|
9
|
+
* 2. What is this router mounted behind? A route file registered by
|
|
10
|
+
* `app.use('/api', requireAuth)` somewhere else is authenticated, and
|
|
11
|
+
* contains no evidence of it.
|
|
12
|
+
* 3. Is this imported singleton a data layer? `lib/prisma.ts` constructs the
|
|
13
|
+
* client so that route files never import `@prisma/client` themselves.
|
|
14
|
+
*
|
|
15
|
+
* The types live here so `layer1.ts` stays a pure AST function; `cache.ts`,
|
|
16
|
+
* which owns the filesystem, builds the context.
|
|
17
|
+
*/
|
|
18
|
+
import { TreeSitterNode } from './parserLoader';
|
|
19
|
+
import { AuthScheme } from './types';
|
|
20
|
+
/** A parsed dependency of the file being extracted. */
|
|
21
|
+
export interface DepFile {
|
|
22
|
+
/** Workspace-relative path. */
|
|
23
|
+
file: string;
|
|
24
|
+
source: string;
|
|
25
|
+
root: TreeSitterNode;
|
|
26
|
+
imports: Map<string, string>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Middleware that applies to every route in a file because of where the file's
|
|
30
|
+
* router was mounted, resolved at the mount site (which is the only place the
|
|
31
|
+
* prefix and the middleware order are both visible).
|
|
32
|
+
*/
|
|
33
|
+
export interface InheritedMiddleware {
|
|
34
|
+
name: string;
|
|
35
|
+
/** File the middleware is defined in, or the mounting file. */
|
|
36
|
+
sourceFile: string;
|
|
37
|
+
registrationLine: number;
|
|
38
|
+
confidence: number;
|
|
39
|
+
/** Auth scheme this middleware implements, resolved where it was mounted. */
|
|
40
|
+
auth: AuthScheme;
|
|
41
|
+
/** True when the middleware expression could not be resolved at all. */
|
|
42
|
+
dynamic: boolean;
|
|
43
|
+
}
|
|
44
|
+
/** Where a file's router is mounted, and what guards it there. */
|
|
45
|
+
export interface MountPoint {
|
|
46
|
+
/** Path prefix the router is mounted under ('/api/users'). */
|
|
47
|
+
prefix: string;
|
|
48
|
+
/** File that performed the mount. */
|
|
49
|
+
mountedBy: string;
|
|
50
|
+
registrationLine: number;
|
|
51
|
+
/** Middleware registered before the mount that covers this prefix. */
|
|
52
|
+
inherited: InheritedMiddleware[];
|
|
53
|
+
}
|
|
54
|
+
export interface CrossFileContext {
|
|
55
|
+
/** Import spec as written in this file -> the parsed dependency. */
|
|
56
|
+
deps: Map<string, DepFile>;
|
|
57
|
+
/** Middleware applying to every route in this file. */
|
|
58
|
+
inherited: InheritedMiddleware[];
|
|
59
|
+
/**
|
|
60
|
+
* Prefix this file's router is mounted under, or '' when it is not mounted
|
|
61
|
+
* anywhere we can see. Kept separate from `EndpointRecord.path`, which the
|
|
62
|
+
* contract defines as the path AS REGISTERED at the route site.
|
|
63
|
+
*/
|
|
64
|
+
mountPrefix: string;
|
|
65
|
+
/** Next.js edge middleware guarding routes whose path matches. */
|
|
66
|
+
nextMiddleware?: {
|
|
67
|
+
matchers: string[];
|
|
68
|
+
auth: AuthScheme;
|
|
69
|
+
file: string;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Join a mount prefix and a router-local path into the external path. */
|
|
73
|
+
export declare function joinMountedPath(prefix: string, routePath: string): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 4 — the facts Layer 1 needs from OUTSIDE the file it is extracting.
|
|
4
|
+
*
|
|
5
|
+
* Three questions cannot be answered from one file, and all three change the
|
|
6
|
+
* security reading of an endpoint:
|
|
7
|
+
*
|
|
8
|
+
* 1. What does this middleware actually check? `requireApiKey` is just a
|
|
9
|
+
* name until the module defining it is read.
|
|
10
|
+
* 2. What is this router mounted behind? A route file registered by
|
|
11
|
+
* `app.use('/api', requireAuth)` somewhere else is authenticated, and
|
|
12
|
+
* contains no evidence of it.
|
|
13
|
+
* 3. Is this imported singleton a data layer? `lib/prisma.ts` constructs the
|
|
14
|
+
* client so that route files never import `@prisma/client` themselves.
|
|
15
|
+
*
|
|
16
|
+
* The types live here so `layer1.ts` stays a pure AST function; `cache.ts`,
|
|
17
|
+
* which owns the filesystem, builds the context.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.joinMountedPath = joinMountedPath;
|
|
21
|
+
/** Join a mount prefix and a router-local path into the external path. */
|
|
22
|
+
function joinMountedPath(prefix, routePath) {
|
|
23
|
+
if (!prefix)
|
|
24
|
+
return routePath;
|
|
25
|
+
if (routePath === '?' || routePath === '')
|
|
26
|
+
return prefix;
|
|
27
|
+
if (routePath === '/')
|
|
28
|
+
return prefix;
|
|
29
|
+
const left = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
|
|
30
|
+
const right = routePath.startsWith('/') ? routePath : '/' + routePath;
|
|
31
|
+
return left + right;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=crossFile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crossFile.js","sourceRoot":"","sources":["../../src/project-map/crossFile.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;;AA0DH,0CAOC;AARD,0EAA0E;AAC1E,SAAgB,eAAe,CAAC,MAAc,EAAE,SAAiB;IAC7D,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAC9B,IAAI,SAAS,KAAK,GAAG,IAAI,SAAS,KAAK,EAAE;QAAE,OAAO,MAAM,CAAC;IACzD,IAAI,SAAS,KAAK,GAAG;QAAE,OAAO,MAAM,CAAC;IACrC,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACjE,MAAM,KAAK,GAAG,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,GAAG,SAAS,CAAC;IACtE,OAAO,IAAI,GAAG,KAAK,CAAC;AACxB,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4 Layer 1 — framework / library detectors.
|
|
3
|
+
*
|
|
4
|
+
* Each detector takes the file's source text + the assignments map + the
|
|
5
|
+
* imports map and returns a label (or 'unknown'). Keeping these in one
|
|
6
|
+
* place means Layer 1 extraction can stay focused on routing structure.
|
|
7
|
+
*/
|
|
8
|
+
import { TreeSitterNode } from './parserLoader';
|
|
9
|
+
import type { DataLayer } from './types';
|
|
10
|
+
export type AuthMatch = 'jwt' | 'session' | 'api-key' | 'oauth' | 'basic' | 'none';
|
|
11
|
+
/** Detect auth scheme from imports + call sites. */
|
|
12
|
+
export declare function detectAuth(imports: Map<string, string>, root: TreeSitterNode, source: string): AuthMatch;
|
|
13
|
+
export type DataLayerMatch = 'prisma' | 'typeorm' | 'sequelize' | 'knex' | 'mongoose' | 'raw-sql' | 'supabase' | 'sqlalchemy' | 'django-orm' | 'none';
|
|
14
|
+
/** Detect ORM / data layer from imports. */
|
|
15
|
+
export declare function detectDataLayer(imports: Map<string, string>): DataLayerMatch;
|
|
16
|
+
/**
|
|
17
|
+
* Map an internal detector match to the public `DataLayer` taxonomy.
|
|
18
|
+
*
|
|
19
|
+
* The Project Map ground truth deliberately uses `"unknown"` (not `"none"`)
|
|
20
|
+
* for a handler that DOES touch a data client the taxonomy cannot name.
|
|
21
|
+
* Supabase/PostgREST is now labeled `"supabase"`. `"none"` is reserved for
|
|
22
|
+
* "the handler touches no data layer at all".
|
|
23
|
+
*/
|
|
24
|
+
export declare function toDataLayer(layer: DataLayerMatch): DataLayer;
|
|
25
|
+
/**
|
|
26
|
+
* Local names that stand for the data layer in this file: the imported client
|
|
27
|
+
* itself plus anything constructed from it (`const prisma = new PrismaClient()`,
|
|
28
|
+
* `const supabase = createClient(...)`).
|
|
29
|
+
*
|
|
30
|
+
* Used to decide whether a given handler actually touches the data layer, as
|
|
31
|
+
* opposed to merely living in a file that imports one.
|
|
32
|
+
*/
|
|
33
|
+
export declare function dataLayerBindings(layer: DataLayerMatch, imports: Map<string, string>, root: TreeSitterNode, source: string, seed?: Iterable<string>): Set<string>;
|
|
34
|
+
export type ValidatorMatch = 'zod' | 'joi' | 'express-validator' | 'yup' | 'ajv' | 'pydantic' | 'marshmallow' | 'none';
|
|
35
|
+
/** Detect validator library from imports. */
|
|
36
|
+
export declare function detectValidator(imports: Map<string, string>): ValidatorMatch;
|
|
37
|
+
/** True iff a call node is a route registration: app.get('/x', handler) etc. */
|
|
38
|
+
export declare function isRouteRegistration(callNode: TreeSitterNode, source: string): {
|
|
39
|
+
receiver: string | null;
|
|
40
|
+
method: string;
|
|
41
|
+
args: TreeSitterNode[];
|
|
42
|
+
line: number;
|
|
43
|
+
} | null;
|
|
44
|
+
/**
|
|
45
|
+
* True iff a call is a Django URLconf entry: `path("articles/", views.list)`.
|
|
46
|
+
*
|
|
47
|
+
* These are bare calls with no receiver, so they cannot be told from any other
|
|
48
|
+
* one-word function call by shape alone. Membership of a `urlpatterns` list is
|
|
49
|
+
* what makes them a route, and it is also how Django itself decides.
|
|
50
|
+
*/
|
|
51
|
+
export declare function isDjangoRouteRegistration(callNode: TreeSitterNode, source: string): {
|
|
52
|
+
path: TreeSitterNode;
|
|
53
|
+
view: TreeSitterNode | null;
|
|
54
|
+
line: number;
|
|
55
|
+
} | null;
|
|
56
|
+
/** True iff a call node is app.use(...) (global/route middleware registration). */
|
|
57
|
+
export declare function isUseRegistration(callNode: TreeSitterNode, source: string): {
|
|
58
|
+
receiver: string | null;
|
|
59
|
+
args: TreeSitterNode[];
|
|
60
|
+
line: number;
|
|
61
|
+
} | null;
|
|
62
|
+
/** HTTP method normalization for the EndpointRecord. */
|
|
63
|
+
export declare function normalizeMethod(m: string): 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'ALL';
|
|
64
|
+
/**
|
|
65
|
+
* Free response constructors. Django and Flask build the response by calling a
|
|
66
|
+
* function rather than by calling a method on a response object, so the shape
|
|
67
|
+
* is carried by the callee name instead of the receiver.
|
|
68
|
+
*/
|
|
69
|
+
export declare const RESPONSE_CONSTRUCTORS: Record<string, 'json' | 'html' | 'redirect' | 'stream' | 'file' | 'text'>;
|
|
70
|
+
/** Detect response shape from the handler's return / res.send / res.json calls. */
|
|
71
|
+
export declare function detectResponseShape(handlerRoot: TreeSitterNode, source: string): 'json' | 'html' | 'redirect' | 'stream' | 'file' | 'text' | 'unknown';
|
|
72
|
+
/**
|
|
73
|
+
* Detect the auth scheme a specific FUNCTION implements, from evidence inside
|
|
74
|
+
* its own body.
|
|
75
|
+
*
|
|
76
|
+
* Distinct from `detectAuth`, which answers "does this FILE deal in auth" from
|
|
77
|
+
* its imports. That question is the wrong one for a middleware or a FastAPI
|
|
78
|
+
* dependency: `get_db` and `get_current_user` live in the same module, and only
|
|
79
|
+
* one of them is a guard.
|
|
80
|
+
*/
|
|
81
|
+
export declare function detectAuthInFunction(fnRoot: TreeSitterNode, source: string): AuthMatch;
|
|
82
|
+
/** Quick check: does the file import any framework we recognize at all? */
|
|
83
|
+
export declare function isLikelyFrameworkFile(imports: Map<string, string>): boolean;
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 4 Layer 1 — framework / library detectors.
|
|
4
|
+
*
|
|
5
|
+
* Each detector takes the file's source text + the assignments map + the
|
|
6
|
+
* imports map and returns a label (or 'unknown'). Keeping these in one
|
|
7
|
+
* place means Layer 1 extraction can stay focused on routing structure.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.RESPONSE_CONSTRUCTORS = void 0;
|
|
11
|
+
exports.detectAuth = detectAuth;
|
|
12
|
+
exports.detectDataLayer = detectDataLayer;
|
|
13
|
+
exports.toDataLayer = toDataLayer;
|
|
14
|
+
exports.dataLayerBindings = dataLayerBindings;
|
|
15
|
+
exports.detectValidator = detectValidator;
|
|
16
|
+
exports.isRouteRegistration = isRouteRegistration;
|
|
17
|
+
exports.isDjangoRouteRegistration = isDjangoRouteRegistration;
|
|
18
|
+
exports.isUseRegistration = isUseRegistration;
|
|
19
|
+
exports.normalizeMethod = normalizeMethod;
|
|
20
|
+
exports.detectResponseShape = detectResponseShape;
|
|
21
|
+
exports.detectAuthInFunction = detectAuthInFunction;
|
|
22
|
+
exports.isLikelyFrameworkFile = isLikelyFrameworkFile;
|
|
23
|
+
const astHelpers_1 = require("./astHelpers");
|
|
24
|
+
/** Detect auth scheme from imports + call sites. */
|
|
25
|
+
function detectAuth(imports, root, source) {
|
|
26
|
+
const from = (mod) => [...imports.values()].some(s => s.includes(mod));
|
|
27
|
+
if (from('passport-jwt') || from('jsonwebtoken') || from('fastapi-jwt') || from('jose'))
|
|
28
|
+
return 'jwt';
|
|
29
|
+
if (from('express-session') || from('cookie-session') || from('django.contrib.sessions') || from('starlette.sessions'))
|
|
30
|
+
return 'session';
|
|
31
|
+
if (from('passport-google') || from('passport-github') || from('authlib') || from('itsdangerous'))
|
|
32
|
+
return 'oauth';
|
|
33
|
+
if (from('passport-http') || from('http-auth'))
|
|
34
|
+
return 'basic';
|
|
35
|
+
// api-key: look for x-api-key access or a Depends(api_key) in the file
|
|
36
|
+
for (const n of (0, astHelpers_1.walk)(root)) {
|
|
37
|
+
if (n.type === 'subscript_expression' || n.type === 'subscript') {
|
|
38
|
+
const txt = source.slice(n.startIndex, n.endIndex);
|
|
39
|
+
if (/x-api-key|apiKey|api_key/i.test(txt))
|
|
40
|
+
return 'api-key';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// JWT heuristic: jsonwebtoken.verify call
|
|
44
|
+
for (const n of (0, astHelpers_1.walk)(root)) {
|
|
45
|
+
if (n.type === 'call_expression' || n.type === 'call') {
|
|
46
|
+
const p = (0, astHelpers_1.callParts)(n, source);
|
|
47
|
+
if (!p)
|
|
48
|
+
continue;
|
|
49
|
+
if (p.receiver === 'jwt' && p.method === 'verify')
|
|
50
|
+
return 'jwt';
|
|
51
|
+
if (p.receiver === 'passport' && p.method === 'authenticate') {
|
|
52
|
+
return 'oauth'; // passport.authenticate('google', ...) default
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return 'none';
|
|
57
|
+
}
|
|
58
|
+
/** Module specs that identify each data layer, most specific first. */
|
|
59
|
+
const DATA_LAYER_SPECS = [
|
|
60
|
+
{ layer: 'prisma', test: s => s.includes('@prisma/client') },
|
|
61
|
+
{ layer: 'typeorm', test: s => s.includes('typeorm') },
|
|
62
|
+
{ layer: 'sequelize', test: s => s.includes('sequelize') },
|
|
63
|
+
{ layer: 'knex', test: s => s === 'knex' || s.includes('/knex') },
|
|
64
|
+
{ layer: 'mongoose', test: s => s.includes('mongoose') },
|
|
65
|
+
{ layer: 'supabase', test: s => s.includes('@supabase/') || s === 'supabase' || s.startsWith('supabase.') },
|
|
66
|
+
{ layer: 'sqlalchemy', test: s => s === 'sqlalchemy' || s.startsWith('sqlalchemy.') },
|
|
67
|
+
{ layer: 'django-orm', test: s => s === 'django' || s.startsWith('django.') },
|
|
68
|
+
];
|
|
69
|
+
/** Detect ORM / data layer from imports. */
|
|
70
|
+
function detectDataLayer(imports) {
|
|
71
|
+
const specs = [...imports.values()];
|
|
72
|
+
for (const { layer, test } of DATA_LAYER_SPECS) {
|
|
73
|
+
if (specs.some(test))
|
|
74
|
+
return layer;
|
|
75
|
+
}
|
|
76
|
+
return 'none';
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Map an internal detector match to the public `DataLayer` taxonomy.
|
|
80
|
+
*
|
|
81
|
+
* The Project Map ground truth deliberately uses `"unknown"` (not `"none"`)
|
|
82
|
+
* for a handler that DOES touch a data client the taxonomy cannot name.
|
|
83
|
+
* Supabase/PostgREST is now labeled `"supabase"`. `"none"` is reserved for
|
|
84
|
+
* "the handler touches no data layer at all".
|
|
85
|
+
*/
|
|
86
|
+
function toDataLayer(layer) {
|
|
87
|
+
switch (layer) {
|
|
88
|
+
case 'supabase': return 'supabase';
|
|
89
|
+
default: return layer;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Local names that stand for the data layer in this file: the imported client
|
|
94
|
+
* itself plus anything constructed from it (`const prisma = new PrismaClient()`,
|
|
95
|
+
* `const supabase = createClient(...)`).
|
|
96
|
+
*
|
|
97
|
+
* Used to decide whether a given handler actually touches the data layer, as
|
|
98
|
+
* opposed to merely living in a file that imports one.
|
|
99
|
+
*/
|
|
100
|
+
function dataLayerBindings(layer, imports, root, source, seed) {
|
|
101
|
+
const names = new Set(seed ?? []);
|
|
102
|
+
if (layer === 'none')
|
|
103
|
+
return names;
|
|
104
|
+
const match = DATA_LAYER_SPECS.find(d => d.layer === layer);
|
|
105
|
+
for (const [local, spec] of imports) {
|
|
106
|
+
if (match ? match.test(spec) : false)
|
|
107
|
+
names.add(local);
|
|
108
|
+
}
|
|
109
|
+
// Anything built from one of those names is also the data layer.
|
|
110
|
+
for (const n of (0, astHelpers_1.walk)(root)) {
|
|
111
|
+
if (n.type !== 'variable_declarator' && n.type !== 'assignment')
|
|
112
|
+
continue;
|
|
113
|
+
const target = n.child(0);
|
|
114
|
+
const value = n.child(n.childCount - 1);
|
|
115
|
+
if (!target || !value || target === value)
|
|
116
|
+
continue;
|
|
117
|
+
if (target.type !== 'identifier')
|
|
118
|
+
continue;
|
|
119
|
+
const valueText = source.slice(value.startIndex, value.endIndex);
|
|
120
|
+
if ([...names].some(b => new RegExp(`\\b${escapeForRegExp(b)}\\b`).test(valueText))) {
|
|
121
|
+
names.add(source.slice(target.startIndex, target.endIndex));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return names;
|
|
125
|
+
}
|
|
126
|
+
function escapeForRegExp(s) {
|
|
127
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
128
|
+
}
|
|
129
|
+
/** Detect validator library from imports. */
|
|
130
|
+
function detectValidator(imports) {
|
|
131
|
+
const specs = [...imports.values()];
|
|
132
|
+
if (specs.some(s => s === 'zod' || s.startsWith('zod/')))
|
|
133
|
+
return 'zod';
|
|
134
|
+
if (specs.some(s => s.includes('joi')))
|
|
135
|
+
return 'joi';
|
|
136
|
+
if (specs.some(s => s.includes('express-validator')))
|
|
137
|
+
return 'express-validator';
|
|
138
|
+
if (specs.some(s => s === 'yup' || s.startsWith('yup/')))
|
|
139
|
+
return 'yup';
|
|
140
|
+
if (specs.some(s => s.includes('ajv')))
|
|
141
|
+
return 'ajv';
|
|
142
|
+
if (specs.some(s => s === 'pydantic' || s.startsWith('pydantic.')))
|
|
143
|
+
return 'pydantic';
|
|
144
|
+
if (specs.some(s => s === 'marshmallow' || s.startsWith('marshmallow.')))
|
|
145
|
+
return 'marshmallow';
|
|
146
|
+
return 'none';
|
|
147
|
+
}
|
|
148
|
+
/** Known route-registration method names across Express/Fastify/Koa/Python. */
|
|
149
|
+
const ROUTE_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'all', 'route']);
|
|
150
|
+
const KNOWN_RECEIVERS = new Set(['app', 'router', 'server', 'fastify', 'api', 'route', 'controller', 'rtr']);
|
|
151
|
+
/** Node types a route path can legitimately be written as. */
|
|
152
|
+
function isPathExpression(node) {
|
|
153
|
+
return (0, astHelpers_1.isStringLiteral)(node)
|
|
154
|
+
|| node.type === 'identifier'
|
|
155
|
+
|| node.type === 'template_string'
|
|
156
|
+
|| node.type === 'template_literal';
|
|
157
|
+
}
|
|
158
|
+
/** True iff a call node is a route registration: app.get('/x', handler) etc. */
|
|
159
|
+
function isRouteRegistration(callNode, source) {
|
|
160
|
+
if (callNode.type !== 'call_expression' && callNode.type !== 'call')
|
|
161
|
+
return null;
|
|
162
|
+
const p = (0, astHelpers_1.callParts)(callNode, source);
|
|
163
|
+
if (!p || !p.receiver)
|
|
164
|
+
return null;
|
|
165
|
+
if (!KNOWN_RECEIVERS.has(p.receiver))
|
|
166
|
+
return null;
|
|
167
|
+
if (!ROUTE_METHODS.has(p.method.toLowerCase()))
|
|
168
|
+
return null;
|
|
169
|
+
if (p.args.length < 1)
|
|
170
|
+
return null;
|
|
171
|
+
// The path may be a constant or a template literal, not just a literal.
|
|
172
|
+
// Resolution (and the '?' fallback for a genuinely dynamic path) belongs to
|
|
173
|
+
// the caller; dropping the registration here loses the endpoint entirely.
|
|
174
|
+
if (!isPathExpression(p.args[0]))
|
|
175
|
+
return null;
|
|
176
|
+
// A non-literal single argument is far more likely to be a getter
|
|
177
|
+
// (`app.get(setting)`) than a route, so require a handler alongside it.
|
|
178
|
+
if (!(0, astHelpers_1.isStringLiteral)(p.args[0]) && p.args.length < 2)
|
|
179
|
+
return null;
|
|
180
|
+
return p;
|
|
181
|
+
}
|
|
182
|
+
/** Django URLconf registration functions. */
|
|
183
|
+
const DJANGO_ROUTE_FUNCTIONS = new Set(['path', 're_path', 'url']);
|
|
184
|
+
/**
|
|
185
|
+
* True iff a call is a Django URLconf entry: `path("articles/", views.list)`.
|
|
186
|
+
*
|
|
187
|
+
* These are bare calls with no receiver, so they cannot be told from any other
|
|
188
|
+
* one-word function call by shape alone. Membership of a `urlpatterns` list is
|
|
189
|
+
* what makes them a route, and it is also how Django itself decides.
|
|
190
|
+
*/
|
|
191
|
+
function isDjangoRouteRegistration(callNode, source) {
|
|
192
|
+
if (callNode.type !== 'call')
|
|
193
|
+
return null;
|
|
194
|
+
const p = (0, astHelpers_1.callParts)(callNode, source);
|
|
195
|
+
if (!p || p.receiver !== null)
|
|
196
|
+
return null;
|
|
197
|
+
if (!DJANGO_ROUTE_FUNCTIONS.has(p.method))
|
|
198
|
+
return null;
|
|
199
|
+
if (p.args.length < 1 || !(0, astHelpers_1.isStringLiteral)(p.args[0]))
|
|
200
|
+
return null;
|
|
201
|
+
if (!(0, astHelpers_1.firstAncestor)(callNode, n => isUrlpatternsAssignment(n, source)))
|
|
202
|
+
return null;
|
|
203
|
+
const positional = p.args.filter(a => a.type !== 'keyword_argument');
|
|
204
|
+
return { path: positional[0], view: positional[1] ?? null, line: p.line };
|
|
205
|
+
}
|
|
206
|
+
/** True iff a node is the `urlpatterns = [...]` assignment. */
|
|
207
|
+
function isUrlpatternsAssignment(node, source) {
|
|
208
|
+
if (node.type !== 'assignment')
|
|
209
|
+
return false;
|
|
210
|
+
const target = node.child(0);
|
|
211
|
+
if (!target)
|
|
212
|
+
return false;
|
|
213
|
+
return source.slice(target.startIndex, target.endIndex).trim() === 'urlpatterns';
|
|
214
|
+
}
|
|
215
|
+
/** True iff a call node is app.use(...) (global/route middleware registration). */
|
|
216
|
+
function isUseRegistration(callNode, source) {
|
|
217
|
+
if (callNode.type !== 'call_expression' && callNode.type !== 'call')
|
|
218
|
+
return null;
|
|
219
|
+
const p = (0, astHelpers_1.callParts)(callNode, source);
|
|
220
|
+
if (!p || !p.receiver)
|
|
221
|
+
return null;
|
|
222
|
+
if (p.method !== 'use')
|
|
223
|
+
return null;
|
|
224
|
+
if (!KNOWN_RECEIVERS.has(p.receiver))
|
|
225
|
+
return null;
|
|
226
|
+
return { receiver: p.receiver, args: p.args, line: p.line };
|
|
227
|
+
}
|
|
228
|
+
/** HTTP method normalization for the EndpointRecord. */
|
|
229
|
+
function normalizeMethod(m) {
|
|
230
|
+
const u = m.toUpperCase();
|
|
231
|
+
if (u === 'ROUTE' || u === 'ALL')
|
|
232
|
+
return 'ALL';
|
|
233
|
+
return u;
|
|
234
|
+
}
|
|
235
|
+
/** Response objects whose methods describe the body being sent. */
|
|
236
|
+
const RESPONSE_OBJECTS = new Set(['res', 'response', 'reply', 'ctx', 'NextResponse', 'Response']);
|
|
237
|
+
/**
|
|
238
|
+
* Free response constructors. Django and Flask build the response by calling a
|
|
239
|
+
* function rather than by calling a method on a response object, so the shape
|
|
240
|
+
* is carried by the callee name instead of the receiver.
|
|
241
|
+
*/
|
|
242
|
+
exports.RESPONSE_CONSTRUCTORS = {
|
|
243
|
+
JsonResponse: 'json',
|
|
244
|
+
jsonify: 'json',
|
|
245
|
+
render: 'html',
|
|
246
|
+
render_template: 'html',
|
|
247
|
+
redirect: 'redirect',
|
|
248
|
+
HttpResponseRedirect: 'redirect',
|
|
249
|
+
FileResponse: 'file',
|
|
250
|
+
send_file: 'file',
|
|
251
|
+
StreamingHttpResponse: 'stream',
|
|
252
|
+
HttpResponse: 'text',
|
|
253
|
+
};
|
|
254
|
+
/** Detect response shape from the handler's return / res.send / res.json calls. */
|
|
255
|
+
function detectResponseShape(handlerRoot, source) {
|
|
256
|
+
let sawJson = false, sawRender = false, sawRedirect = false, sawSend = false, sawStream = false, sawFile = false;
|
|
257
|
+
const note = (shape) => {
|
|
258
|
+
switch (shape) {
|
|
259
|
+
case 'json':
|
|
260
|
+
sawJson = true;
|
|
261
|
+
break;
|
|
262
|
+
case 'html':
|
|
263
|
+
sawRender = true;
|
|
264
|
+
break;
|
|
265
|
+
case 'redirect':
|
|
266
|
+
sawRedirect = true;
|
|
267
|
+
break;
|
|
268
|
+
case 'text':
|
|
269
|
+
sawSend = true;
|
|
270
|
+
break;
|
|
271
|
+
case 'stream':
|
|
272
|
+
sawStream = true;
|
|
273
|
+
break;
|
|
274
|
+
case 'file':
|
|
275
|
+
sawFile = true;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
for (const n of (0, astHelpers_1.walk)(handlerRoot)) {
|
|
280
|
+
if (n.type !== 'call_expression' && n.type !== 'call')
|
|
281
|
+
continue;
|
|
282
|
+
const p = (0, astHelpers_1.callParts)(n, source);
|
|
283
|
+
if (!p)
|
|
284
|
+
continue;
|
|
285
|
+
if (!p.receiver) {
|
|
286
|
+
const shape = exports.RESPONSE_CONSTRUCTORS[p.method];
|
|
287
|
+
if (shape)
|
|
288
|
+
note(shape);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (RESPONSE_OBJECTS.has(p.receiver)) {
|
|
292
|
+
switch (p.method) {
|
|
293
|
+
case 'json':
|
|
294
|
+
sawJson = true;
|
|
295
|
+
break;
|
|
296
|
+
case 'render':
|
|
297
|
+
sawRender = true;
|
|
298
|
+
break;
|
|
299
|
+
case 'redirect':
|
|
300
|
+
sawRedirect = true;
|
|
301
|
+
break;
|
|
302
|
+
case 'send':
|
|
303
|
+
sawSend = true;
|
|
304
|
+
break;
|
|
305
|
+
case 'sendStream':
|
|
306
|
+
case 'stream':
|
|
307
|
+
case 'pipe':
|
|
308
|
+
sawStream = true;
|
|
309
|
+
break;
|
|
310
|
+
case 'sendFile':
|
|
311
|
+
case 'download':
|
|
312
|
+
case 'attachment':
|
|
313
|
+
sawFile = true;
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (sawRedirect)
|
|
319
|
+
return 'redirect';
|
|
320
|
+
if (sawRender)
|
|
321
|
+
return 'html';
|
|
322
|
+
if (sawFile)
|
|
323
|
+
return 'file';
|
|
324
|
+
if (sawStream)
|
|
325
|
+
return 'stream';
|
|
326
|
+
if (sawJson)
|
|
327
|
+
return 'json';
|
|
328
|
+
if (sawSend)
|
|
329
|
+
return 'text';
|
|
330
|
+
return 'unknown';
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Detect the auth scheme a specific FUNCTION implements, from evidence inside
|
|
334
|
+
* its own body.
|
|
335
|
+
*
|
|
336
|
+
* Distinct from `detectAuth`, which answers "does this FILE deal in auth" from
|
|
337
|
+
* its imports. That question is the wrong one for a middleware or a FastAPI
|
|
338
|
+
* dependency: `get_db` and `get_current_user` live in the same module, and only
|
|
339
|
+
* one of them is a guard.
|
|
340
|
+
*/
|
|
341
|
+
function detectAuthInFunction(fnRoot, source) {
|
|
342
|
+
const text = source.slice(fnRoot.startIndex, fnRoot.endIndex);
|
|
343
|
+
if (/x-api-key|\bapi_key\b|\bapiKey\b/i.test(text))
|
|
344
|
+
return 'api-key';
|
|
345
|
+
for (const n of (0, astHelpers_1.walk)(fnRoot)) {
|
|
346
|
+
if (n.type !== 'call_expression' && n.type !== 'call')
|
|
347
|
+
continue;
|
|
348
|
+
const p = (0, astHelpers_1.callParts)(n, source);
|
|
349
|
+
if (!p)
|
|
350
|
+
continue;
|
|
351
|
+
const receiver = (p.receiver ?? '').toLowerCase();
|
|
352
|
+
if (/^(jwt|jose|jsonwebtoken|jwtlib)$/.test(receiver)
|
|
353
|
+
&& ['verify', 'decode', 'sign'].includes(p.method)) {
|
|
354
|
+
return 'jwt';
|
|
355
|
+
}
|
|
356
|
+
if (receiver === 'passport' && p.method === 'authenticate')
|
|
357
|
+
return 'oauth';
|
|
358
|
+
// `req.cookies.get('sb-access-token')` / `request.session[...]`.
|
|
359
|
+
if (/\bcookies?\b|\bsession\b/i.test(p.receiverText ?? ''))
|
|
360
|
+
return 'session';
|
|
361
|
+
}
|
|
362
|
+
if (/\bsession\b/i.test(text))
|
|
363
|
+
return 'session';
|
|
364
|
+
return 'none';
|
|
365
|
+
}
|
|
366
|
+
/** Quick check: does the file import any framework we recognize at all? */
|
|
367
|
+
function isLikelyFrameworkFile(imports) {
|
|
368
|
+
const specs = [...imports.values()];
|
|
369
|
+
return specs.some(s => s === 'express' || s.startsWith('express/') ||
|
|
370
|
+
s === 'fastify' || s.startsWith('fastify/') ||
|
|
371
|
+
s === 'koa' || s.startsWith('koa/') ||
|
|
372
|
+
s === 'koa-router' || s === '@koa/router' ||
|
|
373
|
+
s === 'fastapi' ||
|
|
374
|
+
s === 'django' || s.startsWith('django.'));
|
|
375
|
+
}
|
|
376
|
+
//# sourceMappingURL=detectors.js.map
|