getdoorman 1.0.5 → 1.0.6
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/bin/doorman.js +18 -11
- package/bin/getdoorman.js +18 -11
- package/package.json +4 -4
- package/src/auth.js +2 -2
- package/src/compliance.js +3 -2
- package/src/hooks.js +11 -31
- package/src/reporter.js +7 -6
- package/src/rules/bugs/nextjs-bugs.js +3 -1
- package/src/rules/scope-rules.js +1 -1
- package/src/rules/security/injection.js +5 -2
- package/src/rules/security/mcp-server.js +1 -1
- package/src/rules/security/ssrf.js +18 -2
- package/src/scanner.js +1 -2
- package/src/telemetry.js +2 -1
- package/src/version.js +8 -0
package/bin/doorman.js
CHANGED
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { check } from '../src/index.js';
|
|
5
5
|
import { generateReport } from '../src/compliance.js';
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
6
12
|
|
|
7
13
|
const program = new Command();
|
|
8
14
|
|
|
9
15
|
program
|
|
10
|
-
.name('
|
|
16
|
+
.name('getdoorman')
|
|
11
17
|
.description('Find security issues. Tell your AI to fix them.')
|
|
12
|
-
.version(
|
|
18
|
+
.version(pkg.version);
|
|
13
19
|
|
|
14
20
|
program
|
|
15
21
|
.command('check')
|
|
@@ -53,7 +59,7 @@ program
|
|
|
53
59
|
|
|
54
60
|
// After first scan: add CLAUDE.md, offer hooks, collect email
|
|
55
61
|
if (!options.ci && !options.quiet && !options.json && !options.sarif && !options.html && !options.silent) {
|
|
56
|
-
const { isFirstScan, installClaudeMd, installAgentsMd, installCursorRules, installClaudeHook
|
|
62
|
+
const { isFirstScan, installClaudeMd, installAgentsMd, installCursorRules, installClaudeHook } = await import('../src/hooks.js');
|
|
57
63
|
const { loadAuth, saveAuth } = await import('../src/auth.js');
|
|
58
64
|
const { resolve } = await import('path');
|
|
59
65
|
const resolvedPath = resolve(path);
|
|
@@ -61,19 +67,20 @@ program
|
|
|
61
67
|
if (isFirstScan(resolvedPath)) {
|
|
62
68
|
const chalk = (await import('chalk')).default;
|
|
63
69
|
|
|
64
|
-
// Add to
|
|
70
|
+
// Add Doorman instructions to AI config files (only appends, never overwrites)
|
|
65
71
|
const claudeMd = installClaudeMd(resolvedPath);
|
|
66
72
|
const agentsMd = installAgentsMd(resolvedPath);
|
|
67
73
|
const cursorRules = installCursorRules(resolvedPath);
|
|
68
|
-
if (claudeMd) console.log(chalk.
|
|
69
|
-
if (agentsMd) console.log(chalk.
|
|
70
|
-
if (cursorRules) console.log(chalk.
|
|
74
|
+
if (claudeMd) console.log(chalk.dim(' Added Doorman to CLAUDE.md'));
|
|
75
|
+
if (agentsMd) console.log(chalk.dim(' Added Doorman to AGENTS.md'));
|
|
76
|
+
if (cursorRules) console.log(chalk.dim(' Added Doorman to .cursorrules'));
|
|
71
77
|
|
|
72
|
-
//
|
|
78
|
+
// Auto-run hook for Claude Code (non-blocking, just a scan)
|
|
73
79
|
const hookInstalled = installClaudeHook(resolvedPath);
|
|
74
|
-
if (hookInstalled) console.log(chalk.
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
if (hookInstalled) console.log(chalk.dim(' Auto-run enabled for Claude Code'));
|
|
81
|
+
|
|
82
|
+
// NOTE: No pre-commit hook on first scan — too aggressive.
|
|
83
|
+
// Users can opt in with: npx getdoorman init
|
|
77
84
|
|
|
78
85
|
// Collect email
|
|
79
86
|
const auth = loadAuth();
|
package/bin/getdoorman.js
CHANGED
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { check } from '../src/index.js';
|
|
5
5
|
import { generateReport } from '../src/compliance.js';
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
6
12
|
|
|
7
13
|
const program = new Command();
|
|
8
14
|
|
|
9
15
|
program
|
|
10
|
-
.name('
|
|
16
|
+
.name('getdoorman')
|
|
11
17
|
.description('Find security issues. Tell your AI to fix them.')
|
|
12
|
-
.version(
|
|
18
|
+
.version(pkg.version);
|
|
13
19
|
|
|
14
20
|
program
|
|
15
21
|
.command('check')
|
|
@@ -53,7 +59,7 @@ program
|
|
|
53
59
|
|
|
54
60
|
// After first scan: add CLAUDE.md, offer hooks, collect email
|
|
55
61
|
if (!options.ci && !options.quiet && !options.json && !options.sarif && !options.html && !options.silent) {
|
|
56
|
-
const { isFirstScan, installClaudeMd, installAgentsMd, installCursorRules, installClaudeHook
|
|
62
|
+
const { isFirstScan, installClaudeMd, installAgentsMd, installCursorRules, installClaudeHook } = await import('../src/hooks.js');
|
|
57
63
|
const { loadAuth, saveAuth } = await import('../src/auth.js');
|
|
58
64
|
const { resolve } = await import('path');
|
|
59
65
|
const resolvedPath = resolve(path);
|
|
@@ -61,19 +67,20 @@ program
|
|
|
61
67
|
if (isFirstScan(resolvedPath)) {
|
|
62
68
|
const chalk = (await import('chalk')).default;
|
|
63
69
|
|
|
64
|
-
// Add to
|
|
70
|
+
// Add Doorman instructions to AI config files (only appends, never overwrites)
|
|
65
71
|
const claudeMd = installClaudeMd(resolvedPath);
|
|
66
72
|
const agentsMd = installAgentsMd(resolvedPath);
|
|
67
73
|
const cursorRules = installCursorRules(resolvedPath);
|
|
68
|
-
if (claudeMd) console.log(chalk.
|
|
69
|
-
if (agentsMd) console.log(chalk.
|
|
70
|
-
if (cursorRules) console.log(chalk.
|
|
74
|
+
if (claudeMd) console.log(chalk.dim(' Added Doorman to CLAUDE.md'));
|
|
75
|
+
if (agentsMd) console.log(chalk.dim(' Added Doorman to AGENTS.md'));
|
|
76
|
+
if (cursorRules) console.log(chalk.dim(' Added Doorman to .cursorrules'));
|
|
71
77
|
|
|
72
|
-
//
|
|
78
|
+
// Auto-run hook for Claude Code (non-blocking, just a scan)
|
|
73
79
|
const hookInstalled = installClaudeHook(resolvedPath);
|
|
74
|
-
if (hookInstalled) console.log(chalk.
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
if (hookInstalled) console.log(chalk.dim(' Auto-run enabled for Claude Code'));
|
|
81
|
+
|
|
82
|
+
// NOTE: No pre-commit hook on first scan — too aggressive.
|
|
83
|
+
// Users can opt in with: npx getdoorman init
|
|
77
84
|
|
|
78
85
|
// Collect email
|
|
79
86
|
const auth = loadAuth();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "getdoorman",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Zero-config security scanner for AI-assisted development. 2000+ rules, 11 languages, 4 detection engines.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
"license": "MIT",
|
|
37
37
|
"repository": {
|
|
38
38
|
"type": "git",
|
|
39
|
-
"url": "https://
|
|
39
|
+
"url": "https://doorman.sh"
|
|
40
40
|
},
|
|
41
|
-
"homepage": "https://
|
|
41
|
+
"homepage": "https://doorman.sh",
|
|
42
42
|
"bugs": {
|
|
43
|
-
"url": "https://
|
|
43
|
+
"url": "https://doorman.sh"
|
|
44
44
|
},
|
|
45
45
|
"files": [
|
|
46
46
|
"src/",
|
package/src/auth.js
CHANGED
|
@@ -12,12 +12,12 @@ const PLANS = {
|
|
|
12
12
|
categories: ['security', 'bugs'],
|
|
13
13
|
},
|
|
14
14
|
pro: {
|
|
15
|
-
name: 'Pro', price: '
|
|
15
|
+
name: 'Pro', price: 'coming soon', maxScans: Infinity,
|
|
16
16
|
categories: ['security', 'bugs', 'performance', 'reliability', 'cost', 'data', 'quality'],
|
|
17
17
|
dashboard: true, autoRun: true, cicd: true,
|
|
18
18
|
},
|
|
19
19
|
enterprise: {
|
|
20
|
-
name: 'Enterprise', price: '
|
|
20
|
+
name: 'Enterprise', price: 'coming soon', maxScans: Infinity,
|
|
21
21
|
categories: 'all',
|
|
22
22
|
dashboard: true, autoRun: true, cicd: true,
|
|
23
23
|
compliance: true, teamDashboard: true, prComments: true, slack: true, customRules: true,
|
package/src/compliance.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { VERSION } from './version.js';
|
|
1
2
|
import { writeFileSync } from 'fs';
|
|
2
3
|
import { resolve } from 'path';
|
|
3
4
|
|
|
@@ -421,7 +422,7 @@ export function generateReport(scanResult, options = {}) {
|
|
|
421
422
|
<tr><td>Project</td><td>${escapeHtml(projectName)}</td></tr>
|
|
422
423
|
<tr><td>Detected Stack</td><td>${escapeHtml(stackLabel)}</td></tr>
|
|
423
424
|
<tr><td>Total Findings</td><td>${findings.length}</td></tr>
|
|
424
|
-
<tr><td>Doorman Version</td><td>
|
|
425
|
+
<tr><td>Doorman Version</td><td>v${VERSION}</td></tr>
|
|
425
426
|
</table>
|
|
426
427
|
|
|
427
428
|
<div class="section">
|
|
@@ -486,7 +487,7 @@ export function generateReport(scanResult, options = {}) {
|
|
|
486
487
|
|
|
487
488
|
<div class="attestation">
|
|
488
489
|
<strong>Attestation</strong><br>
|
|
489
|
-
This report was generated by Doorman
|
|
490
|
+
This report was generated by Doorman v${VERSION} on ${escapeHtml(reportTime)}. The findings
|
|
490
491
|
reflect an automated analysis of the project source code at the time of the scan. This report
|
|
491
492
|
is intended to support compliance efforts and should be reviewed by a qualified auditor before
|
|
492
493
|
being used as evidence for certification or regulatory purposes.
|
package/src/hooks.js
CHANGED
|
@@ -90,23 +90,15 @@ npx getdoorman check --quiet --ci
|
|
|
90
90
|
*/
|
|
91
91
|
export function installClaudeMd(targetPath) {
|
|
92
92
|
const claudeMdPath = join(targetPath, 'CLAUDE.md');
|
|
93
|
-
const
|
|
94
|
-
## Doorman — Code Safety
|
|
95
|
-
|
|
96
|
-
Before shipping or when asked to check code safety, run:
|
|
97
|
-
\`\`\`bash
|
|
98
|
-
npx getdoorman check
|
|
99
|
-
\`\`\`
|
|
100
|
-
|
|
101
|
-
To fix issues, run \`npx getdoorman fix critical\` and follow the instructions.
|
|
102
|
-
`;
|
|
93
|
+
const doormanLine = '\n# Run `npx getdoorman check` before shipping to find security issues.\n';
|
|
103
94
|
|
|
104
95
|
if (existsSync(claudeMdPath)) {
|
|
105
96
|
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
106
97
|
if (content.includes('doorman') || content.includes('getdoorman')) return false;
|
|
107
|
-
|
|
98
|
+
// Append one line — don't overwrite existing content
|
|
99
|
+
writeFileSync(claudeMdPath, content + doormanLine);
|
|
108
100
|
} else {
|
|
109
|
-
writeFileSync(claudeMdPath,
|
|
101
|
+
writeFileSync(claudeMdPath, doormanLine.trimStart());
|
|
110
102
|
}
|
|
111
103
|
return true;
|
|
112
104
|
}
|
|
@@ -116,23 +108,14 @@ To fix issues, run \`npx getdoorman fix critical\` and follow the instructions.
|
|
|
116
108
|
*/
|
|
117
109
|
export function installAgentsMd(targetPath) {
|
|
118
110
|
const agentsMdPath = join(targetPath, 'AGENTS.md');
|
|
119
|
-
const
|
|
120
|
-
## Doorman — Code Safety
|
|
121
|
-
|
|
122
|
-
Before shipping or when asked to check code safety, run:
|
|
123
|
-
\`\`\`bash
|
|
124
|
-
npx getdoorman check
|
|
125
|
-
\`\`\`
|
|
126
|
-
|
|
127
|
-
To fix issues, run \`npx getdoorman fix critical\` and follow the instructions.
|
|
128
|
-
`;
|
|
111
|
+
const doormanLine = '\n# Run `npx getdoorman check` before shipping to find security issues.\n';
|
|
129
112
|
|
|
130
113
|
if (existsSync(agentsMdPath)) {
|
|
131
114
|
const content = readFileSync(agentsMdPath, 'utf-8');
|
|
132
115
|
if (content.includes('doorman') || content.includes('getdoorman')) return false;
|
|
133
|
-
writeFileSync(agentsMdPath, content +
|
|
116
|
+
writeFileSync(agentsMdPath, content + doormanLine);
|
|
134
117
|
} else {
|
|
135
|
-
writeFileSync(agentsMdPath,
|
|
118
|
+
writeFileSync(agentsMdPath, doormanLine.trimStart());
|
|
136
119
|
}
|
|
137
120
|
return true;
|
|
138
121
|
}
|
|
@@ -142,18 +125,15 @@ To fix issues, run \`npx getdoorman fix critical\` and follow the instructions.
|
|
|
142
125
|
*/
|
|
143
126
|
export function installCursorRules(targetPath) {
|
|
144
127
|
const cursorPath = join(targetPath, '.cursorrules');
|
|
145
|
-
const
|
|
146
|
-
# Doorman — Code Safety
|
|
147
|
-
Before shipping or when asked to check code safety, run: npx getdoorman check
|
|
148
|
-
To fix issues, run: npx getdoorman fix critical
|
|
149
|
-
`;
|
|
128
|
+
const doormanLine = '\n# Run `npx getdoorman check` before shipping to find security issues.\n';
|
|
150
129
|
|
|
151
130
|
if (existsSync(cursorPath)) {
|
|
152
131
|
const content = readFileSync(cursorPath, 'utf-8');
|
|
153
132
|
if (content.includes('doorman') || content.includes('getdoorman')) return false;
|
|
154
|
-
writeFileSync(cursorPath, content +
|
|
133
|
+
writeFileSync(cursorPath, content + doormanLine);
|
|
155
134
|
} else {
|
|
156
|
-
|
|
135
|
+
// Don't create .cursorrules if it doesn't exist — too invasive
|
|
136
|
+
return false;
|
|
157
137
|
}
|
|
158
138
|
return true;
|
|
159
139
|
}
|
package/src/reporter.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { writeFileSync } from 'fs';
|
|
3
|
+
import { VERSION } from './version.js';
|
|
3
4
|
|
|
4
5
|
const SEVERITY_COLORS = {
|
|
5
6
|
critical: chalk.bgRed.white.bold,
|
|
@@ -406,7 +407,7 @@ export function printReport(findings, stack, score, options = {}) {
|
|
|
406
407
|
|
|
407
408
|
console.log('');
|
|
408
409
|
console.log(chalk.bold.cyan('\u2501'.repeat(60)));
|
|
409
|
-
console.log(chalk.bold.cyan(
|
|
410
|
+
console.log(chalk.bold.cyan(` Doorman v 2014 Scan Results`));
|
|
410
411
|
console.log(chalk.bold.cyan('\u2501'.repeat(60)));
|
|
411
412
|
console.log('');
|
|
412
413
|
|
|
@@ -556,7 +557,7 @@ function printVibeCoderReport(visible, score, hiddenCategoryCount = 0, proUnlock
|
|
|
556
557
|
// --- Upsell ---
|
|
557
558
|
if (proUnlockCount > 0) {
|
|
558
559
|
console.log(chalk.cyan(` 🔒 ${proUnlockCount} more issues in performance, cost, reliability, data...`));
|
|
559
|
-
console.log(chalk.gray(' Upgrade to Pro
|
|
560
|
+
console.log(chalk.gray(' Upgrade to Pro to see all categories. Coming soon at doorman.sh.'));
|
|
560
561
|
console.log('');
|
|
561
562
|
}
|
|
562
563
|
if (enterpriseUnlockCount > 0 && proUnlockCount === 0) {
|
|
@@ -575,7 +576,7 @@ export function generateJSON(findings, stack, score, metadata = {}) {
|
|
|
575
576
|
const categoryCounts = countByCategory(findings);
|
|
576
577
|
|
|
577
578
|
return {
|
|
578
|
-
version:
|
|
579
|
+
version: VERSION,
|
|
579
580
|
timestamp: new Date().toISOString(),
|
|
580
581
|
score,
|
|
581
582
|
summary: {
|
|
@@ -695,8 +696,8 @@ export function generateSARIF(findings, stack, score, metadata = {}) {
|
|
|
695
696
|
tool: {
|
|
696
697
|
driver: {
|
|
697
698
|
name: 'Doorman',
|
|
698
|
-
version:
|
|
699
|
-
informationUri: 'https://
|
|
699
|
+
version: VERSION,
|
|
700
|
+
informationUri: 'https://doorman.sh',
|
|
700
701
|
rules,
|
|
701
702
|
},
|
|
702
703
|
},
|
|
@@ -886,7 +887,7 @@ export function generateHTML(findings, stack, score, metadata = {}) {
|
|
|
886
887
|
</table>
|
|
887
888
|
</div>
|
|
888
889
|
|
|
889
|
-
<p class="meta">Generated by Doorman
|
|
890
|
+
<p class="meta">Generated by Doorman v</p>
|
|
890
891
|
</div>
|
|
891
892
|
|
|
892
893
|
<script>
|
|
@@ -20,7 +20,9 @@ const rules = [
|
|
|
20
20
|
if (hasUseClient) continue;
|
|
21
21
|
const lines = content.split('\n');
|
|
22
22
|
for (let i = 0; i < lines.length; i++) {
|
|
23
|
-
|
|
23
|
+
// Skip API route files — request.signal, addEventListener are valid server-side
|
|
24
|
+
if (/\/api\//.test(fp) || /route\.(ts|js)$/.test(fp)) continue;
|
|
25
|
+
if (/\b(window|document|localStorage|sessionStorage|navigator)\b/.test(lines[i]) && !/typeof\s+(window|document)/.test(lines[i]) && !/\/\//.test(lines[i].split(/window|document/)[0]) && !/request\.signal|AbortSignal|abortController/i.test(lines[i])) {
|
|
24
26
|
findings.push({
|
|
25
27
|
ruleId: 'BUG-NEXT-001', category: 'bugs', severity: 'high',
|
|
26
28
|
title: 'Browser API used in server component — will crash at build time',
|
package/src/rules/scope-rules.js
CHANGED
|
@@ -596,7 +596,7 @@ const rules = [
|
|
|
596
596
|
for (let i = 0; i < lines.length; i++) {
|
|
597
597
|
const lineNum = i + 1;
|
|
598
598
|
if (scope.isInsideComment(lineNum) || scope.isInsideString(lineNum)) continue;
|
|
599
|
-
if (/\b(eval|Function)\s*\(/.test(lines[i]) || /\bexec\s*\(/.test(lines[i])) {
|
|
599
|
+
if (/\b(eval|Function)\s*\(/.test(lines[i]) || /\b(child_process\.|execSync|execFileSync)\s*\(/.test(lines[i]) || /(?<!\w\.)\bexec\s*\(/.test(lines[i]) && !/\.exec\s*\(/.test(lines[i])) {
|
|
600
600
|
if (!hasSandbox) {
|
|
601
601
|
findings.push({
|
|
602
602
|
ruleId: this.id, category: this.category, severity: this.severity,
|
|
@@ -1295,13 +1295,16 @@ rules.push({
|
|
|
1295
1295
|
title: 'SQL Injection via template literal in query string',
|
|
1296
1296
|
check({ files }) {
|
|
1297
1297
|
const findings = [];
|
|
1298
|
-
|
|
1298
|
+
// Must have SQL keyword + SQL clause keyword (FROM/INTO/SET/WHERE/VALUES/TABLE) + ${interpolation}
|
|
1299
|
+
const p = /`\s*(?:SELECT\s.*\sFROM|INSERT\s.*\sINTO|UPDATE\s.*\sSET|DELETE\s.*\sFROM|CREATE\s.*\sTABLE|DROP\s.*\sTABLE|ALTER\s.*\sTABLE)[^`]*\$\{/i;
|
|
1299
1300
|
for (const [path, content] of files) {
|
|
1300
1301
|
if (SKIP_PATH.test(path) || !isJS(path)) continue;
|
|
1301
1302
|
const lines = content.split('\n');
|
|
1302
1303
|
for (let i = 0; i < lines.length; i++) {
|
|
1303
1304
|
if (COMMENT_LINE.test(lines[i])) continue;
|
|
1304
|
-
|
|
1305
|
+
// Check current line + next 2 lines (SQL queries often span multiple lines)
|
|
1306
|
+
const block = lines.slice(i, i + 3).join(' ');
|
|
1307
|
+
if (p.test(block)) findings.push({ ruleId: 'SEC-INJ-041', category: 'security', severity: 'critical', title: 'SQL built with template literal — injection risk', description: 'Template literals in SQL strings allow injection when they contain user input. Use parameterized queries.', file: path, line: i + 1, fix: null });
|
|
1305
1308
|
}
|
|
1306
1309
|
}
|
|
1307
1310
|
return findings;
|
|
@@ -47,7 +47,7 @@ const rules = [
|
|
|
47
47
|
{ id: 'SEC-MCP-028', category: 'security', severity: 'low', confidence: 'suggestion', title: 'MCP description leaks impl', check({ files }) { return checkAll(this, files, /description:\s*['"].*(?:database|internal|private)/i); } },
|
|
48
48
|
{ id: 'SEC-MCP-029', category: 'security', severity: 'medium', confidence: 'likely', title: 'MCP cross-tool data leak', check({ files }) { return checkAll(this, files, /(?:global|globalThis)\.\w+.*=.*(?:params|args)\./); } },
|
|
49
49
|
{ id: 'SEC-MCP-030', category: 'security', severity: 'medium', confidence: 'suggestion', title: 'MCP debug mode enabled', check({ files }) { return checkAll(this, files, /(?:debug|verbose)\s*[:=]\s*true/); } },
|
|
50
|
-
{ id: 'SEC-MCP-031', category: 'security', severity: 'critical', confidence: 'likely', title: 'MCP shell injection', check({ files }) { return checkAll(this, files, /
|
|
50
|
+
{ id: 'SEC-MCP-031', category: 'security', severity: 'critical', confidence: 'likely', title: 'MCP shell injection', check({ files }) { return checkAll(this, files, /(?:child_process\.|execSync|execFile)\s*\(\s*`[^`]*\$\{(?:params|args|input)\./); } },
|
|
51
51
|
{ id: 'SEC-MCP-032', category: 'security', severity: 'critical', confidence: 'likely', title: 'MCP template injection', check({ files }) { return checkAll(this, files, /(?:render|compile|template)\(\s*(?:params|args|input)\./); } },
|
|
52
52
|
{ id: 'SEC-MCP-033', category: 'security', severity: 'high', confidence: 'likely', title: 'MCP LDAP injection', check({ files }) { return checkAll(this, files, /(?:ldap|LDAP).*(?:search|bind).*(?:params|args)\./); } },
|
|
53
53
|
{ id: 'SEC-MCP-034', category: 'security', severity: 'high', confidence: 'likely', title: 'MCP XML injection', check({ files }) { return checkAll(this, files, /[<].*\$\{(?:params|args|input)\./); } },
|
|
@@ -49,10 +49,20 @@ const rules = [
|
|
|
49
49
|
check({ files }) {
|
|
50
50
|
const findings = [];
|
|
51
51
|
const pattern = /(?:fetch|axios\.get|axios\.post|axios\.put|axios\.delete|axios\.request|axios\(|http\.get|http\.request|https\.get|https\.request|got\(|got\.get|request\(|needle\(|superagent\.get)\s*\(\s*(?:req\.body|req\.query|req\.params|userUrl|url|targetUrl|input)\b/;
|
|
52
|
+
const validationContext = /validateUrl|isValidUrl|allowedDomains|urlAllowlist|safeUrl|URL\.parse|new URL\(|protocol\s*[!=]==|hostname\s*[!=]==|startsWith\s*\(\s*['"]https/i;
|
|
52
53
|
for (const [path, content] of files) {
|
|
53
54
|
if (SKIP_PATH.test(path)) continue;
|
|
54
55
|
if (isJS(path)) {
|
|
55
|
-
|
|
56
|
+
const lines = content.split('\n');
|
|
57
|
+
for (let i = 0; i < lines.length; i++) {
|
|
58
|
+
if (pattern.test(lines[i])) {
|
|
59
|
+
// Check surrounding 10 lines for URL validation
|
|
60
|
+
const context = lines.slice(Math.max(0, i - 10), i + 5).join('\n');
|
|
61
|
+
if (!validationContext.test(context)) {
|
|
62
|
+
findings.push({ ruleId: this.id, category: this.category, severity: this.severity, title: this.title, description: this.description, confidence: this.confidence, file: path, line: i + 1, fix: this.fix });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
56
66
|
}
|
|
57
67
|
}
|
|
58
68
|
return findings;
|
|
@@ -95,10 +105,16 @@ const rules = [
|
|
|
95
105
|
check({ files }) {
|
|
96
106
|
const findings = [];
|
|
97
107
|
const pattern = /169\.254\.169\.254|metadata\.google\.internal|metadata\.azure\.com/;
|
|
108
|
+
const blocklistContext = /block|deny|forbidden|not.?allowed|invalid|reject|blacklist|safelist|denylist|disallow|banned|BLOCKED/i;
|
|
98
109
|
for (const [path, content] of files) {
|
|
99
110
|
if (SKIP_PATH.test(path)) continue;
|
|
100
111
|
if (isJS(path)) {
|
|
101
|
-
|
|
112
|
+
const lines = content.split('\n');
|
|
113
|
+
for (let i = 0; i < lines.length; i++) {
|
|
114
|
+
if (pattern.test(lines[i]) && !blocklistContext.test(lines[i])) {
|
|
115
|
+
findings.push({ ruleId: this.id, category: this.category, severity: this.severity, title: this.title, description: this.description, confidence: this.confidence, file: path, line: i + 1, fix: this.fix });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
102
118
|
}
|
|
103
119
|
}
|
|
104
120
|
return findings;
|
package/src/scanner.js
CHANGED
|
@@ -8,8 +8,7 @@ import { Worker } from 'worker_threads';
|
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
9
|
import { join, relative, dirname } from 'path';
|
|
10
10
|
import { loadIgnorePatterns } from './ignore.js';
|
|
11
|
-
|
|
12
|
-
const DOORMAN_VERSION = '1.0.0';
|
|
11
|
+
import { VERSION as DOORMAN_VERSION } from './version.js';
|
|
13
12
|
|
|
14
13
|
const SOURCE_PATTERNS = [
|
|
15
14
|
'**/*.js',
|
package/src/telemetry.js
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
30
|
|
|
31
31
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
32
|
+
import { VERSION } from './version.js';
|
|
32
33
|
import { join } from 'path';
|
|
33
34
|
import { createHash } from 'crypto';
|
|
34
35
|
import { execSync } from 'child_process';
|
|
@@ -88,7 +89,7 @@ export function getAnonymousId(targetPath) {
|
|
|
88
89
|
*/
|
|
89
90
|
export function buildPayload(scanResult, stack) {
|
|
90
91
|
return {
|
|
91
|
-
version:
|
|
92
|
+
version: VERSION,
|
|
92
93
|
timestamp: new Date().toISOString(),
|
|
93
94
|
// What stack (generic, not specific project)
|
|
94
95
|
stack: {
|
package/src/version.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
7
|
+
|
|
8
|
+
export const VERSION = pkg.version;
|