getdoorman 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/LICENSE +21 -0
- package/README.md +181 -0
- package/bin/doorman.js +444 -0
- package/package.json +74 -0
- package/src/ai-fixer.js +559 -0
- package/src/ast-scanner.js +434 -0
- package/src/auth.js +149 -0
- package/src/baseline.js +48 -0
- package/src/compliance.js +539 -0
- package/src/config.js +466 -0
- package/src/custom-rules.js +32 -0
- package/src/dashboard.js +202 -0
- package/src/detector.js +142 -0
- package/src/fix-engine.js +48 -0
- package/src/fix-registry-extra.js +95 -0
- package/src/fix-registry-go-rust.js +77 -0
- package/src/fix-registry-java-csharp.js +77 -0
- package/src/fix-registry-js.js +99 -0
- package/src/fix-registry-mcp-ai.js +57 -0
- package/src/fix-registry-python.js +87 -0
- package/src/fixer-ruby-php.js +608 -0
- package/src/fixer.js +2113 -0
- package/src/hooks.js +115 -0
- package/src/ignore.js +176 -0
- package/src/index.js +384 -0
- package/src/metrics.js +126 -0
- package/src/monorepo.js +65 -0
- package/src/presets.js +54 -0
- package/src/reporter.js +975 -0
- package/src/rule-worker.js +36 -0
- package/src/rules/ast-rules.js +756 -0
- package/src/rules/bugs/accessibility.js +235 -0
- package/src/rules/bugs/ai-codegen-fixable.js +172 -0
- package/src/rules/bugs/ai-codegen.js +365 -0
- package/src/rules/bugs/code-smell-bugs.js +247 -0
- package/src/rules/bugs/crypto-bugs.js +195 -0
- package/src/rules/bugs/docker-bugs.js +158 -0
- package/src/rules/bugs/general.js +361 -0
- package/src/rules/bugs/go-bugs.js +279 -0
- package/src/rules/bugs/index.js +73 -0
- package/src/rules/bugs/js-api.js +257 -0
- package/src/rules/bugs/js-array-object.js +210 -0
- package/src/rules/bugs/js-async-fixable.js +223 -0
- package/src/rules/bugs/js-async.js +211 -0
- package/src/rules/bugs/js-closure-scope.js +182 -0
- package/src/rules/bugs/js-database.js +203 -0
- package/src/rules/bugs/js-error-handling.js +148 -0
- package/src/rules/bugs/js-logic.js +261 -0
- package/src/rules/bugs/js-memory.js +214 -0
- package/src/rules/bugs/js-node.js +361 -0
- package/src/rules/bugs/js-react.js +373 -0
- package/src/rules/bugs/js-regex.js +200 -0
- package/src/rules/bugs/js-state.js +272 -0
- package/src/rules/bugs/js-type-coercion.js +318 -0
- package/src/rules/bugs/nextjs-bugs.js +242 -0
- package/src/rules/bugs/nextjs-fixable.js +120 -0
- package/src/rules/bugs/node-fixable.js +178 -0
- package/src/rules/bugs/python-advanced.js +245 -0
- package/src/rules/bugs/python-fixable.js +98 -0
- package/src/rules/bugs/python.js +284 -0
- package/src/rules/bugs/react-fixable.js +207 -0
- package/src/rules/bugs/ruby-bugs.js +182 -0
- package/src/rules/bugs/shell-bugs.js +181 -0
- package/src/rules/bugs/silent-failures.js +261 -0
- package/src/rules/bugs/ts-bugs.js +235 -0
- package/src/rules/bugs/unused-vars.js +65 -0
- package/src/rules/compliance/accessibility-ext.js +468 -0
- package/src/rules/compliance/education.js +322 -0
- package/src/rules/compliance/financial.js +421 -0
- package/src/rules/compliance/frameworks.js +507 -0
- package/src/rules/compliance/healthcare.js +520 -0
- package/src/rules/compliance/index.js +2714 -0
- package/src/rules/compliance/regional-eu.js +480 -0
- package/src/rules/compliance/regional-international.js +903 -0
- package/src/rules/cost/index.js +1993 -0
- package/src/rules/data/index.js +2503 -0
- package/src/rules/dependencies/index.js +1684 -0
- package/src/rules/deployment/index.js +2050 -0
- package/src/rules/index.js +71 -0
- package/src/rules/infrastructure/index.js +3048 -0
- package/src/rules/performance/index.js +3455 -0
- package/src/rules/quality/index.js +3175 -0
- package/src/rules/reliability/index.js +3040 -0
- package/src/rules/scope-rules.js +815 -0
- package/src/rules/security/ai-api.js +1177 -0
- package/src/rules/security/auth.js +1328 -0
- package/src/rules/security/cors.js +127 -0
- package/src/rules/security/crypto.js +527 -0
- package/src/rules/security/csharp.js +862 -0
- package/src/rules/security/csrf.js +193 -0
- package/src/rules/security/dart.js +835 -0
- package/src/rules/security/deserialization.js +291 -0
- package/src/rules/security/file-upload.js +187 -0
- package/src/rules/security/go.js +850 -0
- package/src/rules/security/headers.js +235 -0
- package/src/rules/security/index.js +65 -0
- package/src/rules/security/injection.js +1639 -0
- package/src/rules/security/mcp-server.js +71 -0
- package/src/rules/security/misconfiguration.js +660 -0
- package/src/rules/security/oauth-jwt.js +329 -0
- package/src/rules/security/path-traversal.js +295 -0
- package/src/rules/security/php.js +1054 -0
- package/src/rules/security/prototype-pollution.js +283 -0
- package/src/rules/security/rate-limiting.js +208 -0
- package/src/rules/security/ruby.js +1061 -0
- package/src/rules/security/rust.js +693 -0
- package/src/rules/security/secrets.js +747 -0
- package/src/rules/security/shell.js +647 -0
- package/src/rules/security/ssrf.js +298 -0
- package/src/rules/security/supply-chain-advanced.js +393 -0
- package/src/rules/security/supply-chain.js +734 -0
- package/src/rules/security/swift.js +835 -0
- package/src/rules/security/taint.js +27 -0
- package/src/rules/security/xss.js +520 -0
- package/src/scan-cache.js +71 -0
- package/src/scanner.js +710 -0
- package/src/scope-analyzer.js +685 -0
- package/src/share.js +88 -0
- package/src/taint.js +300 -0
- package/src/telemetry.js +183 -0
- package/src/tracer.js +190 -0
- package/src/upload.js +35 -0
- package/src/worker.js +31 -0
package/src/tracer.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attack path tracer — lightweight data-flow analysis to determine whether
|
|
3
|
+
* vulnerability findings are reachable from public-facing routes.
|
|
4
|
+
* Heuristic-based (regex, not AST). Supports Express, Koa, and Next.js patterns.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const ROUTE_PATTERN = /(?:app|router)\.(get|post|put|delete|patch)\(\s*['"`]([^'"`]+)['"`]/g;
|
|
8
|
+
const NEXTJS_HANDLER = /export\s+(?:default\s+)?(?:async\s+)?function\s+(handler|GET|POST|PUT|DELETE|PATCH)\b/g;
|
|
9
|
+
|
|
10
|
+
/** Scan every file for Express / Koa / Next.js route definitions. */
|
|
11
|
+
function findRoutes(files) {
|
|
12
|
+
const routes = new Map();
|
|
13
|
+
for (const [filePath, content] of files.entries()) {
|
|
14
|
+
const hits = [];
|
|
15
|
+
let m;
|
|
16
|
+
ROUTE_PATTERN.lastIndex = 0;
|
|
17
|
+
while ((m = ROUTE_PATTERN.exec(content)) !== null) {
|
|
18
|
+
const line = content.slice(0, m.index).split('\n').length;
|
|
19
|
+
hits.push({ method: m[1].toUpperCase(), path: m[2], line });
|
|
20
|
+
}
|
|
21
|
+
NEXTJS_HANDLER.lastIndex = 0;
|
|
22
|
+
while ((m = NEXTJS_HANDLER.exec(content)) !== null) {
|
|
23
|
+
const line = content.slice(0, m.index).split('\n').length;
|
|
24
|
+
const method = m[1] === 'handler' ? 'ALL' : m[1].toUpperCase();
|
|
25
|
+
const urlPath = deriveNextRoute(filePath);
|
|
26
|
+
hits.push({ method, path: urlPath, line });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Files under pages/api or app/api are route handlers even without explicit exports
|
|
30
|
+
if (hits.length === 0 && /(?:pages|app)\/api\//.test(filePath)) {
|
|
31
|
+
hits.push({ method: 'ALL', path: deriveNextRoute(filePath), line: 1 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (hits.length > 0) routes.set(filePath, hits);
|
|
35
|
+
}
|
|
36
|
+
return routes;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function deriveNextRoute(filePath) {
|
|
40
|
+
const match = filePath.match(/(?:pages|app)(\/api\/.+?)(?:\/route)?\.\w+$/);
|
|
41
|
+
return match ? match[1].replace(/\/index$/, '') : '/api/unknown';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ESM_IMPORT = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
|
|
45
|
+
const CJS_REQUIRE = /require\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
46
|
+
|
|
47
|
+
/** Build a mapping of file -> list of files it imports (resolved to keys in `files`). */
|
|
48
|
+
function buildImportGraph(files) {
|
|
49
|
+
const graph = new Map();
|
|
50
|
+
const fileKeys = [...files.keys()];
|
|
51
|
+
for (const [filePath, content] of files.entries()) {
|
|
52
|
+
const imports = new Set();
|
|
53
|
+
for (const regex of [ESM_IMPORT, CJS_REQUIRE]) {
|
|
54
|
+
regex.lastIndex = 0;
|
|
55
|
+
let m;
|
|
56
|
+
while ((m = regex.exec(content)) !== null) {
|
|
57
|
+
const specifier = m[1];
|
|
58
|
+
if (!specifier.startsWith('.')) continue;
|
|
59
|
+
const resolved = resolveSpecifier(filePath, specifier, fileKeys);
|
|
60
|
+
if (resolved) imports.add(resolved);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (imports.size > 0) graph.set(filePath, [...imports]);
|
|
65
|
+
}
|
|
66
|
+
return graph;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Resolve a relative import specifier against the importer's directory. */
|
|
70
|
+
function resolveSpecifier(importer, specifier, fileKeys) {
|
|
71
|
+
const dir = importer.includes('/') ? importer.slice(0, importer.lastIndexOf('/')) : '';
|
|
72
|
+
const base = normalizePath(`${dir}/${specifier}`);
|
|
73
|
+
const candidates = [
|
|
74
|
+
base,
|
|
75
|
+
`${base}.js`,
|
|
76
|
+
`${base}.ts`,
|
|
77
|
+
`${base}.mjs`,
|
|
78
|
+
`${base}.jsx`,
|
|
79
|
+
`${base}.tsx`,
|
|
80
|
+
`${base}/index.js`,
|
|
81
|
+
`${base}/index.ts`,
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
for (const c of candidates) {
|
|
85
|
+
if (fileKeys.includes(c)) return c;
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizePath(p) {
|
|
91
|
+
const parts = [];
|
|
92
|
+
for (const seg of p.split('/')) {
|
|
93
|
+
if (seg === '.' || seg === '') continue;
|
|
94
|
+
if (seg === '..') { parts.pop(); continue; }
|
|
95
|
+
parts.push(seg);
|
|
96
|
+
}
|
|
97
|
+
return parts.join('/');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** BFS from route files through imports (max depth 3) to find reachable files. */
|
|
101
|
+
function computeReachability(routes, importGraph, maxDepth = 3) {
|
|
102
|
+
const reachable = new Map();
|
|
103
|
+
for (const [routeFile, routeList] of routes.entries()) {
|
|
104
|
+
const queue = [{ file: routeFile, depth: 0 }];
|
|
105
|
+
const visited = new Set();
|
|
106
|
+
|
|
107
|
+
while (queue.length > 0) {
|
|
108
|
+
const { file, depth } = queue.shift();
|
|
109
|
+
if (visited.has(file)) continue;
|
|
110
|
+
visited.add(file);
|
|
111
|
+
|
|
112
|
+
const existing = reachable.get(file);
|
|
113
|
+
if (!existing || existing.depth > depth) {
|
|
114
|
+
reachable.set(file, { depth, routeFile, routes: routeList });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (depth < maxDepth) {
|
|
118
|
+
const deps = importGraph.get(file) || [];
|
|
119
|
+
for (const dep of deps) {
|
|
120
|
+
if (!visited.has(dep)) {
|
|
121
|
+
queue.push({ file: dep, depth: depth + 1 });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return reachable;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const SEVERITY_ORDER = ['info', 'low', 'medium', 'high', 'critical'];
|
|
132
|
+
|
|
133
|
+
function bumpSeverity(severity) {
|
|
134
|
+
const idx = SEVERITY_ORDER.indexOf(severity);
|
|
135
|
+
if (idx >= 0 && idx < SEVERITY_ORDER.length - 1) {
|
|
136
|
+
return SEVERITY_ORDER[idx + 1];
|
|
137
|
+
}
|
|
138
|
+
return severity;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function formatRouteLabel(route, routeFile) {
|
|
142
|
+
return `${route.method} ${route.path} (${routeFile}:${route.line})`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Trace attack paths and enrich findings with reachability data. */
|
|
146
|
+
export function traceAttackPaths(findings, files, stack) {
|
|
147
|
+
if (!findings || findings.length === 0) return findings;
|
|
148
|
+
|
|
149
|
+
const routes = findRoutes(files);
|
|
150
|
+
const importGraph = buildImportGraph(files);
|
|
151
|
+
const reachable = computeReachability(routes, importGraph);
|
|
152
|
+
|
|
153
|
+
return findings.map((finding) => {
|
|
154
|
+
const file = finding.file || finding.filePath || null;
|
|
155
|
+
if (!file) {
|
|
156
|
+
return {
|
|
157
|
+
...finding,
|
|
158
|
+
attackPath: { reachable: false, chain: [], exposure: 'unknown' },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const info = reachable.get(file);
|
|
163
|
+
if (!info) {
|
|
164
|
+
return {
|
|
165
|
+
...finding,
|
|
166
|
+
attackPath: { reachable: false, chain: [], exposure: 'internal' },
|
|
167
|
+
note: (finding.note ? finding.note + ' | ' : '') + 'Internal only \u2014 lower risk',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Build the chain description
|
|
172
|
+
const chain = [];
|
|
173
|
+
const routeLabel = formatRouteLabel(info.routes[0], info.routeFile);
|
|
174
|
+
chain.push(routeLabel);
|
|
175
|
+
|
|
176
|
+
if (info.depth > 0) {
|
|
177
|
+
const loc = finding.line ? `${file}:${finding.line}` : file;
|
|
178
|
+
chain.push(loc);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const exposure = info.depth === 0 ? 'direct' : 'indirect';
|
|
182
|
+
const adjustedSeverity = bumpSeverity(finding.severity || 'medium');
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
...finding,
|
|
186
|
+
severity: adjustedSeverity,
|
|
187
|
+
attackPath: { reachable: true, chain, exposure },
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
}
|
package/src/upload.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload scan results to the Doorman dashboard API.
|
|
3
|
+
*
|
|
4
|
+
* This runs after every `doorman check` invocation. If no API key is
|
|
5
|
+
* configured the call is silently skipped so it never blocks the CLI.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export async function uploadScan(targetPath, scanResult, options = {}) {
|
|
9
|
+
const apiKey = process.env.DOORMAN_API_KEY || options.apiKey;
|
|
10
|
+
const endpoint = (
|
|
11
|
+
process.env.DOORMAN_API_URL || options.apiUrl || 'https://api.doorman.dev'
|
|
12
|
+
).replace(/\/+$/, '');
|
|
13
|
+
|
|
14
|
+
if (!apiKey) return null; // Silent skip — no key configured
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch(`${endpoint}/api/scans`, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: {
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
'x-api-key': apiKey,
|
|
22
|
+
},
|
|
23
|
+
body: JSON.stringify({
|
|
24
|
+
score: scanResult.score,
|
|
25
|
+
findings: scanResult.findings,
|
|
26
|
+
stack: scanResult.stack,
|
|
27
|
+
fileCount: scanResult.fileCount || 0,
|
|
28
|
+
}),
|
|
29
|
+
signal: AbortSignal.timeout(5000),
|
|
30
|
+
});
|
|
31
|
+
return res.ok;
|
|
32
|
+
} catch {
|
|
33
|
+
return false; // Never block the CLI
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/worker.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { parentPort, workerData } from 'worker_threads';
|
|
2
|
+
|
|
3
|
+
const { ruleModulePath, ruleIds, filesEntries, stack } = workerData;
|
|
4
|
+
|
|
5
|
+
async function run() {
|
|
6
|
+
try {
|
|
7
|
+
const mod = await import(ruleModulePath);
|
|
8
|
+
const allRules = mod.default;
|
|
9
|
+
const assignedRules = allRules.filter(r => ruleIds.includes(r.id));
|
|
10
|
+
|
|
11
|
+
const files = new Map(filesEntries);
|
|
12
|
+
const context = { files, stack };
|
|
13
|
+
const findings = [];
|
|
14
|
+
|
|
15
|
+
for (const rule of assignedRules) {
|
|
16
|
+
try {
|
|
17
|
+
const result = await rule.check(context);
|
|
18
|
+
if (Array.isArray(result)) findings.push(...result);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
// Log failed rules for debugging, but continue scanning
|
|
21
|
+
console.warn(`[worker] Rule "${rule.id || rule.name || 'unknown'}" failed: ${e.message}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
parentPort.postMessage({ findings });
|
|
26
|
+
} catch (e) {
|
|
27
|
+
parentPort.postMessage({ findings: [], error: e.message });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
run();
|