@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 +46 -0
- package/cli.js +113 -0
- package/detectors/auth/auth0.js +7 -0
- package/detectors/auth/better-auth.js +14 -0
- package/detectors/auth/clerk.js +7 -0
- package/detectors/auth/express-session.js +7 -0
- package/detectors/auth/firebase.js +15 -0
- package/detectors/auth/jwt.js +7 -0
- package/detectors/auth/keycloak.js +7 -0
- package/detectors/auth/lucia.js +14 -0
- package/detectors/auth/nextauth.js +7 -0
- package/detectors/auth/passport.js +7 -0
- package/detectors/auth/supabase.js +15 -0
- package/detectors/framework/express.js +7 -0
- package/detectors/framework/fastify.js +7 -0
- package/detectors/framework/hapi.js +7 -0
- package/detectors/framework/koa.js +7 -0
- package/detectors/framework/nestjs.js +7 -0
- package/detectors/framework/nextjs.js +7 -0
- package/detectors/index.js +228 -0
- package/detectors/roles/accesscontrol.js +7 -0
- package/detectors/roles/casl.js +7 -0
- package/detectors/roles/custom-guards.js +64 -0
- package/detectors/roles/nestjs-guards.js +7 -0
- package/detectors/session/cookie-session.js +10 -0
- package/detectors/session/express-session.js +37 -0
- package/detectors/session/jwt-stateless.js +10 -0
- package/index.js +41 -0
- package/lib/file-scanner.js +74 -0
- package/lib/report-generator.js +542 -0
- package/package.json +16 -0
- package/report.json +84 -0
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,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,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,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,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,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,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,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
|
+
};
|