getdoorman 1.0.9 → 1.1.1
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 +45 -41
- package/bin/getdoorman.js +45 -41
- package/package.json +1 -1
- package/src/rules/bugs/general.js +3 -2
- package/src/rules/compliance/healthcare.js +1 -1
- package/src/rules/compliance/index.js +24 -24
- package/src/rules/data/index.js +26 -26
- package/src/rules/dependencies/index.js +5 -5
- package/src/rules/deployment/index.js +7 -7
- package/src/rules/infrastructure/index.js +28 -28
- package/src/rules/performance/index.js +1 -1
- package/src/rules/reliability/index.js +1 -1
- package/src/rules/security/ai-api.js +1 -0
- package/src/rules/security/auth.js +2 -2
- package/src/rules/security/cors.js +2 -2
- package/src/rules/security/crypto.js +2 -2
- package/src/rules/security/csharp.js +3 -3
- package/src/rules/security/csrf.js +1 -1
- package/src/rules/security/file-upload.js +1 -1
- package/src/rules/security/go.js +3 -3
- package/src/rules/security/misconfiguration.js +4 -4
- package/src/rules/security/oauth-jwt.js +5 -2
- package/src/rules/security/prototype-pollution.js +2 -2
- package/src/rules/security/rate-limiting.js +1 -1
- package/src/rules/security/rust.js +3 -3
- package/src/rules/security/ssrf.js +8 -4
- package/src/rules/security/supply-chain-advanced.js +2 -2
- package/src/rules/security/xss.js +2 -2
- package/src/simple-checks.js +257 -0
- package/src/simple-reporter.js +60 -0
|
@@ -259,7 +259,7 @@ export default rules;
|
|
|
259
259
|
|
|
260
260
|
// SEC-CRY-011: Hardcoded encryption key
|
|
261
261
|
rules.push({
|
|
262
|
-
id: 'SEC-CRY-011', category: 'security', severity: '
|
|
262
|
+
id: 'SEC-CRY-011', category: 'security', severity: 'high', confidence: 'definite',
|
|
263
263
|
title: 'Hardcoded encryption key or IV',
|
|
264
264
|
check({ files }) {
|
|
265
265
|
const findings = [];
|
|
@@ -467,7 +467,7 @@ rules.push({
|
|
|
467
467
|
|
|
468
468
|
// SEC-CRY-021: Hardcoded salt
|
|
469
469
|
rules.push({
|
|
470
|
-
id: 'SEC-CRY-021', category: 'security', severity: '
|
|
470
|
+
id: 'SEC-CRY-021', category: 'security', severity: 'high', confidence: 'definite',
|
|
471
471
|
title: 'Hardcoded salt for password hashing',
|
|
472
472
|
check({ files }) {
|
|
473
473
|
const findings = [];
|
|
@@ -132,7 +132,7 @@ const rules = [
|
|
|
132
132
|
{
|
|
133
133
|
id: 'SEC-CS-006',
|
|
134
134
|
category: 'security',
|
|
135
|
-
severity: '
|
|
135
|
+
severity: 'high',
|
|
136
136
|
confidence: 'definite',
|
|
137
137
|
title: 'Insecure Deserialization: BinaryFormatter',
|
|
138
138
|
description: 'BinaryFormatter is inherently insecure and can execute arbitrary code during deserialization.',
|
|
@@ -603,7 +603,7 @@ const rules = [
|
|
|
603
603
|
{
|
|
604
604
|
id: 'SEC-CS-035',
|
|
605
605
|
category: 'security',
|
|
606
|
-
severity: '
|
|
606
|
+
severity: 'high',
|
|
607
607
|
confidence: 'definite',
|
|
608
608
|
title: 'CORS: Credentials with Any Origin',
|
|
609
609
|
description: 'AllowCredentials with AllowAnyOrigin is a severe misconfiguration.',
|
|
@@ -731,7 +731,7 @@ const rules = [
|
|
|
731
731
|
{
|
|
732
732
|
id: 'SEC-CS-043',
|
|
733
733
|
category: 'security',
|
|
734
|
-
severity: '
|
|
734
|
+
severity: 'high',
|
|
735
735
|
confidence: 'definite',
|
|
736
736
|
title: 'TLS Certificate Validation Disabled',
|
|
737
737
|
description: 'Disabling certificate validation allows man-in-the-middle attacks.',
|
|
@@ -34,7 +34,7 @@ const rules = [
|
|
|
34
34
|
|
|
35
35
|
// SEC-CSRF-002
|
|
36
36
|
{
|
|
37
|
-
id: 'SEC-CSRF-002', category: 'security', severity: '
|
|
37
|
+
id: 'SEC-CSRF-002', category: 'security', severity: 'high', confidence: 'definite',
|
|
38
38
|
title: 'CSRF token not validated on server',
|
|
39
39
|
check({ files }) {
|
|
40
40
|
const findings = [];
|
|
@@ -95,7 +95,7 @@ const rules = [
|
|
|
95
95
|
|
|
96
96
|
// SEC-UPL-005
|
|
97
97
|
{
|
|
98
|
-
id: 'SEC-UPL-005', category: 'security', severity: '
|
|
98
|
+
id: 'SEC-UPL-005', category: 'security', severity: 'high', confidence: 'definite',
|
|
99
99
|
title: 'Executable file upload allowed',
|
|
100
100
|
check({ files }) {
|
|
101
101
|
const findings = [];
|
package/src/rules/security/go.js
CHANGED
|
@@ -258,7 +258,7 @@ const rules = [
|
|
|
258
258
|
{
|
|
259
259
|
id: 'SEC-GO-014',
|
|
260
260
|
category: 'security',
|
|
261
|
-
severity: '
|
|
261
|
+
severity: 'high',
|
|
262
262
|
confidence: 'definite',
|
|
263
263
|
title: 'Insecure TLS: Certificate Verification Disabled',
|
|
264
264
|
description: 'Setting InsecureSkipVerify to true disables TLS certificate validation, enabling MITM attacks.',
|
|
@@ -450,7 +450,7 @@ const rules = [
|
|
|
450
450
|
{
|
|
451
451
|
id: 'SEC-GO-026',
|
|
452
452
|
category: 'security',
|
|
453
|
-
severity: '
|
|
453
|
+
severity: 'high',
|
|
454
454
|
confidence: 'definite',
|
|
455
455
|
title: 'CORS Allow Credentials with Wildcard Origin',
|
|
456
456
|
description: 'Allowing credentials with wildcard origin is a severe misconfiguration enabling credential theft.',
|
|
@@ -482,7 +482,7 @@ const rules = [
|
|
|
482
482
|
{
|
|
483
483
|
id: 'SEC-GO-028',
|
|
484
484
|
category: 'security',
|
|
485
|
-
severity: '
|
|
485
|
+
severity: 'high',
|
|
486
486
|
confidence: 'definite',
|
|
487
487
|
title: 'JWT None Algorithm Accepted',
|
|
488
488
|
description: 'Not validating the JWT signing method allows attackers to use the "none" algorithm to forge tokens.',
|
|
@@ -59,7 +59,7 @@ const rules = [
|
|
|
59
59
|
{
|
|
60
60
|
id: 'SEC-MISC-001',
|
|
61
61
|
category: 'security',
|
|
62
|
-
severity: '
|
|
62
|
+
severity: 'high',
|
|
63
63
|
confidence: 'definite',
|
|
64
64
|
title: 'Public S3 bucket configuration detected',
|
|
65
65
|
check({ files }) {
|
|
@@ -82,7 +82,7 @@ const rules = [
|
|
|
82
82
|
{
|
|
83
83
|
id: 'SEC-MISC-002',
|
|
84
84
|
category: 'security',
|
|
85
|
-
severity: '
|
|
85
|
+
severity: 'high',
|
|
86
86
|
confidence: 'definite',
|
|
87
87
|
title: 'Database bound to all network interfaces',
|
|
88
88
|
check({ files }) {
|
|
@@ -268,7 +268,7 @@ const rules = [
|
|
|
268
268
|
{
|
|
269
269
|
id: 'SEC-MISC-007',
|
|
270
270
|
category: 'security',
|
|
271
|
-
severity: '
|
|
271
|
+
severity: 'high',
|
|
272
272
|
confidence: 'definite',
|
|
273
273
|
title: 'Permissive database security rules',
|
|
274
274
|
check({ files }) {
|
|
@@ -592,7 +592,7 @@ const rules = [
|
|
|
592
592
|
{
|
|
593
593
|
id: 'SEC-MISC-015',
|
|
594
594
|
category: 'security',
|
|
595
|
-
severity: '
|
|
595
|
+
severity: 'high',
|
|
596
596
|
confidence: 'definite',
|
|
597
597
|
title: 'Overly permissive IAM policy detected',
|
|
598
598
|
check({ files }) {
|
|
@@ -39,7 +39,7 @@ const rules = [
|
|
|
39
39
|
{
|
|
40
40
|
id: 'SEC-JWT-001',
|
|
41
41
|
category: 'security',
|
|
42
|
-
severity: '
|
|
42
|
+
severity: 'high',
|
|
43
43
|
confidence: 'definite',
|
|
44
44
|
title: 'JWT Algorithm "none" Allowed',
|
|
45
45
|
description:
|
|
@@ -108,7 +108,7 @@ const rules = [
|
|
|
108
108
|
{
|
|
109
109
|
id: 'SEC-JWT-004',
|
|
110
110
|
category: 'security',
|
|
111
|
-
severity: '
|
|
111
|
+
severity: 'high',
|
|
112
112
|
confidence: 'definite',
|
|
113
113
|
title: 'Weak JWT Signing Secret',
|
|
114
114
|
description:
|
|
@@ -241,10 +241,13 @@ const rules = [
|
|
|
241
241
|
const findings = [];
|
|
242
242
|
const callbackPattern = /(?:\/callback|\/oauth\/callback|\/auth\/callback)/;
|
|
243
243
|
const tokenExchange = /(?:getToken|requestToken|exchangeCode|code.*token|grant_type.*authorization_code)/;
|
|
244
|
+
// Check if ANY file in the project validates OAuth state (cross-file check)
|
|
245
|
+
const projectHasStateValidation = [...files.values()].some(c => /(?:state|csrf|nonce).*(?:verify|validate|check|compare|match|===)/i.test(c));
|
|
244
246
|
for (const [path, content] of files) {
|
|
245
247
|
if (SKIP_PATH.test(path)) continue;
|
|
246
248
|
if (!isJS(path)) continue;
|
|
247
249
|
if (callbackPattern.test(content) && tokenExchange.test(content)) {
|
|
250
|
+
if (projectHasStateValidation) continue; // State is validated somewhere in the project
|
|
248
251
|
if (!/(?:state|csrf|nonce)/.test(content)) {
|
|
249
252
|
findings.push({
|
|
250
253
|
ruleId: this.id,
|
|
@@ -39,7 +39,7 @@ const rules = [
|
|
|
39
39
|
{
|
|
40
40
|
id: 'SEC-PP-001',
|
|
41
41
|
category: 'security',
|
|
42
|
-
severity: '
|
|
42
|
+
severity: 'high',
|
|
43
43
|
confidence: 'definite',
|
|
44
44
|
title: 'Direct __proto__ Property Access',
|
|
45
45
|
description:
|
|
@@ -108,7 +108,7 @@ const rules = [
|
|
|
108
108
|
{
|
|
109
109
|
id: 'SEC-PP-004',
|
|
110
110
|
category: 'security',
|
|
111
|
-
severity: '
|
|
111
|
+
severity: 'high',
|
|
112
112
|
confidence: 'definite',
|
|
113
113
|
title: 'Constructor Prototype Access Pattern',
|
|
114
114
|
description:
|
|
@@ -30,7 +30,7 @@ const rules = [
|
|
|
30
30
|
|
|
31
31
|
// SEC-RL-002
|
|
32
32
|
{
|
|
33
|
-
id: 'SEC-RL-002', category: 'security', severity: '
|
|
33
|
+
id: 'SEC-RL-002', category: 'security', severity: 'high', confidence: 'definite',
|
|
34
34
|
title: 'No rate limiting on login endpoint',
|
|
35
35
|
check({ files, stack }) {
|
|
36
36
|
const findings = [];
|
|
@@ -274,7 +274,7 @@ const rules = [
|
|
|
274
274
|
{
|
|
275
275
|
id: 'SEC-RS-015',
|
|
276
276
|
category: 'security',
|
|
277
|
-
severity: '
|
|
277
|
+
severity: 'high',
|
|
278
278
|
confidence: 'definite',
|
|
279
279
|
title: 'TLS Certificate Validation Disabled',
|
|
280
280
|
description: 'Disabling TLS certificate validation allows man-in-the-middle attacks.',
|
|
@@ -290,7 +290,7 @@ const rules = [
|
|
|
290
290
|
{
|
|
291
291
|
id: 'SEC-RS-016',
|
|
292
292
|
category: 'security',
|
|
293
|
-
severity: '
|
|
293
|
+
severity: 'high',
|
|
294
294
|
confidence: 'definite',
|
|
295
295
|
title: 'TLS Hostname Validation Disabled',
|
|
296
296
|
description: 'Disabling hostname validation in TLS allows certificate substitution attacks.',
|
|
@@ -549,7 +549,7 @@ const rules = [
|
|
|
549
549
|
{
|
|
550
550
|
id: 'SEC-RS-032',
|
|
551
551
|
category: 'security',
|
|
552
|
-
severity: '
|
|
552
|
+
severity: 'high',
|
|
553
553
|
confidence: 'definite',
|
|
554
554
|
title: 'Hardcoded Private Key',
|
|
555
555
|
description: 'Private keys embedded in source code can be extracted and used to impersonate the service.',
|
|
@@ -96,7 +96,7 @@ const rules = [
|
|
|
96
96
|
{
|
|
97
97
|
id: 'SEC-SSRF-003',
|
|
98
98
|
category: 'security',
|
|
99
|
-
severity: '
|
|
99
|
+
severity: 'high',
|
|
100
100
|
confidence: 'definite',
|
|
101
101
|
title: 'Cloud Metadata Endpoint URL in Code',
|
|
102
102
|
description:
|
|
@@ -105,14 +105,18 @@ const rules = [
|
|
|
105
105
|
check({ files }) {
|
|
106
106
|
const findings = [];
|
|
107
107
|
const pattern = /169\.254\.169\.254|metadata\.google\.internal|metadata\.azure\.com/;
|
|
108
|
-
const
|
|
108
|
+
const safeContext = /block|deny|forbidden|not.?allowed|invalid|reject|blacklist|safelist|denylist|disallow|banned|BLOCKED|isAllowed|validate|security|guard|check|filter|protect/i;
|
|
109
109
|
for (const [path, content] of files) {
|
|
110
110
|
if (SKIP_PATH.test(path)) continue;
|
|
111
111
|
if (isJS(path)) {
|
|
112
112
|
const lines = content.split('\n');
|
|
113
113
|
for (let i = 0; i < lines.length; i++) {
|
|
114
|
-
if (pattern.test(lines[i])
|
|
115
|
-
|
|
114
|
+
if (pattern.test(lines[i])) {
|
|
115
|
+
// Check ±5 lines for security/validation context
|
|
116
|
+
const ctx = lines.slice(Math.max(0, i - 5), i + 5).join(' ');
|
|
117
|
+
if (!safeContext.test(ctx)) {
|
|
118
|
+
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 });
|
|
119
|
+
}
|
|
116
120
|
}
|
|
117
121
|
}
|
|
118
122
|
}
|
|
@@ -40,7 +40,7 @@ const rules = [
|
|
|
40
40
|
{
|
|
41
41
|
id: 'SEC-SCA-001',
|
|
42
42
|
category: 'security',
|
|
43
|
-
severity: '
|
|
43
|
+
severity: 'high',
|
|
44
44
|
confidence: 'definite',
|
|
45
45
|
title: 'Postinstall Script Executes Binary or Shell Command',
|
|
46
46
|
description:
|
|
@@ -77,7 +77,7 @@ const rules = [
|
|
|
77
77
|
{
|
|
78
78
|
id: 'SEC-SCA-002',
|
|
79
79
|
category: 'security',
|
|
80
|
-
severity: '
|
|
80
|
+
severity: 'high',
|
|
81
81
|
confidence: 'definite',
|
|
82
82
|
title: 'Install Hook Downloads and Executes Remote Code',
|
|
83
83
|
description:
|
|
@@ -230,7 +230,7 @@ const rules = [
|
|
|
230
230
|
{
|
|
231
231
|
id: 'SEC-XSS-007',
|
|
232
232
|
category: 'security',
|
|
233
|
-
severity: '
|
|
233
|
+
severity: 'high',
|
|
234
234
|
confidence: 'definite',
|
|
235
235
|
title: 'javascript: protocol in href/src attributes',
|
|
236
236
|
check({ files }) {
|
|
@@ -498,7 +498,7 @@ const rules = [
|
|
|
498
498
|
},
|
|
499
499
|
},
|
|
500
500
|
// SEC-XSS-013: Server-side reflected XSS via res.send/res.write
|
|
501
|
-
{ id: 'SEC-XSS-013', category: 'security', severity: '
|
|
501
|
+
{ id: 'SEC-XSS-013', category: 'security', severity: 'high', confidence: 'definite', title: 'Server-Side Reflected XSS via res.send()',
|
|
502
502
|
check({ files }) {
|
|
503
503
|
const findings = [];
|
|
504
504
|
for (const [fp, c] of files) {
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doorman Simple Checks — 10 things that matter, zero false positives.
|
|
3
|
+
* Each check returns { pass: boolean, message: string, file?: string, line?: number }
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const CHECKS = [
|
|
7
|
+
{
|
|
8
|
+
id: 'leaked-keys',
|
|
9
|
+
name: 'Leaked API Keys',
|
|
10
|
+
icon: '🔑',
|
|
11
|
+
pass: 'No leaked API keys found',
|
|
12
|
+
fail: 'API key hardcoded in source code',
|
|
13
|
+
check(files) {
|
|
14
|
+
const patterns = [
|
|
15
|
+
{ name: 'AWS Access Key', regex: /(?:^|[^A-Z0-9])AKIA[0-9A-Z]{16}(?:[^A-Z0-9]|$)/ },
|
|
16
|
+
{ name: 'Stripe Secret Key', regex: /sk_live_[0-9a-zA-Z]{24,}/ },
|
|
17
|
+
{ name: 'OpenAI API Key', regex: /sk-[a-zA-Z0-9]{20,}T3BlbkFJ/ },
|
|
18
|
+
{ name: 'Anthropic API Key', regex: /sk-ant-[a-zA-Z0-9-]{20,}/ },
|
|
19
|
+
{ name: 'GitHub Token', regex: /ghp_[0-9a-zA-Z]{36}/ },
|
|
20
|
+
{ name: 'Supabase Service Key', regex: /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[a-zA-Z0-9_-]{50,}/ },
|
|
21
|
+
{ name: 'Google API Key', regex: /AIza[0-9A-Za-z_-]{35}/ },
|
|
22
|
+
{ name: 'Slack Token', regex: /xoxb-[0-9]{11,}-[0-9a-zA-Z]{24,}/ },
|
|
23
|
+
];
|
|
24
|
+
const findings = [];
|
|
25
|
+
for (const [fp, content] of files) {
|
|
26
|
+
if (fp.endsWith('.example') || fp.endsWith('.sample') || fp.endsWith('.template')) continue;
|
|
27
|
+
if (/test|spec|mock|fixture|__test__|\.test\.|\.spec\./i.test(fp)) continue;
|
|
28
|
+
const lines = content.split('\n');
|
|
29
|
+
for (let i = 0; i < lines.length; i++) {
|
|
30
|
+
if (/^\s*(\/\/|#|\*|\/\*)/.test(lines[i])) continue; // skip comments
|
|
31
|
+
for (const p of patterns) {
|
|
32
|
+
if (p.regex.test(lines[i])) {
|
|
33
|
+
findings.push({ message: `${p.name} found`, file: fp, line: i + 1 });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return findings;
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'env-exposed',
|
|
43
|
+
name: '.env File Exposed',
|
|
44
|
+
icon: '📄',
|
|
45
|
+
pass: '.env is safe (in .gitignore)',
|
|
46
|
+
fail: '.env file may be committed to git',
|
|
47
|
+
check(files) {
|
|
48
|
+
const hasEnv = [...files.keys()].some(f => f === '.env' || f.match(/^\.env\.[^.]+$/) && !f.endsWith('.example') && !f.endsWith('.sample'));
|
|
49
|
+
const hasGitignore = files.has('.gitignore');
|
|
50
|
+
const gitignoreContent = files.get('.gitignore') || '';
|
|
51
|
+
const envInGitignore = /^\.env$/m.test(gitignoreContent) || /^\.env\b/m.test(gitignoreContent);
|
|
52
|
+
|
|
53
|
+
if (hasEnv && !envInGitignore) {
|
|
54
|
+
return [{ message: '.env file found but not in .gitignore' }];
|
|
55
|
+
}
|
|
56
|
+
return [];
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'sql-injection',
|
|
61
|
+
name: 'SQL Injection',
|
|
62
|
+
icon: '💉',
|
|
63
|
+
pass: 'No SQL injection patterns found',
|
|
64
|
+
fail: 'SQL query built with user input',
|
|
65
|
+
check(files) {
|
|
66
|
+
const findings = [];
|
|
67
|
+
const sqlPattern = /(?:query|execute|raw)\s*\(\s*`[^`]*(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)\b[^`]*\$\{/i;
|
|
68
|
+
for (const [fp, content] of files) {
|
|
69
|
+
if (/test|spec|mock/i.test(fp)) continue;
|
|
70
|
+
if (!fp.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) continue;
|
|
71
|
+
const lines = content.split('\n');
|
|
72
|
+
for (let i = 0; i < lines.length; i++) {
|
|
73
|
+
const block = lines.slice(i, i + 3).join(' ');
|
|
74
|
+
if (sqlPattern.test(block)) {
|
|
75
|
+
findings.push({ message: 'SQL query built with template literal', file: fp, line: i + 1 });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return findings;
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'no-error-handling',
|
|
84
|
+
name: 'Missing Error Handling',
|
|
85
|
+
icon: '💥',
|
|
86
|
+
pass: 'API routes have error handling',
|
|
87
|
+
fail: 'API route will crash on error',
|
|
88
|
+
check(files) {
|
|
89
|
+
const findings = [];
|
|
90
|
+
for (const [fp, content] of files) {
|
|
91
|
+
if (/test|spec|mock/i.test(fp)) continue;
|
|
92
|
+
if (!fp.match(/\.(js|ts|jsx|tsx)$/)) continue;
|
|
93
|
+
if (!fp.match(/api|route|handler|controller/i)) continue;
|
|
94
|
+
const lines = content.split('\n');
|
|
95
|
+
for (let i = 0; i < lines.length; i++) {
|
|
96
|
+
if (/export\s+(?:async\s+)?function\s+(?:GET|POST|PUT|DELETE|PATCH)\b/.test(lines[i])) {
|
|
97
|
+
const body = lines.slice(i, Math.min(lines.length, i + 30)).join('\n');
|
|
98
|
+
if (!/try\s*\{|\.catch\s*\(/.test(body)) {
|
|
99
|
+
findings.push({ message: `${fp.split('/').pop()} has no error handling`, file: fp, line: i + 1 });
|
|
100
|
+
break; // one per file
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return findings;
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 'hardcoded-secrets',
|
|
110
|
+
name: 'Hardcoded Secrets',
|
|
111
|
+
icon: '🔒',
|
|
112
|
+
pass: 'No hardcoded passwords or secrets',
|
|
113
|
+
fail: 'Password or secret hardcoded in source',
|
|
114
|
+
check(files) {
|
|
115
|
+
const findings = [];
|
|
116
|
+
const pattern = /(?:password|passwd|pwd|secret|secret_key|jwt_secret)\s*[:=]\s*['"][^'"]{8,}['"]/i;
|
|
117
|
+
for (const [fp, content] of files) {
|
|
118
|
+
if (/test|spec|mock|example|sample|\.env/i.test(fp)) continue;
|
|
119
|
+
if (!fp.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php|yml|yaml|json)$/)) continue;
|
|
120
|
+
const lines = content.split('\n');
|
|
121
|
+
for (let i = 0; i < lines.length; i++) {
|
|
122
|
+
if (/^\s*(\/\/|#|\*|\/\*)/.test(lines[i])) continue;
|
|
123
|
+
if (pattern.test(lines[i]) && !/process\.env|os\.environ|ENV\[|config\.|getenv/i.test(lines[i])) {
|
|
124
|
+
findings.push({ message: 'Hardcoded secret', file: fp, line: i + 1 });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return findings;
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: 'eval-danger',
|
|
133
|
+
name: 'Code Execution',
|
|
134
|
+
icon: '⚠️',
|
|
135
|
+
pass: 'No dangerous eval() or exec() calls',
|
|
136
|
+
fail: 'eval() or exec() with dynamic input',
|
|
137
|
+
check(files) {
|
|
138
|
+
const findings = [];
|
|
139
|
+
for (const [fp, content] of files) {
|
|
140
|
+
if (/test|spec|mock/i.test(fp)) continue;
|
|
141
|
+
if (!fp.match(/\.(js|ts|jsx|tsx)$/)) continue;
|
|
142
|
+
const lines = content.split('\n');
|
|
143
|
+
for (let i = 0; i < lines.length; i++) {
|
|
144
|
+
if (/^\s*(\/\/|#|\*|\/\*)/.test(lines[i])) continue;
|
|
145
|
+
// Only flag eval() with variable input, not eval('literal')
|
|
146
|
+
if (/\beval\s*\(\s*(?!['"`])/.test(lines[i]) || /new\s+Function\s*\(\s*(?!['"`])/.test(lines[i])) {
|
|
147
|
+
findings.push({ message: 'eval() with dynamic input', file: fp, line: i + 1 });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return findings;
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'ai-cost-waste',
|
|
156
|
+
name: 'AI Cost Waste',
|
|
157
|
+
icon: '💰',
|
|
158
|
+
pass: 'AI API calls look efficient',
|
|
159
|
+
fail: 'AI API called without caching or limits',
|
|
160
|
+
check(files) {
|
|
161
|
+
const findings = [];
|
|
162
|
+
for (const [fp, content] of files) {
|
|
163
|
+
if (/test|spec|mock/i.test(fp)) continue;
|
|
164
|
+
if (!fp.match(/\.(js|ts|jsx|tsx)$/)) continue;
|
|
165
|
+
// Check for AI API calls without max_tokens
|
|
166
|
+
if (/openai|anthropic|chat\.completions\.create|messages\.create/i.test(content)) {
|
|
167
|
+
if (!/max_tokens|maxTokens|max_output_tokens/i.test(content)) {
|
|
168
|
+
findings.push({ message: 'AI API call without token limit', file: fp });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return findings;
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: 'debug-in-prod',
|
|
177
|
+
name: 'Debug Code Left In',
|
|
178
|
+
icon: '🐛',
|
|
179
|
+
pass: 'No debug code in production files',
|
|
180
|
+
fail: 'console.log or debug code left in',
|
|
181
|
+
check(files) {
|
|
182
|
+
const findings = [];
|
|
183
|
+
let count = 0;
|
|
184
|
+
for (const [fp, content] of files) {
|
|
185
|
+
if (/test|spec|mock/i.test(fp)) continue;
|
|
186
|
+
if (!fp.match(/\.(js|ts|jsx|tsx)$/)) continue;
|
|
187
|
+
if (fp.includes('logger') || fp.includes('debug') || fp.includes('config')) continue;
|
|
188
|
+
const lines = content.split('\n');
|
|
189
|
+
for (let i = 0; i < lines.length; i++) {
|
|
190
|
+
if (/console\.log\(/.test(lines[i]) && !/\/\//.test(lines[i].split('console')[0])) {
|
|
191
|
+
count++;
|
|
192
|
+
if (count <= 3) findings.push({ message: 'console.log left in code', file: fp, line: i + 1 });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (count > 3) findings.push({ message: `...and ${count - 3} more console.log calls` });
|
|
197
|
+
return findings;
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: 'open-database',
|
|
202
|
+
name: 'Database Security',
|
|
203
|
+
icon: '🗄️',
|
|
204
|
+
pass: 'Database configuration looks secure',
|
|
205
|
+
fail: 'Database may be publicly accessible',
|
|
206
|
+
check(files) {
|
|
207
|
+
const findings = [];
|
|
208
|
+
for (const [fp, content] of files) {
|
|
209
|
+
if (/test|spec|mock/i.test(fp)) continue;
|
|
210
|
+
// MongoDB without auth
|
|
211
|
+
if (/mongodb:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(?::\d+)?\/\w+['"]/.test(content) && !/authSource|user.*password|mongodb\+srv/i.test(content)) {
|
|
212
|
+
findings.push({ message: 'MongoDB connection without authentication', file: fp });
|
|
213
|
+
}
|
|
214
|
+
// Binding to all interfaces
|
|
215
|
+
if (/\.listen\(\s*\d+\s*,\s*['"]0\.0\.0\.0['"]/.test(content)) {
|
|
216
|
+
findings.push({ message: 'Server bound to all network interfaces (0.0.0.0)', file: fp });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return findings;
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: 'outdated-deps',
|
|
224
|
+
name: 'Dangerous Dependencies',
|
|
225
|
+
icon: '📦',
|
|
226
|
+
pass: 'No known vulnerable packages',
|
|
227
|
+
fail: 'Package with known security issue',
|
|
228
|
+
check(files) {
|
|
229
|
+
const findings = [];
|
|
230
|
+
const dangerous = {
|
|
231
|
+
'event-stream': 'Compromised package — cryptocurrency theft (2018)',
|
|
232
|
+
'flatmap-stream': 'Malicious package — injected into event-stream',
|
|
233
|
+
'ua-parser-js': 'Versions <0.7.31 — cryptocurrency miner injected',
|
|
234
|
+
'colors': 'v1.4.1+ — intentional sabotage (infinite loop)',
|
|
235
|
+
'faker': 'v6+ — intentional sabotage (random data)',
|
|
236
|
+
'node-ipc': 'v10.1.1+ — intentional destructive code',
|
|
237
|
+
'node-serialize': 'All versions — RCE via deserialization',
|
|
238
|
+
'serialize-to-js': 'RCE via deserialization',
|
|
239
|
+
};
|
|
240
|
+
const pkgJson = files.get('package.json');
|
|
241
|
+
if (pkgJson) {
|
|
242
|
+
try {
|
|
243
|
+
const pkg = JSON.parse(pkgJson);
|
|
244
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
245
|
+
for (const [name, version] of Object.entries(allDeps)) {
|
|
246
|
+
if (dangerous[name]) {
|
|
247
|
+
findings.push({ message: `${name}: ${dangerous[name]}`, file: 'package.json' });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} catch {}
|
|
251
|
+
}
|
|
252
|
+
return findings;
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
export default CHECKS;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import CHECKS from './simple-checks.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Run all simple checks and print clean output.
|
|
6
|
+
* Returns { passed, failed, issues }
|
|
7
|
+
*/
|
|
8
|
+
export function runSimpleChecks(files) {
|
|
9
|
+
const results = [];
|
|
10
|
+
|
|
11
|
+
for (const check of CHECKS) {
|
|
12
|
+
const findings = check.check(files);
|
|
13
|
+
results.push({
|
|
14
|
+
...check,
|
|
15
|
+
findings,
|
|
16
|
+
passed: findings.length === 0,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return results;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function printSimpleReport(results) {
|
|
24
|
+
console.log('');
|
|
25
|
+
console.log(chalk.bold(' Doorman'));
|
|
26
|
+
console.log('');
|
|
27
|
+
|
|
28
|
+
let passCount = 0;
|
|
29
|
+
let failCount = 0;
|
|
30
|
+
const issues = [];
|
|
31
|
+
|
|
32
|
+
for (const r of results) {
|
|
33
|
+
if (r.passed) {
|
|
34
|
+
passCount++;
|
|
35
|
+
console.log(chalk.green(` ✓ ${r.name}`));
|
|
36
|
+
} else {
|
|
37
|
+
failCount++;
|
|
38
|
+
console.log(chalk.red(` ✗ ${r.name}`));
|
|
39
|
+
for (const f of r.findings.slice(0, 3)) {
|
|
40
|
+
const loc = f.file ? chalk.gray(` — ${f.file}${f.line ? ':' + f.line : ''}`) : '';
|
|
41
|
+
console.log(chalk.gray(` ${f.message}${loc}`));
|
|
42
|
+
issues.push(`${r.name}: ${f.message}${f.file ? ' in ' + f.file + (f.line ? ':' + f.line : '') : ''}`);
|
|
43
|
+
}
|
|
44
|
+
if (r.findings.length > 3) {
|
|
45
|
+
console.log(chalk.gray(` ...and ${r.findings.length - 3} more`));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log('');
|
|
51
|
+
if (failCount === 0) {
|
|
52
|
+
console.log(chalk.green.bold(' All clear. Ship it.'));
|
|
53
|
+
} else {
|
|
54
|
+
console.log(chalk.yellow(` ${failCount} issue${failCount === 1 ? '' : 's'} to fix before shipping.`));
|
|
55
|
+
console.log(chalk.gray(' Run `npx getdoorman fix` to generate a prompt for Claude/Codex.'));
|
|
56
|
+
}
|
|
57
|
+
console.log('');
|
|
58
|
+
|
|
59
|
+
return { passCount, failCount, issues };
|
|
60
|
+
}
|