@trustnext/ztam-analyzer 1.0.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/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # @trustnext/ztam-analyzer
2
+
3
+ A read-only static analyzer for Node.js applications. Inspects a project and generates a structured ZTAM integration report — without touching, modifying, or installing anything in the client project.
4
+
5
+ ## What it does
6
+
7
+ - Detects **frameworks** (Express, NestJS, Fastify, Next.js, Koa, Hapi).
8
+ - Detects **authentication libraries** and **user object patterns**.
9
+ - Detects **session backends** and **authorization / RBAC** models.
10
+ - Recommends the least-disruptive **integration strategy** (e.g., Session Bridge, Custom Context Bridge).
11
+ - Generates reports in Terminal, Markdown, and machine-readable JSON.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @trustnext/ztam-analyzer
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ # Print report to terminal
23
+ node cli.js analyze /path/to/app
24
+
25
+ # Save as Markdown and JSON
26
+ node cli.js analyze /path/to/app --output ztam-report.md --json report.json
27
+ ```
28
+
29
+ ## Programmatic API
30
+
31
+ ```javascript
32
+ const { analyze } = require('@trustnext/ztam-analyzer');
33
+
34
+ const report = analyze('/path/to/app');
35
+
36
+ console.log(report.terminal); // terminal-formatted string
37
+ console.log(report.markdown); // markdown-formatted string
38
+ console.log(report.json); // schema-validated JSON string
39
+ console.log(report.strategy); // recommended integration path
40
+ ```
41
+
42
+ ## Important
43
+
44
+ - **Read-only**: Never writes, installs, or executes anything inside the client project.
45
+ - **Generic**: Works for any Node.js application — not tied to any specific customer.
46
+ - **Extensible**: Add new frameworks/libraries by dropping a descriptor into `detectors/`.
package/cli.js ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * cli.js (v2) — Command-line interface for @trustnext/ztam-analyzer
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { analyze } = require('./index');
11
+
12
+ const USAGE = `
13
+ Usage:
14
+ node cli.js analyze <path-to-app> [options]
15
+
16
+ Options:
17
+ --output <file.md> Save the report as a Markdown file
18
+ --json <file.json> Save the report as a machine-readable JSON file
19
+
20
+ Examples:
21
+ node cli.js analyze /home/user/my-app
22
+ node cli.js analyze ./my-app --output ztam-report.md --json report.json
23
+ `;
24
+
25
+ function run() {
26
+ const args = process.argv.slice(2);
27
+
28
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
29
+ console.log(USAGE);
30
+ process.exit(0);
31
+ }
32
+
33
+ const command = args[0];
34
+ if (command !== 'analyze') {
35
+ console.error(`Unknown command: "${command}"`);
36
+ console.log(USAGE);
37
+ process.exit(1);
38
+ }
39
+
40
+ const projectPath = args[1];
41
+ if (!projectPath || projectPath.startsWith('--')) {
42
+ console.error('Error: path to app is required as the first argument after "analyze".');
43
+ console.log(USAGE);
44
+ process.exit(1);
45
+ }
46
+
47
+ const absolutePath = path.resolve(projectPath);
48
+ if (!fs.existsSync(absolutePath)) {
49
+ console.error(`Error: path does not exist: ${absolutePath}`);
50
+ process.exit(1);
51
+ }
52
+
53
+ // Parse options
54
+ let outputFile = null;
55
+ const outputIdx = args.indexOf('--output');
56
+ if (outputIdx !== -1) {
57
+ outputFile = args[outputIdx + 1];
58
+ if (!outputFile || outputFile.startsWith('--')) {
59
+ console.error('Error: --output requires a filename argument.');
60
+ process.exit(1);
61
+ }
62
+ }
63
+
64
+ let jsonFile = null;
65
+ const jsonIdx = args.indexOf('--json');
66
+ if (jsonIdx !== -1) {
67
+ jsonFile = args[jsonIdx + 1];
68
+ if (!jsonFile || jsonFile.startsWith('--')) {
69
+ console.error('Error: --json requires a filename argument.');
70
+ process.exit(1);
71
+ }
72
+ }
73
+
74
+ // Run analysis
75
+ let report;
76
+ try {
77
+ report = analyze(absolutePath);
78
+ } catch (err) {
79
+ console.error(`Analysis failed: ${err.message}`);
80
+ process.exit(1);
81
+ }
82
+
83
+ // Print to terminal (always)
84
+ process.stdout.write(report.terminal);
85
+
86
+ // Write Markdown file (if requested)
87
+ if (outputFile) {
88
+ const outputPath = path.resolve(outputFile);
89
+ try {
90
+ fs.writeFileSync(outputPath, report.markdown, 'utf8');
91
+ console.log(`\nMarkdown report saved to: ${outputPath}`);
92
+ } catch (err) {
93
+ console.error(`Failed to write markdown file: ${err.message}`);
94
+ process.exit(1);
95
+ }
96
+ }
97
+
98
+ // Write JSON file (if requested)
99
+ if (jsonFile) {
100
+ const jsonPath = path.resolve(jsonFile);
101
+ try {
102
+ fs.writeFileSync(jsonPath, report.json, 'utf8');
103
+ console.log(`JSON report saved to: ${jsonPath}`);
104
+ } catch (err) {
105
+ console.error(`Failed to write json file: ${err.message}`);
106
+ process.exit(1);
107
+ }
108
+ }
109
+
110
+ if (outputFile || jsonFile) console.log('');
111
+ }
112
+
113
+ run();
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'Auth0',
4
+ category: 'auth',
5
+ depSignals: ['auth0', '@auth0/nextjs-auth0', 'express-openid-connect'],
6
+ codeSignals: [/require\(['"]auth0['"]\)/, /withApiAuthRequired/, /auth0\./],
7
+ };
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'Better Auth',
4
+ category: 'auth',
5
+ depSignals: ['better-auth'],
6
+ codeSignals: [
7
+ /better-auth/,
8
+ /auth\.api\./,
9
+ /betterAuth\(/,
10
+ /fromNodeHeaders/,
11
+ /require\(['"]better-auth['"]\)/,
12
+ /from ['"]better-auth['"]/,
13
+ ],
14
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'Clerk',
4
+ category: 'auth',
5
+ depSignals: ['@clerk/clerk-sdk-node', '@clerk/nextjs', '@clerk/express'],
6
+ codeSignals: [/clerkMiddleware/, /requireAuth\(\)/, /getAuth\(/, /@clerk\//],
7
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'express-session',
4
+ category: 'auth',
5
+ depSignals: ['express-session'],
6
+ codeSignals: [/require\(['"]express-session['"]\)/, /app\.use\(session\(/, /req\.session/],
7
+ };
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'Firebase Auth',
4
+ category: 'auth',
5
+ depSignals: ['firebase', 'firebase-admin'],
6
+ codeSignals: [
7
+ /firebase\/auth/,
8
+ /firebase-admin\/auth/,
9
+ /getAuth\(\)/,
10
+ /admin\.auth\(\)/,
11
+ /verifyIdToken\(/,
12
+ /signInWithEmailAndPassword/,
13
+ /signOut\(\)/,
14
+ ],
15
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'JWT (jsonwebtoken)',
4
+ category: 'auth',
5
+ depSignals: ['jsonwebtoken'],
6
+ codeSignals: [/jwt\.verify\(/, /jwt\.sign\(/, /Bearer /, /jsonwebtoken/],
7
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'Keycloak',
4
+ category: 'auth',
5
+ depSignals: ['keycloak-connect', 'keycloak-js', '@keycloak/keycloak-admin-client'],
6
+ codeSignals: [/keycloak\.protect\(/, /new Keycloak\(/, /keycloak-connect/],
7
+ };
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'Lucia',
4
+ category: 'auth',
5
+ depSignals: ['lucia'],
6
+ codeSignals: [
7
+ /new Lucia\(/,
8
+ /lucia\.validateSession/,
9
+ /lucia\.createSession/,
10
+ /lucia\.invalidateSession/,
11
+ /require\(['"]lucia['"]\)/,
12
+ /from ['"]lucia['"]/,
13
+ ],
14
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'NextAuth',
4
+ category: 'auth',
5
+ depSignals: ['next-auth'],
6
+ codeSignals: [/getServerSession/, /getSession\(/, /NextAuth\(/, /next-auth/],
7
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'Passport',
4
+ category: 'auth',
5
+ depSignals: ['passport'],
6
+ codeSignals: [/require\(['"]passport['"]\)/, /passport\.use\(/, /passport\.authenticate\(/],
7
+ };
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'Supabase Auth',
4
+ category: 'auth',
5
+ depSignals: ['@supabase/supabase-js', '@supabase/ssr', '@supabase/auth-helpers-nextjs'],
6
+ codeSignals: [
7
+ /supabase\.auth/,
8
+ /createServerClient/,
9
+ /createBrowserClient/,
10
+ /getUser\(\)/,
11
+ /getSession\(\)/,
12
+ /supabase\.auth\.signInWithPassword/,
13
+ /supabase\.auth\.signOut/,
14
+ ],
15
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'Express',
4
+ category: 'framework',
5
+ depSignals: ['express'],
6
+ codeSignals: [/express\(\)/, /app\.use\(/, /app\.get\(/, /app\.post\(/, /Router\(\)/],
7
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'Fastify',
4
+ category: 'framework',
5
+ depSignals: ['fastify'],
6
+ codeSignals: [/fastify\(\)/, /app\.register\(/, /fastify\.register\(/],
7
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'Hapi',
4
+ category: 'framework',
5
+ depSignals: ['@hapi/hapi', 'hapi'],
6
+ codeSignals: [/Hapi\.server\(/, /server\.route\(/, /server\.register\(/],
7
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'Koa',
4
+ category: 'framework',
5
+ depSignals: ['koa'],
6
+ codeSignals: [/new Koa\(\)/, /ctx\.body/, /ctx\.state/, /ctx\.request/],
7
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'NestJS',
4
+ category: 'framework',
5
+ depSignals: ['@nestjs/core'],
6
+ codeSignals: [/@Module\(/, /@Controller\(/, /@Injectable\(/, /NestFactory/],
7
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'Next.js',
4
+ category: 'framework',
5
+ depSignals: ['next'],
6
+ codeSignals: [/getServerSideProps/, /getStaticProps/, /NextApiRequest/, /NextApiResponse/],
7
+ };
@@ -0,0 +1,228 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * detectors/index.js — Detector Registry
5
+ *
6
+ * Loads all detector descriptors, runs them against the shared FileMap,
7
+ * and returns a unified DetectionResult.
8
+ *
9
+ * To add a new framework/library: create one file in the appropriate
10
+ * detectors/<category>/ directory and require() it here. Nothing else changes.
11
+ */
12
+
13
+ // ── Framework descriptors ─────────────────────────────────────────────────────
14
+ const FRAMEWORK_DETECTORS = [
15
+ require('./framework/nestjs'), // NestJS must be checked before Express
16
+ require('./framework/nextjs'),
17
+ require('./framework/fastify'),
18
+ require('./framework/koa'),
19
+ require('./framework/hapi'),
20
+ require('./framework/express'), // Express last — most generic signals
21
+ ];
22
+
23
+ // ── Auth descriptors ──────────────────────────────────────────────────────────
24
+ const AUTH_DETECTORS = [
25
+ require('./auth/passport'),
26
+ require('./auth/jwt'),
27
+ require('./auth/auth0'),
28
+ require('./auth/clerk'),
29
+ require('./auth/nextauth'),
30
+ require('./auth/keycloak'),
31
+ require('./auth/express-session'),
32
+ require('./auth/supabase'),
33
+ require('./auth/firebase'),
34
+ require('./auth/lucia'),
35
+ require('./auth/better-auth'),
36
+ ];
37
+
38
+ // ── Session descriptors ───────────────────────────────────────────────────────
39
+ const SESSION_DETECTORS = [
40
+ require('./session/express-session'),
41
+ require('./session/cookie-session'),
42
+ require('./session/jwt-stateless'),
43
+ ];
44
+
45
+ // ── Role descriptors ──────────────────────────────────────────────────────────
46
+ const ROLE_DETECTORS = [
47
+ require('./roles/casl'),
48
+ require('./roles/nestjs-guards'),
49
+ require('./roles/accesscontrol'),
50
+ require('./roles/custom-guards'), // must run last — has extended scan()
51
+ ];
52
+
53
+ // ── User object patterns to scan for ─────────────────────────────────────────
54
+ const USER_OBJECT_PATTERNS = [
55
+ { pattern: 'req.user', regex: /req\.user\b/ },
56
+ { pattern: 'req.session.user', regex: /req\.session\.user\b/ },
57
+ { pattern: 'req.auth', regex: /req\.auth\b/ },
58
+ { pattern: 'req.currentUser', regex: /req\.currentUser\b/ },
59
+ { pattern: 'ctx.state.user', regex: /ctx\.state\.user\b/ },
60
+ { pattern: 'ctx.user', regex: /ctx\.user\b/ },
61
+ { pattern: 'request.user', regex: /request\.user\b/ },
62
+ { pattern: 'res.locals.user', regex: /res\.locals\.user\b/ },
63
+ ];
64
+
65
+ // ── Login/logout route detection ──────────────────────────────────────────────
66
+ const LOGIN_PATTERNS = [
67
+ /router\.(post|get)\(['"]\/login['"]/,
68
+ /app\.(post|get)\(['"]\/login['"]/,
69
+ /\/api\/auth\/signin/,
70
+ /\/api\/auth\/login/,
71
+ /signInWithPassword/,
72
+ /signInWithEmailAndPassword/,
73
+ /lucia\.createSession/,
74
+ /auth\.api\.signIn/,
75
+ ];
76
+ const LOGOUT_PATTERNS = [
77
+ /router\.(post|get)\(['"]\/logout['"]/,
78
+ /app\.(post|get)\(['"]\/logout['"]/,
79
+ /\/api\/auth\/signout/,
80
+ /\/api\/auth\/logout/,
81
+ /supabase\.auth\.signOut/,
82
+ /lucia\.invalidateSession/,
83
+ /auth\.api\.signOut/,
84
+ /signOut\(\)/,
85
+ ];
86
+
87
+ // ── Helpers ───────────────────────────────────────────────────────────────────
88
+
89
+ function allDeps(packageJson) {
90
+ if (!packageJson) return new Set();
91
+ return new Set([
92
+ ...Object.keys(packageJson.dependencies || {}),
93
+ ...Object.keys(packageJson.devDependencies || {}),
94
+ ...Object.keys(packageJson.peerDependencies || {}),
95
+ ]);
96
+ }
97
+
98
+ function matchesDescriptor(descriptor, deps, allCode, files) {
99
+ const depMatch = descriptor.depSignals.some(d => deps.has(d));
100
+ const codeMatch = descriptor.codeSignals.some(re =>
101
+ typeof re === 'object' ? re.test(allCode) : allCode.includes(re)
102
+ );
103
+ return depMatch || codeMatch;
104
+ }
105
+
106
+ // ── Main export ───────────────────────────────────────────────────────────────
107
+
108
+ /**
109
+ * @param {Object|null} packageJson
110
+ * @param {Object.<string,string>} files FileMap from file-scanner
111
+ * @returns {Object} DetectionResult
112
+ */
113
+ function runAll(packageJson, files) {
114
+ const deps = allDeps(packageJson);
115
+ const allCode = Object.values(files).join('\n');
116
+
117
+ // ── Frameworks ──────────────────────────────────────────────────────────────
118
+ const frameworks = [];
119
+ for (const fd of FRAMEWORK_DETECTORS) {
120
+ const depHit = fd.depSignals.some(d => deps.has(d));
121
+ const codeHit = fd.codeSignals.some(re => re.test(allCode));
122
+ if (depHit && codeHit) frameworks.push({ name: fd.name, confidence: 'high' });
123
+ else if (depHit) frameworks.push({ name: fd.name, confidence: 'medium' });
124
+ else if (codeHit) frameworks.push({ name: fd.name, confidence: 'low' });
125
+ }
126
+ const primaryFramework = frameworks[0] || { name: 'Unknown', confidence: 'low' };
127
+
128
+ // ── Auth libraries ───────────────────────────────────────────────────────────
129
+ const authLibraries = AUTH_DETECTORS
130
+ .filter(d => matchesDescriptor(d, deps, allCode, files))
131
+ .map(d => d.name);
132
+
133
+ if (authLibraries.length === 0) {
134
+ // Fallback: custom if there's session or manual token logic
135
+ if (/req\.session/.test(allCode) || /jwt\.verify/.test(allCode)) {
136
+ authLibraries.push('Custom authentication');
137
+ }
138
+ }
139
+
140
+ // ── User object patterns ──────────────────────────────────────────────────
141
+ const userPatternsFound = new Set();
142
+ const authAffectedFiles = new Set();
143
+ let hasLocalLogin = false;
144
+ let hasLocalLogout = false;
145
+
146
+ for (const [relPath, content] of Object.entries(files)) {
147
+ let touched = false;
148
+ for (const up of USER_OBJECT_PATTERNS) {
149
+ if (up.regex.test(content)) { userPatternsFound.add(up.pattern); touched = true; }
150
+ }
151
+ if (LOGIN_PATTERNS.some(re => re.test(content))) { hasLocalLogin = true; touched = true; }
152
+ if (LOGOUT_PATTERNS.some(re => re.test(content))) { hasLocalLogout = true; touched = true; }
153
+ if (touched) authAffectedFiles.add(relPath);
154
+ }
155
+
156
+ // Primary user object = first matched (most specific wins due to pattern order)
157
+ const allUserPatterns = [...userPatternsFound];
158
+ const primaryUserObject = allUserPatterns[0] || null;
159
+
160
+ const referenceCounts = {};
161
+ for (const up of USER_OBJECT_PATTERNS) {
162
+ const globalRegex = new RegExp(up.regex, 'g');
163
+ const matches = allCode.match(globalRegex);
164
+ if (matches) {
165
+ referenceCounts[up.pattern] = matches.length;
166
+ }
167
+ }
168
+
169
+ // ── Session ───────────────────────────────────────────────────────────────
170
+ let sessionResult = { approach: 'None detected', store: 'N/A', cookieBased: false, secureCookie: null };
171
+ for (const sd of SESSION_DETECTORS) {
172
+ if (matchesDescriptor(sd, deps, allCode, files)) {
173
+ sessionResult = sd.scan(packageJson, files, deps, allCode);
174
+ break;
175
+ }
176
+ }
177
+
178
+ // ── Roles ─────────────────────────────────────────────────────────────────
179
+ const roleLibraries = ROLE_DETECTORS
180
+ .filter(d => d.name !== 'Custom Guards' && matchesDescriptor(d, deps, allCode, files))
181
+ .map(d => d.name);
182
+
183
+ const customGuardsDetector = require('./roles/custom-guards');
184
+ const { guards: customGuards, patterns: rolePatterns } = customGuardsDetector.scan(files);
185
+
186
+ const roleAffectedFiles = new Set();
187
+ for (const [relPath, content] of Object.entries(files)) {
188
+ const isRoleFile = customGuardsDetector.codeSignals.some(re => re.test(content));
189
+ if (isRoleFile) roleAffectedFiles.add(relPath);
190
+ }
191
+
192
+ for (const guard of customGuards) {
193
+ const fnName = guard.replace('()', '');
194
+ const globalRegex = new RegExp(`\\b${fnName}\\b`, 'g');
195
+ const matches = allCode.match(globalRegex);
196
+ if (matches) {
197
+ referenceCounts[fnName] = matches.length;
198
+ }
199
+ }
200
+
201
+ // ── Merge affected files ──────────────────────────────────────────────────
202
+ const allAffectedFiles = [...new Set([...authAffectedFiles, ...roleAffectedFiles])].sort();
203
+
204
+ return {
205
+ framework: primaryFramework,
206
+ allFrameworks: frameworks,
207
+ auth: {
208
+ libraries: authLibraries,
209
+ userObjectPattern: primaryUserObject,
210
+ allUserPatterns,
211
+ hasLocalLogin,
212
+ hasLocalLogout,
213
+ affectedFiles: [...authAffectedFiles].sort(),
214
+ },
215
+ session: sessionResult,
216
+ roles: {
217
+ libraries: roleLibraries,
218
+ patterns: [...new Set(rolePatterns)],
219
+ customGuards,
220
+ affectedFiles: [...roleAffectedFiles].sort(),
221
+ },
222
+ allAffectedFiles,
223
+ referenceCounts,
224
+ packageJson,
225
+ };
226
+ }
227
+
228
+ module.exports = { runAll };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'accesscontrol',
4
+ category: 'roles',
5
+ depSignals: ['accesscontrol', 'role-acl'],
6
+ codeSignals: [/AccessControl/, /\.can\(.*\)\.createOwn\(/, /\.permission\(/],
7
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'CASL',
4
+ category: 'roles',
5
+ depSignals: ['@casl/ability', '@casl/mongoose', '@casl/prisma'],
6
+ codeSignals: [/defineAbility/, /ability\.can\(/, /new AbilityBuilder\(/, /@casl\//],
7
+ };
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * custom-guards.js
5
+ *
6
+ * Detects hand-written authorization middleware functions and role patterns.
7
+ * This is the only "roles" detector that does per-file scanning rather than
8
+ * simple signal matching — because custom guards have no fixed library name.
9
+ */
10
+
11
+ const GUARD_PATTERNS = [
12
+ /function\s+requireRole\b/,
13
+ /function\s+requireAdmin\b/,
14
+ /function\s+requireAuth\b/,
15
+ /function\s+isAuthorized\b/,
16
+ /function\s+isAuthenticated\b/,
17
+ /function\s+checkRole\b/,
18
+ /function\s+hasRole\b/,
19
+ /const\s+requireRole\s*=/,
20
+ /const\s+requireAdmin\s*=/,
21
+ /const\s+protect\s*=/,
22
+ /const\s+checkPermission\s*=/,
23
+ ];
24
+
25
+ const ROLE_CHECK_PATTERNS = [
26
+ { name: 'Role string comparison', regex: /user\.role\s*===?\s*['"]/ },
27
+ { name: 'Role string comparison', regex: /req\.session\.user\.role\b/ },
28
+ { name: 'Role array check', regex: /user\.roles\.includes\(/ },
29
+ { name: 'Role array check', regex: /req\.user\.roles\b/ },
30
+ { name: 'Roles in JWT claims', regex: /decoded\.roles\b|token\.roles\b|claims\.roles\b/ },
31
+ { name: 'Permission check', regex: /user\.permissions\b|req\.user\.permissions\b/ },
32
+ ];
33
+
34
+ module.exports = {
35
+ name: 'Custom Guards',
36
+ category: 'roles',
37
+ depSignals: [],
38
+ codeSignals: [...GUARD_PATTERNS, ...ROLE_CHECK_PATTERNS.map(p => p.regex)],
39
+
40
+ // Extended scan: returns names of detected guards and role pattern names
41
+ scan(files) {
42
+ const guards = new Set();
43
+ const patterns = new Set();
44
+
45
+ for (const content of Object.values(files)) {
46
+ for (const re of GUARD_PATTERNS) {
47
+ const match = content.match(re);
48
+ if (match) {
49
+ const name = match[0]
50
+ .replace(/function\s+/, '')
51
+ .replace(/const\s+/, '')
52
+ .replace(/\s*=.*/, '')
53
+ .trim();
54
+ guards.add(name + '()');
55
+ }
56
+ }
57
+ for (const rp of ROLE_CHECK_PATTERNS) {
58
+ if (rp.regex.test(content)) patterns.add(rp.name);
59
+ }
60
+ }
61
+
62
+ return { guards: [...guards], patterns: [...patterns] };
63
+ },
64
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'NestJS Guards',
4
+ category: 'roles',
5
+ depSignals: ['@nestjs/core'],
6
+ codeSignals: [/@UseGuards\(/, /CanActivate/, /RolesGuard/, /@Roles\(/],
7
+ };
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ name: 'cookie-session',
4
+ category: 'session',
5
+ depSignals: ['cookie-session'],
6
+ codeSignals: [/require\(['"]cookie-session['"]\)/],
7
+ scan() {
8
+ return { approach: 'cookie-session', store: 'Signed cookie (stateless)', cookieBased: true, secureCookie: null };
9
+ },
10
+ };