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
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree-sitter based AST analysis engine.
|
|
3
|
+
*
|
|
4
|
+
* Provides precise, structure-aware code analysis that avoids the false
|
|
5
|
+
* positives and false negatives inherent in regex-based scanning.
|
|
6
|
+
*
|
|
7
|
+
* Gracefully falls back when tree-sitter native modules are not installed —
|
|
8
|
+
* the code becomes fully functional once `npm install` is run.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let Parser;
|
|
12
|
+
let treeSitterAvailable = false;
|
|
13
|
+
|
|
14
|
+
const LANGUAGE_MODULES = {};
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// eslint-disable-next-line
|
|
18
|
+
const TreeSitter = await import('tree-sitter');
|
|
19
|
+
Parser = TreeSitter.default || TreeSitter;
|
|
20
|
+
treeSitterAvailable = true;
|
|
21
|
+
} catch {
|
|
22
|
+
// tree-sitter is not installed — AST analysis will be unavailable
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Lazily load a tree-sitter language grammar.
|
|
27
|
+
*/
|
|
28
|
+
async function loadLanguage(lang) {
|
|
29
|
+
if (LANGUAGE_MODULES[lang]) return LANGUAGE_MODULES[lang];
|
|
30
|
+
|
|
31
|
+
const moduleMap = {
|
|
32
|
+
javascript: 'tree-sitter-javascript',
|
|
33
|
+
python: 'tree-sitter-python',
|
|
34
|
+
go: 'tree-sitter-go',
|
|
35
|
+
ruby: 'tree-sitter-ruby',
|
|
36
|
+
php: 'tree-sitter-php',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const modName = moduleMap[lang];
|
|
40
|
+
if (!modName) return null;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const mod = await import(modName);
|
|
44
|
+
LANGUAGE_MODULES[lang] = mod.default || mod;
|
|
45
|
+
return LANGUAGE_MODULES[lang];
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Map file extension to language name.
|
|
53
|
+
*/
|
|
54
|
+
export function detectLanguage(filePath) {
|
|
55
|
+
const ext = filePath.slice(filePath.lastIndexOf('.'));
|
|
56
|
+
const map = {
|
|
57
|
+
'.js': 'javascript',
|
|
58
|
+
'.jsx': 'javascript',
|
|
59
|
+
'.ts': 'javascript', // tree-sitter-javascript handles TS basics
|
|
60
|
+
'.tsx': 'javascript',
|
|
61
|
+
'.mjs': 'javascript',
|
|
62
|
+
'.cjs': 'javascript',
|
|
63
|
+
'.py': 'python',
|
|
64
|
+
'.go': 'go',
|
|
65
|
+
'.rb': 'ruby',
|
|
66
|
+
'.php': 'php',
|
|
67
|
+
};
|
|
68
|
+
return map[ext] || null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check whether the tree-sitter AST engine is available.
|
|
73
|
+
*/
|
|
74
|
+
export function isASTAvailable() {
|
|
75
|
+
return treeSitterAvailable;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parse source code into a tree-sitter AST.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} content - Source code to parse
|
|
82
|
+
* @param {string} language - Language name (javascript, python, go, ruby, php)
|
|
83
|
+
* @returns {ASTContext|null} An ASTContext wrapping the AST, or null on failure.
|
|
84
|
+
*/
|
|
85
|
+
export async function parseFile(content, language) {
|
|
86
|
+
if (!treeSitterAvailable) return null;
|
|
87
|
+
|
|
88
|
+
const lang = await loadLanguage(language);
|
|
89
|
+
if (!lang) return null;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const parser = new Parser();
|
|
93
|
+
parser.setLanguage(lang);
|
|
94
|
+
const tree = parser.parse(content);
|
|
95
|
+
return new ASTContext(tree, content, language);
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Helpers: walk the tree
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
function* walk(node) {
|
|
106
|
+
yield node;
|
|
107
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
108
|
+
yield* walk(node.child(i));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// ASTContext — query API for AST rules
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
export class ASTContext {
|
|
117
|
+
/**
|
|
118
|
+
* @param {object} tree - tree-sitter Tree object
|
|
119
|
+
* @param {string} source - original source text
|
|
120
|
+
* @param {string} language - language name
|
|
121
|
+
*/
|
|
122
|
+
constructor(tree, source, language) {
|
|
123
|
+
this.tree = tree;
|
|
124
|
+
this.source = source;
|
|
125
|
+
this.language = language;
|
|
126
|
+
this._root = tree.rootNode;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// -----------------------------------------------------------------------
|
|
130
|
+
// findNodes(type) — return every node whose type matches `type`.
|
|
131
|
+
// -----------------------------------------------------------------------
|
|
132
|
+
findNodes(type) {
|
|
133
|
+
const results = [];
|
|
134
|
+
for (const node of walk(this._root)) {
|
|
135
|
+
if (node.type === type) {
|
|
136
|
+
results.push(node);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return results;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// -----------------------------------------------------------------------
|
|
143
|
+
// findCalls(functionName) — find call_expression nodes for a function.
|
|
144
|
+
//
|
|
145
|
+
// Handles:
|
|
146
|
+
// foo() — identifier calls
|
|
147
|
+
// obj.method() — member expression calls
|
|
148
|
+
// a.b.method() — chained member expression calls
|
|
149
|
+
// -----------------------------------------------------------------------
|
|
150
|
+
findCalls(functionName) {
|
|
151
|
+
const callNodes = this.findNodes('call_expression');
|
|
152
|
+
const results = [];
|
|
153
|
+
|
|
154
|
+
for (const node of callNodes) {
|
|
155
|
+
const callee = node.childForFieldName('function') || node.child(0);
|
|
156
|
+
if (!callee) continue;
|
|
157
|
+
|
|
158
|
+
const calleeText = callee.text;
|
|
159
|
+
|
|
160
|
+
// Exact match: foo() or obj.method()
|
|
161
|
+
if (calleeText === functionName) {
|
|
162
|
+
results.push(node);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Match just the method name for member expressions: *.method()
|
|
167
|
+
if (callee.type === 'member_expression') {
|
|
168
|
+
const property = callee.childForFieldName('property') || callee.child(callee.childCount - 1);
|
|
169
|
+
if (property && property.text === functionName) {
|
|
170
|
+
results.push(node);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Match dot-separated name: e.g. "db.query"
|
|
176
|
+
if (functionName.includes('.') && calleeText === functionName) {
|
|
177
|
+
results.push(node);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return results;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// -----------------------------------------------------------------------
|
|
185
|
+
// findAssignments(variableName) — find assignments to a variable.
|
|
186
|
+
//
|
|
187
|
+
// Handles variable_declarator and assignment_expression.
|
|
188
|
+
// -----------------------------------------------------------------------
|
|
189
|
+
findAssignments(variableName) {
|
|
190
|
+
const results = [];
|
|
191
|
+
|
|
192
|
+
// variable_declarator: const x = ...
|
|
193
|
+
for (const node of this.findNodes('variable_declarator')) {
|
|
194
|
+
const name = node.childForFieldName('name') || node.child(0);
|
|
195
|
+
if (name && name.text === variableName) {
|
|
196
|
+
results.push(node);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// assignment_expression: x = ...
|
|
201
|
+
for (const node of this.findNodes('assignment_expression')) {
|
|
202
|
+
const left = node.childForFieldName('left') || node.child(0);
|
|
203
|
+
if (left && left.text === variableName) {
|
|
204
|
+
results.push(node);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return results;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// -----------------------------------------------------------------------
|
|
212
|
+
// getParentFunction(node) — walk up to the enclosing function node.
|
|
213
|
+
// -----------------------------------------------------------------------
|
|
214
|
+
getParentFunction(node) {
|
|
215
|
+
const functionTypes = new Set([
|
|
216
|
+
'function_declaration',
|
|
217
|
+
'function_expression',
|
|
218
|
+
'arrow_function',
|
|
219
|
+
'method_definition',
|
|
220
|
+
'function_definition', // python
|
|
221
|
+
'func_declaration', // go
|
|
222
|
+
]);
|
|
223
|
+
|
|
224
|
+
let current = node.parent;
|
|
225
|
+
while (current) {
|
|
226
|
+
if (functionTypes.has(current.type)) return current;
|
|
227
|
+
current = current.parent;
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// -----------------------------------------------------------------------
|
|
233
|
+
// getScope(node) — return the innermost scope context.
|
|
234
|
+
// -----------------------------------------------------------------------
|
|
235
|
+
getScope(node) {
|
|
236
|
+
const scopeTypes = new Set([
|
|
237
|
+
'function_declaration',
|
|
238
|
+
'function_expression',
|
|
239
|
+
'arrow_function',
|
|
240
|
+
'method_definition',
|
|
241
|
+
'class_declaration',
|
|
242
|
+
'class_expression',
|
|
243
|
+
'program',
|
|
244
|
+
'module',
|
|
245
|
+
'function_definition',
|
|
246
|
+
'class_definition',
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
let current = node.parent;
|
|
250
|
+
while (current) {
|
|
251
|
+
if (scopeTypes.has(current.type)) {
|
|
252
|
+
return { type: current.type, node: current };
|
|
253
|
+
}
|
|
254
|
+
current = current.parent;
|
|
255
|
+
}
|
|
256
|
+
return { type: 'module', node: this._root };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// -----------------------------------------------------------------------
|
|
260
|
+
// isInsideTryCatch(node) — check whether node is inside a try body.
|
|
261
|
+
// -----------------------------------------------------------------------
|
|
262
|
+
isInsideTryCatch(node) {
|
|
263
|
+
let current = node.parent;
|
|
264
|
+
while (current) {
|
|
265
|
+
if (current.type === 'try_statement' || current.type === 'try') {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
current = current.parent;
|
|
269
|
+
}
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// -----------------------------------------------------------------------
|
|
274
|
+
// getArguments(callNode) — return the argument nodes of a call.
|
|
275
|
+
// -----------------------------------------------------------------------
|
|
276
|
+
getArguments(callNode) {
|
|
277
|
+
const args = callNode.childForFieldName('arguments');
|
|
278
|
+
if (!args) {
|
|
279
|
+
// fallback: look for an arguments node among children
|
|
280
|
+
for (let i = 0; i < callNode.childCount; i++) {
|
|
281
|
+
const child = callNode.child(i);
|
|
282
|
+
if (child.type === 'arguments' || child.type === 'argument_list') {
|
|
283
|
+
return this._extractArgChildren(child);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
return this._extractArgChildren(args);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
_extractArgChildren(argsNode) {
|
|
292
|
+
const result = [];
|
|
293
|
+
for (let i = 0; i < argsNode.childCount; i++) {
|
|
294
|
+
const child = argsNode.child(i);
|
|
295
|
+
// Skip punctuation (, and )
|
|
296
|
+
if (child.type !== ',' && child.type !== '(' && child.type !== ')') {
|
|
297
|
+
result.push(child);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// -----------------------------------------------------------------------
|
|
304
|
+
// isStringLiteral(node) — check if a node is a string literal.
|
|
305
|
+
// -----------------------------------------------------------------------
|
|
306
|
+
isStringLiteral(node) {
|
|
307
|
+
const stringTypes = new Set([
|
|
308
|
+
'string',
|
|
309
|
+
'string_literal',
|
|
310
|
+
'template_string',
|
|
311
|
+
'interpreted_string_literal',
|
|
312
|
+
'raw_string_literal',
|
|
313
|
+
]);
|
|
314
|
+
return stringTypes.has(node.type);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// -----------------------------------------------------------------------
|
|
318
|
+
// isUserInput(node) — check if node references common user-input sources.
|
|
319
|
+
//
|
|
320
|
+
// Recognises: req.query, req.body, req.params, req.headers,
|
|
321
|
+
// request.GET, request.POST, request.args, request.form,
|
|
322
|
+
// process.env, params[:...], etc.
|
|
323
|
+
// -----------------------------------------------------------------------
|
|
324
|
+
isUserInput(node) {
|
|
325
|
+
const text = node.text;
|
|
326
|
+
const patterns = [
|
|
327
|
+
/\breq\.(query|body|params|headers|cookies)\b/,
|
|
328
|
+
/\brequest\.(query|body|params|headers|cookies)\b/,
|
|
329
|
+
/\brequest\.(GET|POST|args|form|data|json)\b/,
|
|
330
|
+
/\bparams\[/,
|
|
331
|
+
/\bctx\.(request|query|params)\b/,
|
|
332
|
+
/\bc\.Query\b/, // Go gin
|
|
333
|
+
/\bc\.Param\b/, // Go gin
|
|
334
|
+
/\br\.URL\.Query\b/, // Go net/http
|
|
335
|
+
/\br\.FormValue\b/, // Go net/http
|
|
336
|
+
];
|
|
337
|
+
return patterns.some((p) => p.test(text));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// -----------------------------------------------------------------------
|
|
341
|
+
// nodeText(node) — safe accessor for a node's source text.
|
|
342
|
+
// -----------------------------------------------------------------------
|
|
343
|
+
nodeText(node) {
|
|
344
|
+
return node ? node.text : '';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// -----------------------------------------------------------------------
|
|
348
|
+
// nodeLocation(node) — return { line, column } (1-based line).
|
|
349
|
+
// -----------------------------------------------------------------------
|
|
350
|
+
nodeLocation(node) {
|
|
351
|
+
return {
|
|
352
|
+
line: node.startPosition.row + 1,
|
|
353
|
+
column: node.startPosition.column,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// -----------------------------------------------------------------------
|
|
358
|
+
// containsConcatenation(node) — check if a node contains string
|
|
359
|
+
// concatenation with non-literal operands.
|
|
360
|
+
// -----------------------------------------------------------------------
|
|
361
|
+
containsConcatenation(node) {
|
|
362
|
+
if (node.type === 'binary_expression') {
|
|
363
|
+
const op = node.childForFieldName('operator');
|
|
364
|
+
if (op && op.text === '+') {
|
|
365
|
+
const left = node.childForFieldName('left');
|
|
366
|
+
const right = node.childForFieldName('right');
|
|
367
|
+
// At least one side is not a string literal
|
|
368
|
+
if (
|
|
369
|
+
(left && !this.isStringLiteral(left)) ||
|
|
370
|
+
(right && !this.isStringLiteral(right))
|
|
371
|
+
) {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Check template literals with expressions
|
|
378
|
+
if (node.type === 'template_string') {
|
|
379
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
380
|
+
const child = node.child(i);
|
|
381
|
+
if (child.type === 'template_substitution') {
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Recurse into children
|
|
388
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
389
|
+
if (this.containsConcatenation(node.child(i))) return true;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// -----------------------------------------------------------------------
|
|
396
|
+
// containsUserInput(node) — recursively check if any descendant is
|
|
397
|
+
// user input.
|
|
398
|
+
// -----------------------------------------------------------------------
|
|
399
|
+
containsUserInput(node) {
|
|
400
|
+
if (this.isUserInput(node)) return true;
|
|
401
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
402
|
+
if (this.containsUserInput(node.child(i))) return true;
|
|
403
|
+
}
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// -----------------------------------------------------------------------
|
|
408
|
+
// isNonLiteral(node) — check if a node is NOT a simple literal value.
|
|
409
|
+
// -----------------------------------------------------------------------
|
|
410
|
+
isNonLiteral(node) {
|
|
411
|
+
const literalTypes = new Set([
|
|
412
|
+
'string',
|
|
413
|
+
'string_literal',
|
|
414
|
+
'number',
|
|
415
|
+
'integer',
|
|
416
|
+
'float',
|
|
417
|
+
'true',
|
|
418
|
+
'false',
|
|
419
|
+
'null',
|
|
420
|
+
'undefined',
|
|
421
|
+
'template_string',
|
|
422
|
+
]);
|
|
423
|
+
|
|
424
|
+
// A template_string with substitutions is non-literal
|
|
425
|
+
if (node.type === 'template_string') {
|
|
426
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
427
|
+
if (node.child(i).type === 'template_substitution') return true;
|
|
428
|
+
}
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return !literalTypes.has(node.type);
|
|
433
|
+
}
|
|
434
|
+
}
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
|
|
5
|
+
const AUTH_DIR = join(homedir(), '.doorman');
|
|
6
|
+
const AUTH_FILE = join(AUTH_DIR, 'auth.json');
|
|
7
|
+
|
|
8
|
+
// Plans
|
|
9
|
+
const PLANS = {
|
|
10
|
+
free: {
|
|
11
|
+
name: 'Free', price: '$0', maxScans: 5,
|
|
12
|
+
categories: ['security', 'bugs'],
|
|
13
|
+
},
|
|
14
|
+
pro: {
|
|
15
|
+
name: 'Pro', price: '$20/mo', maxScans: Infinity,
|
|
16
|
+
categories: ['security', 'bugs', 'performance', 'reliability', 'cost', 'data', 'quality'],
|
|
17
|
+
dashboard: true, autoRun: true, cicd: true,
|
|
18
|
+
},
|
|
19
|
+
enterprise: {
|
|
20
|
+
name: 'Enterprise', price: '$100/mo', maxScans: Infinity,
|
|
21
|
+
categories: 'all',
|
|
22
|
+
dashboard: true, autoRun: true, cicd: true,
|
|
23
|
+
compliance: true, teamDashboard: true, prComments: true, slack: true, customRules: true,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const FREE_MONTHLY_SCANS = 5;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load saved account from ~/.doorman/auth.json
|
|
31
|
+
*/
|
|
32
|
+
export function loadAuth() {
|
|
33
|
+
if (!existsSync(AUTH_FILE)) return null;
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(readFileSync(AUTH_FILE, 'utf-8'));
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Save account locally.
|
|
43
|
+
*/
|
|
44
|
+
function saveAuthData(data) {
|
|
45
|
+
if (!existsSync(AUTH_DIR)) mkdirSync(AUTH_DIR, { recursive: true });
|
|
46
|
+
writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2) + '\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Save account by email.
|
|
51
|
+
*/
|
|
52
|
+
export function saveAuth(email) {
|
|
53
|
+
const existing = loadAuth();
|
|
54
|
+
const data = {
|
|
55
|
+
email,
|
|
56
|
+
plan: existing?.plan || 'free',
|
|
57
|
+
scansThisMonth: existing?.scansThisMonth || 0,
|
|
58
|
+
monthKey: getCurrentMonthKey(),
|
|
59
|
+
savedAt: new Date().toISOString(),
|
|
60
|
+
};
|
|
61
|
+
saveAuthData(data);
|
|
62
|
+
return data;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get current month key (YYYY-MM) for tracking monthly scan count.
|
|
67
|
+
*/
|
|
68
|
+
function getCurrentMonthKey() {
|
|
69
|
+
const now = new Date();
|
|
70
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Record a scan and check if the user has scans remaining.
|
|
75
|
+
* Returns { allowed: boolean, remaining: number, plan: string }
|
|
76
|
+
*/
|
|
77
|
+
export function recordScanUsage() {
|
|
78
|
+
const bypass = process.env.DOORMAN_BYPASS === '1';
|
|
79
|
+
if (bypass) return { allowed: true, remaining: Infinity, plan: 'bypass' };
|
|
80
|
+
|
|
81
|
+
let auth = loadAuth();
|
|
82
|
+
|
|
83
|
+
// No account yet — create one
|
|
84
|
+
if (!auth) {
|
|
85
|
+
auth = { plan: 'free', scansThisMonth: 0, monthKey: getCurrentMonthKey(), savedAt: new Date().toISOString() };
|
|
86
|
+
saveAuthData(auth);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const plan = PLANS[auth.plan] || PLANS.free;
|
|
90
|
+
|
|
91
|
+
// Reset count if new month
|
|
92
|
+
if (auth.monthKey !== getCurrentMonthKey()) {
|
|
93
|
+
auth.scansThisMonth = 0;
|
|
94
|
+
auth.monthKey = getCurrentMonthKey();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const maxScans = plan.maxScans ?? FREE_MONTHLY_SCANS;
|
|
98
|
+
const remaining = maxScans - auth.scansThisMonth;
|
|
99
|
+
|
|
100
|
+
if (remaining <= 0 && maxScans !== Infinity) {
|
|
101
|
+
return { allowed: false, remaining: 0, plan: plan.name };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Record this scan
|
|
105
|
+
auth.scansThisMonth = (auth.scansThisMonth || 0) + 1;
|
|
106
|
+
saveAuthData(auth);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
allowed: true,
|
|
110
|
+
remaining: maxScans === Infinity ? Infinity : maxScans - auth.scansThisMonth,
|
|
111
|
+
plan: plan.name,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get the user's plan and which categories they can see.
|
|
117
|
+
*/
|
|
118
|
+
export function getUserPlan(email) {
|
|
119
|
+
const auth = loadAuth();
|
|
120
|
+
if (!auth || auth.email !== email) return PLANS.free;
|
|
121
|
+
const planId = auth.plan || 'free';
|
|
122
|
+
return PLANS[planId] || PLANS.free;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get the user's scan usage info without recording a scan.
|
|
127
|
+
*/
|
|
128
|
+
export function getScanUsage() {
|
|
129
|
+
const auth = loadAuth();
|
|
130
|
+
if (!auth) return { scansUsed: 0, maxScans: FREE_MONTHLY_SCANS, plan: 'Free' };
|
|
131
|
+
|
|
132
|
+
const plan = PLANS[auth.plan] || PLANS.free;
|
|
133
|
+
const monthKey = getCurrentMonthKey();
|
|
134
|
+
const scansUsed = auth.monthKey === monthKey ? (auth.scansThisMonth || 0) : 0;
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
scansUsed,
|
|
138
|
+
maxScans: plan.maxScans ?? FREE_MONTHLY_SCANS,
|
|
139
|
+
plan: plan.name,
|
|
140
|
+
email: auth.email,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get all plan definitions.
|
|
146
|
+
*/
|
|
147
|
+
export function getPlans() {
|
|
148
|
+
return PLANS;
|
|
149
|
+
}
|
package/src/baseline.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Baseline mode: save/load/diff findings to show only NEW issues
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PATH = '.doorman-baseline.json';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a fingerprint for a finding (file + rule + line range)
|
|
9
|
+
*/
|
|
10
|
+
function fingerprint(f) {
|
|
11
|
+
return createHash('sha256')
|
|
12
|
+
.update(`${f.ruleId}::${f.file}::${Math.floor((f.line || 0) / 5)}`)
|
|
13
|
+
.digest('hex')
|
|
14
|
+
.slice(0, 32);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Save current findings as baseline
|
|
19
|
+
*/
|
|
20
|
+
export function saveBaseline(findings, path = DEFAULT_PATH) {
|
|
21
|
+
const baseline = {
|
|
22
|
+
version: 1,
|
|
23
|
+
createdAt: new Date().toISOString(),
|
|
24
|
+
count: findings.length,
|
|
25
|
+
fingerprints: findings.map(fingerprint),
|
|
26
|
+
};
|
|
27
|
+
writeFileSync(path, JSON.stringify(baseline, null, 2) + '\n');
|
|
28
|
+
return baseline;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load existing baseline
|
|
33
|
+
*/
|
|
34
|
+
export function loadBaseline(path = DEFAULT_PATH) {
|
|
35
|
+
if (!existsSync(path)) return null;
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
38
|
+
} catch { return null; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Filter findings to only show NEW ones (not in baseline)
|
|
43
|
+
*/
|
|
44
|
+
export function diffFindings(findings, baseline) {
|
|
45
|
+
if (!baseline || !baseline.fingerprints) return findings;
|
|
46
|
+
const known = new Set(baseline.fingerprints);
|
|
47
|
+
return findings.filter(f => !known.has(fingerprint(f)));
|
|
48
|
+
}
|