deltarq-scan 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +210 -0
- package/bin/deltarq-scan.js +198 -0
- package/logo.png +0 -0
- package/package.json +50 -0
- package/src/engine/aggregator.js +96 -0
- package/src/engine/anonymizer.js +79 -0
- package/src/output/terminal.js +218 -0
- package/src/output/uploader.js +122 -0
- package/src/rules/data.js +24 -0
- package/src/rules/git.js +14 -0
- package/src/rules/identity.js +34 -0
- package/src/rules/index.js +53 -0
- package/src/rules/infrastructure.js +44 -0
- package/src/rules/logging.js +14 -0
- package/src/scanner/awsScanner.js +138 -0
- package/src/scanner/dbScanner.js +67 -0
- package/src/scanner/fileScanner.js +422 -0
- package/src/scanner/gitScanner.js +115 -0
- package/src/utils/detect.js +90 -0
- package/src/utils/fileUtils.js +88 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal Reporter — Rich chalk-based terminal output
|
|
3
|
+
* Renders the branded DELTARQ scan report with colors, tables, and icons
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import Table from 'cli-table3';
|
|
8
|
+
import { getRule, SEVERITY_ORDER } from '../rules/index.js';
|
|
9
|
+
import { getComplianceGapCount, estimateRemediationEffort } from '../engine/aggregator.js';
|
|
10
|
+
|
|
11
|
+
// Color scheme
|
|
12
|
+
const COLORS = {
|
|
13
|
+
brand: chalk.hex('#6C5CE7'), // DELTARQ purple
|
|
14
|
+
brandBold: chalk.hex('#6C5CE7').bold,
|
|
15
|
+
critical: chalk.red.bold,
|
|
16
|
+
high: chalk.hex('#FF6B35').bold, // Orange
|
|
17
|
+
medium: chalk.yellow.bold,
|
|
18
|
+
drift: chalk.blue.bold,
|
|
19
|
+
pass: chalk.green.bold,
|
|
20
|
+
dim: chalk.dim,
|
|
21
|
+
white: chalk.white,
|
|
22
|
+
whiteBold: chalk.white.bold,
|
|
23
|
+
score: {
|
|
24
|
+
excellent: chalk.green.bold,
|
|
25
|
+
acceptable: chalk.green,
|
|
26
|
+
atRisk: chalk.yellow.bold,
|
|
27
|
+
vulnerable: chalk.hex('#FF6B35').bold,
|
|
28
|
+
critical: chalk.red.bold,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const SEVERITY_ICONS = {
|
|
33
|
+
CRITICAL: '🔴',
|
|
34
|
+
HIGH: '🟠',
|
|
35
|
+
MEDIUM: '🟡',
|
|
36
|
+
DRIFT: '🔵',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const SEVERITY_COLORS = {
|
|
40
|
+
CRITICAL: COLORS.critical,
|
|
41
|
+
HIGH: COLORS.high,
|
|
42
|
+
MEDIUM: COLORS.medium,
|
|
43
|
+
DRIFT: COLORS.drift,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Print the branded header banner
|
|
48
|
+
*/
|
|
49
|
+
export function printBanner(targetDir) {
|
|
50
|
+
const line = COLORS.brand('─'.repeat(56));
|
|
51
|
+
console.log();
|
|
52
|
+
console.log(line);
|
|
53
|
+
console.log(COLORS.brandBold(' DELTARQ Security Scanner') + COLORS.dim(' v0.1.0'));
|
|
54
|
+
console.log(COLORS.dim(` Scanning: ${targetDir}`));
|
|
55
|
+
console.log(line);
|
|
56
|
+
console.log();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Print a scan phase result line
|
|
61
|
+
* @param {string} label - Phase description
|
|
62
|
+
* @param {string} result - Result text
|
|
63
|
+
* @param {'ok'|'warning'|'error'|'skip'} status - Result status
|
|
64
|
+
*/
|
|
65
|
+
export function printScanPhase(label, result, status = 'ok') {
|
|
66
|
+
const icons = {
|
|
67
|
+
ok: chalk.green('✓'),
|
|
68
|
+
warning: chalk.yellow('⚠'),
|
|
69
|
+
error: chalk.red('✗'),
|
|
70
|
+
skip: chalk.dim('○'),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const icon = icons[status] || icons.ok;
|
|
74
|
+
const labelPadded = label.padEnd(35);
|
|
75
|
+
const resultColor = status === 'error' ? COLORS.critical :
|
|
76
|
+
status === 'warning' ? COLORS.medium :
|
|
77
|
+
status === 'skip' ? COLORS.dim :
|
|
78
|
+
COLORS.pass;
|
|
79
|
+
|
|
80
|
+
console.log(` ${icon} ${COLORS.white(labelPadded)} ${resultColor(result)}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Print the full scan report
|
|
85
|
+
*/
|
|
86
|
+
export function printReport(findings, scoreResult, projectInfo) {
|
|
87
|
+
const line = COLORS.brand('─'.repeat(56));
|
|
88
|
+
const failedFindings = findings.filter(f => !f.passed);
|
|
89
|
+
|
|
90
|
+
// Deduplicate by rule ID for display
|
|
91
|
+
const seenRules = new Set();
|
|
92
|
+
const uniqueFindings = [];
|
|
93
|
+
for (const f of failedFindings) {
|
|
94
|
+
if (!seenRules.has(f.rule)) {
|
|
95
|
+
seenRules.add(f.rule);
|
|
96
|
+
uniqueFindings.push(f);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Sort by severity
|
|
101
|
+
uniqueFindings.sort((a, b) => {
|
|
102
|
+
return SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
console.log();
|
|
106
|
+
console.log(line);
|
|
107
|
+
|
|
108
|
+
// Score display
|
|
109
|
+
const scoreColor = getScoreColor(scoreResult.score);
|
|
110
|
+
console.log(
|
|
111
|
+
` YOUR SECURITY SCORE: ${scoreColor(`${scoreResult.score}/100`)} ` +
|
|
112
|
+
`[${scoreColor(`${scoreResult.grade} — ${scoreResult.label}`)}]`
|
|
113
|
+
);
|
|
114
|
+
console.log(line);
|
|
115
|
+
console.log();
|
|
116
|
+
|
|
117
|
+
// Findings
|
|
118
|
+
if (uniqueFindings.length === 0) {
|
|
119
|
+
console.log(COLORS.pass(' ✓ No security issues detected! Your project looks solid.'));
|
|
120
|
+
console.log();
|
|
121
|
+
} else {
|
|
122
|
+
for (const finding of uniqueFindings) {
|
|
123
|
+
const icon = SEVERITY_ICONS[finding.severity] || '⚪';
|
|
124
|
+
const sevColor = SEVERITY_COLORS[finding.severity] || COLORS.dim;
|
|
125
|
+
const rule = getRule(finding.rule);
|
|
126
|
+
const ruleName = rule ? rule.name : finding.rule;
|
|
127
|
+
const ruleDesc = rule ? rule.description : finding.detail;
|
|
128
|
+
|
|
129
|
+
console.log(
|
|
130
|
+
` ${icon} ${sevColor(finding.severity.padEnd(10))} ${COLORS.whiteBold(finding.rule)} ${COLORS.white(ruleName)}`
|
|
131
|
+
);
|
|
132
|
+
console.log(
|
|
133
|
+
` ${' '.repeat(3)} ${' '.repeat(10)} ${COLORS.dim('→')} ${COLORS.dim(ruleDesc)}`
|
|
134
|
+
);
|
|
135
|
+
console.log();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Footer stats
|
|
140
|
+
console.log(line);
|
|
141
|
+
const gapCount = getComplianceGapCount(findings);
|
|
142
|
+
const effort = estimateRemediationEffort(findings);
|
|
143
|
+
|
|
144
|
+
const readyIcon = scoreResult.enterpriseReady ? COLORS.pass('✓') : COLORS.critical('✗');
|
|
145
|
+
const readyLabel = scoreResult.enterpriseReady
|
|
146
|
+
? COLORS.pass('Ready')
|
|
147
|
+
: COLORS.critical('Not Ready');
|
|
148
|
+
|
|
149
|
+
console.log(` ENTERPRISE READINESS: ${readyIcon} ${readyLabel}`);
|
|
150
|
+
console.log(` SOC 2 Gap Count: ${COLORS.whiteBold(String(gapCount) + ' controls failing')}`);
|
|
151
|
+
console.log(` Estimated remediation effort: ${COLORS.whiteBold(effort)}`);
|
|
152
|
+
console.log(line);
|
|
153
|
+
console.log();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Print the summary table (alternative compact view)
|
|
158
|
+
*/
|
|
159
|
+
export function printSummaryTable(findings) {
|
|
160
|
+
const failedFindings = findings.filter(f => !f.passed);
|
|
161
|
+
const seenRules = new Set();
|
|
162
|
+
const unique = [];
|
|
163
|
+
for (const f of failedFindings) {
|
|
164
|
+
if (!seenRules.has(f.rule)) {
|
|
165
|
+
seenRules.add(f.rule);
|
|
166
|
+
unique.push(f);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (unique.length === 0) return;
|
|
171
|
+
|
|
172
|
+
const table = new Table({
|
|
173
|
+
head: [
|
|
174
|
+
chalk.white.bold('Severity'),
|
|
175
|
+
chalk.white.bold('Rule'),
|
|
176
|
+
chalk.white.bold('Issue'),
|
|
177
|
+
chalk.white.bold('Status'),
|
|
178
|
+
],
|
|
179
|
+
colWidths: [12, 12, 40, 10],
|
|
180
|
+
style: {
|
|
181
|
+
head: [],
|
|
182
|
+
border: ['dim'],
|
|
183
|
+
},
|
|
184
|
+
chars: {
|
|
185
|
+
'top': '─', 'top-mid': '┬', 'top-left': '┌', 'top-right': '┐',
|
|
186
|
+
'bottom': '─', 'bottom-mid': '┴', 'bottom-left': '└', 'bottom-right': '┘',
|
|
187
|
+
'left': '│', 'left-mid': '├', 'mid': '─', 'mid-mid': '┼',
|
|
188
|
+
'right': '│', 'right-mid': '┤', 'middle': '│',
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
for (const f of unique) {
|
|
193
|
+
const rule = getRule(f.rule);
|
|
194
|
+
const sevColor = SEVERITY_COLORS[f.severity] || COLORS.dim;
|
|
195
|
+
const icon = SEVERITY_ICONS[f.severity] || '⚪';
|
|
196
|
+
|
|
197
|
+
table.push([
|
|
198
|
+
`${icon} ${sevColor(f.severity)}`,
|
|
199
|
+
COLORS.whiteBold(f.rule),
|
|
200
|
+
COLORS.white(rule ? rule.name : f.detail),
|
|
201
|
+
COLORS.critical('FAIL'),
|
|
202
|
+
]);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log(table.toString());
|
|
206
|
+
console.log();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get the color function for a score value
|
|
211
|
+
*/
|
|
212
|
+
function getScoreColor(score) {
|
|
213
|
+
if (score >= 90) return COLORS.score.excellent;
|
|
214
|
+
if (score >= 80) return COLORS.score.acceptable;
|
|
215
|
+
if (score >= 60) return COLORS.score.atRisk;
|
|
216
|
+
if (score >= 40) return COLORS.score.vulnerable;
|
|
217
|
+
return COLORS.score.critical;
|
|
218
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uploader — Handles anonymous scan data upload to DELTARQ dashboard
|
|
3
|
+
* Includes consent prompt and transparent preview before any data leaves
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import readline from 'readline';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { validateNoSecrets } from '../engine/anonymizer.js';
|
|
9
|
+
|
|
10
|
+
const DELTARQ_API_URL = process.env.DELTARQ_API_URL || 'https://deltarq-scan.vercel.app/v1/scans';
|
|
11
|
+
const DELTARQ_DASHBOARD_URL = process.env.DELTARQ_DASHBOARD_URL || 'https://deltarq-scan.vercel.app';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Prompt the user for consent and upload anonymous scan results
|
|
15
|
+
* @param {Object} anonymousPayload - The anonymized scan JSON
|
|
16
|
+
* @param {{ noUpload: boolean }} options - CLI options
|
|
17
|
+
* @returns {Promise<{ uploaded: boolean, reportUrl?: string }>}
|
|
18
|
+
*/
|
|
19
|
+
export async function uploadWithConsent(anonymousPayload, options = {}) {
|
|
20
|
+
if (options.noUpload) {
|
|
21
|
+
return { uploaded: false };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log();
|
|
25
|
+
console.log(chalk.hex('#6C5CE7').bold(' Would you like to see your full report on the DELTARQ dashboard?'));
|
|
26
|
+
console.log(chalk.dim(' (No sensitive data is uploaded — only boolean pass/fail metadata)'));
|
|
27
|
+
console.log();
|
|
28
|
+
|
|
29
|
+
// Show preview of what will be uploaded
|
|
30
|
+
if (options.verbose) {
|
|
31
|
+
console.log(chalk.dim(' Preview of upload payload:'));
|
|
32
|
+
console.log(chalk.dim(' ' + JSON.stringify(anonymousPayload, null, 2).split('\n').join('\n ')));
|
|
33
|
+
console.log();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const consent = await askYesNo(' [Y/n]: ');
|
|
37
|
+
|
|
38
|
+
if (!consent) {
|
|
39
|
+
console.log();
|
|
40
|
+
console.log(chalk.dim(' Skipped upload. Your scan results are only on this machine.'));
|
|
41
|
+
return { uploaded: false };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Safety check before upload
|
|
45
|
+
try {
|
|
46
|
+
validateNoSecrets(anonymousPayload);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.log();
|
|
49
|
+
console.log(chalk.red.bold(` ✗ ${err.message}`));
|
|
50
|
+
return { uploaded: false, error: err.message };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Perform the upload
|
|
54
|
+
console.log();
|
|
55
|
+
process.stdout.write(chalk.dim(' Uploading anonymous scan metadata... '));
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const response = await fetch(DELTARQ_API_URL, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: {
|
|
61
|
+
'Content-Type': 'application/json',
|
|
62
|
+
'User-Agent': `deltarq-scan/${anonymousPayload.scanner_version}`,
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify(anonymousPayload),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
// Non-critical failure — the scan still ran successfully
|
|
69
|
+
console.log(chalk.yellow('⚠ Upload failed (non-critical)'));
|
|
70
|
+
console.log(chalk.dim(` Server responded with status ${response.status}`));
|
|
71
|
+
return { uploaded: false };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const result = await response.json();
|
|
75
|
+
const reportUrl = result.report_url || `${DELTARQ_DASHBOARD_URL}/report/${anonymousPayload.scan_id}`;
|
|
76
|
+
|
|
77
|
+
console.log(chalk.green('✓ Done'));
|
|
78
|
+
console.log();
|
|
79
|
+
console.log(chalk.white(' Your report is live at:'));
|
|
80
|
+
console.log(chalk.hex('#6C5CE7').bold(` ${reportUrl}`));
|
|
81
|
+
console.log();
|
|
82
|
+
console.log(chalk.white(' Book a 20-min call to fix these gaps with a DELTARQ engineer:'));
|
|
83
|
+
console.log(chalk.hex('#6C5CE7').bold(' → https://calendar.app.google/ExhxfcYvbV5PKMs36'));
|
|
84
|
+
console.log();
|
|
85
|
+
|
|
86
|
+
return { uploaded: true, reportUrl };
|
|
87
|
+
|
|
88
|
+
} catch (err) {
|
|
89
|
+
// Network error — API might be down or no internet
|
|
90
|
+
console.log(chalk.yellow('⚠ Upload failed'));
|
|
91
|
+
console.log(chalk.dim(` ${err.message}`));
|
|
92
|
+
console.log(chalk.dim(' Your scan results are still available locally.'));
|
|
93
|
+
console.log();
|
|
94
|
+
|
|
95
|
+
// Still show the CTA
|
|
96
|
+
console.log(chalk.white(' Book a 20-min call to fix these gaps with a DELTARQ engineer:'));
|
|
97
|
+
console.log(chalk.hex('#6C5CE7').bold(' → https://calendar.app.google/ExhxfcYvbV5PKMs36'));
|
|
98
|
+
console.log();
|
|
99
|
+
|
|
100
|
+
return { uploaded: false };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Ask a yes/no question in the terminal
|
|
106
|
+
* @param {string} prompt - The prompt text
|
|
107
|
+
* @returns {Promise<boolean>}
|
|
108
|
+
*/
|
|
109
|
+
function askYesNo(prompt) {
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
const rl = readline.createInterface({
|
|
112
|
+
input: process.stdin,
|
|
113
|
+
output: process.stdout,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
rl.question(prompt, (answer) => {
|
|
117
|
+
rl.close();
|
|
118
|
+
const normalized = answer.trim().toLowerCase();
|
|
119
|
+
resolve(normalized === '' || normalized === 'y' || normalized === 'yes');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data & Database security rules
|
|
3
|
+
* Checks database configuration for encryption, access, and resilience
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const DB_001 = {
|
|
7
|
+
id: 'DB-001',
|
|
8
|
+
name: 'Postgres No SSL + Public Endpoint',
|
|
9
|
+
severity: 'CRITICAL',
|
|
10
|
+
category: 'Data',
|
|
11
|
+
description: 'Your DB connection is unencrypted on a public-reachable host. MITM attack can dump your entire user table in transit.',
|
|
12
|
+
remediation: 'Enable sslmode=require in your DATABASE_URL and ensure the host is not publicly accessible.',
|
|
13
|
+
sopLink: 'https://www.postgresql.org/docs/current/ssl-tcp.html',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const DB_002 = {
|
|
17
|
+
id: 'DB-002',
|
|
18
|
+
name: 'Connection Pool — No Max Limit',
|
|
19
|
+
severity: 'MEDIUM',
|
|
20
|
+
category: 'Data',
|
|
21
|
+
description: 'Unbounded connection pools under load = DB crash. Also a common DoS attack vector.',
|
|
22
|
+
remediation: 'Set a pool.max or pool.size limit in your database configuration (recommended: 10-20 for small apps).',
|
|
23
|
+
sopLink: 'https://node-postgres.com/apis/pool',
|
|
24
|
+
};
|
package/src/rules/git.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Configuration & Secret Exposure rules
|
|
3
|
+
* Checks for gitignore configuration and git history leaks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const GIT_001 = {
|
|
7
|
+
id: 'GIT-001',
|
|
8
|
+
name: 'Git Secret & Config Exposure',
|
|
9
|
+
severity: 'MEDIUM',
|
|
10
|
+
category: 'Git Configuration',
|
|
11
|
+
description: 'Secrets or sensitive files are tracked in git or not excluded in .gitignore, exposing credentials in repository history.',
|
|
12
|
+
remediation: 'Add .env and credential files to .gitignore and purge any existing secrets from git history using git-filter-repo.',
|
|
13
|
+
sopLink: 'https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning',
|
|
14
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity & Access Management rules
|
|
3
|
+
* Checks IAM policies and credential management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const IAM_001 = {
|
|
7
|
+
id: 'IAM-001',
|
|
8
|
+
name: 'Wildcard IAM Policy',
|
|
9
|
+
severity: 'CRITICAL',
|
|
10
|
+
category: 'Identity',
|
|
11
|
+
description: 'Your IAM policy grants God-mode access. Any compromised Lambda = full account takeover.',
|
|
12
|
+
remediation: 'Apply least-privilege IAM policies. Replace "Action": "*" with specific service actions.',
|
|
13
|
+
sopLink: 'https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const IAM_002 = {
|
|
17
|
+
id: 'IAM-002',
|
|
18
|
+
name: 'Hardcoded Cloud/API Secrets',
|
|
19
|
+
severity: 'HIGH',
|
|
20
|
+
category: 'Identity',
|
|
21
|
+
description: 'Hardcoded API tokens or cloud keys with no rotation = one git leak away from a massive breach or financial loss.',
|
|
22
|
+
remediation: 'Use IAM roles, environment-based credentials, or secret managers (like AWS Secrets Manager or Vault) instead of hardcoding keys.',
|
|
23
|
+
sopLink: 'https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const IAM_003 = {
|
|
27
|
+
id: 'IAM-003',
|
|
28
|
+
name: 'No MFA on Root Account',
|
|
29
|
+
severity: 'MEDIUM',
|
|
30
|
+
category: 'Identity',
|
|
31
|
+
description: 'Your AWS root account has no MFA. This is the master key to your entire infrastructure.',
|
|
32
|
+
remediation: 'Enable MFA on the AWS root account immediately.',
|
|
33
|
+
sopLink: 'https://docs.aws.amazon.com/IAM/latest/UserGuide/id_root-user.html#id_root-user_manage_mfa',
|
|
34
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule Registry — Central index of all scan rules
|
|
3
|
+
* Import and re-export all rules for the scanner engine
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { INFRA_001, INFRA_002, INFRA_003, INFRA_004 } from './infrastructure.js';
|
|
7
|
+
import { IAM_001, IAM_002, IAM_003 } from './identity.js';
|
|
8
|
+
import { DB_001, DB_002 } from './data.js';
|
|
9
|
+
import { LOG_001 } from './logging.js';
|
|
10
|
+
import { GIT_001 } from './git.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* All registered rules, indexed by ID
|
|
14
|
+
*/
|
|
15
|
+
export const RULES = {
|
|
16
|
+
'INFRA-001': INFRA_001,
|
|
17
|
+
'INFRA-002': INFRA_002,
|
|
18
|
+
'INFRA-003': INFRA_003,
|
|
19
|
+
'INFRA-004': INFRA_004,
|
|
20
|
+
'IAM-001': IAM_001,
|
|
21
|
+
'IAM-002': IAM_002,
|
|
22
|
+
'IAM-003': IAM_003,
|
|
23
|
+
'DB-001': DB_001,
|
|
24
|
+
'DB-002': DB_002,
|
|
25
|
+
'LOG-001': LOG_001,
|
|
26
|
+
'GIT-001': GIT_001,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get all rules as an array
|
|
31
|
+
*/
|
|
32
|
+
export function getAllRules() {
|
|
33
|
+
return Object.values(RULES);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get a rule by its ID
|
|
38
|
+
*/
|
|
39
|
+
export function getRule(id) {
|
|
40
|
+
return RULES[id] || null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get rules filtered by severity
|
|
45
|
+
*/
|
|
46
|
+
export function getRulesBySeverity(severity) {
|
|
47
|
+
return getAllRules().filter(r => r.severity === severity);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Severity levels in order of priority
|
|
52
|
+
*/
|
|
53
|
+
export const SEVERITY_ORDER = ['CRITICAL', 'HIGH', 'MEDIUM', 'DRIFT'];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure security rules
|
|
3
|
+
* Checks Docker and container configuration for security gaps
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const INFRA_001 = {
|
|
7
|
+
id: 'INFRA-001',
|
|
8
|
+
name: 'Docker Running as Root',
|
|
9
|
+
severity: 'CRITICAL',
|
|
10
|
+
category: 'Infrastructure',
|
|
11
|
+
description: 'Your containers run as root. Container escape = full host compromise.',
|
|
12
|
+
remediation: 'Add a USER directive in your Dockerfile to run as a non-root user.',
|
|
13
|
+
sopLink: 'https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#user',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const INFRA_002 = {
|
|
17
|
+
id: 'INFRA-002',
|
|
18
|
+
name: 'Database Port Exposed to All Interfaces',
|
|
19
|
+
severity: 'HIGH',
|
|
20
|
+
category: 'Infrastructure',
|
|
21
|
+
description: 'Your Postgres port is exposed to the open internet via Docker. Shodan already knows about hosts like yours.',
|
|
22
|
+
remediation: 'Bind database ports to 127.0.0.1 only (e.g., "127.0.0.1:5432:5432").',
|
|
23
|
+
sopLink: 'https://docs.docker.com/compose/networking/',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const INFRA_003 = {
|
|
27
|
+
id: 'INFRA-003',
|
|
28
|
+
name: 'Missing Lockfile (Supply Chain)',
|
|
29
|
+
severity: 'HIGH',
|
|
30
|
+
category: 'Infrastructure',
|
|
31
|
+
description: 'No package-lock.json or yarn.lock found. This violates SOC 2 requirements for deterministic, reproducible builds and leaves you vulnerable to dependency hijacking.',
|
|
32
|
+
remediation: 'Generate and commit a lockfile using your package manager (e.g., run npm install).',
|
|
33
|
+
sopLink: 'https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const INFRA_004 = {
|
|
37
|
+
id: 'INFRA-004',
|
|
38
|
+
name: 'Insecure CI/CD Pipeline',
|
|
39
|
+
severity: 'CRITICAL',
|
|
40
|
+
category: 'Infrastructure',
|
|
41
|
+
description: 'Your GitHub Actions workflow uses dangerous triggers (like pull_request_target). Malicious PRs could steal your repository secrets or tamper with your releases.',
|
|
42
|
+
remediation: 'Remove the pull_request_target trigger, or carefully gate access using environment protection rules.',
|
|
43
|
+
sopLink: 'https://securitylab.github.com/research/github-actions-preventing-pwn-requests/',
|
|
44
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging & Audit Trail rules
|
|
3
|
+
* Checks for structured logging and audit capabilities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const LOG_001 = {
|
|
7
|
+
id: 'LOG-001',
|
|
8
|
+
name: 'No Structured Audit Trail',
|
|
9
|
+
severity: 'HIGH',
|
|
10
|
+
category: 'Logging',
|
|
11
|
+
description: 'You have no audit trail. If you\'re breached, you won\'t know who accessed what or when. SOC 2 Type II requires this.',
|
|
12
|
+
remediation: 'Implement structured logging with a library like Winston, Pino, or Morgan for access/auth events.',
|
|
13
|
+
sopLink: 'https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html',
|
|
14
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS Scanner — Checks AWS IAM configuration for security issues
|
|
3
|
+
* Uses locally configured ~/.aws/credentials (read-only)
|
|
4
|
+
*
|
|
5
|
+
* Day 1-2: Local file checks only
|
|
6
|
+
* Day 3: AWS SDK integration for live API checks (MFA, CloudTrail, IAM drift)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import { readFileSafe, findFiles } from '../utils/fileUtils.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Run AWS-related security scans
|
|
15
|
+
* @param {string} targetDir - The project root directory
|
|
16
|
+
* @returns {Promise<{ findings: Array, awsConfigured: boolean }>}
|
|
17
|
+
*/
|
|
18
|
+
export async function runAwsScanner(targetDir) {
|
|
19
|
+
const findings = [];
|
|
20
|
+
|
|
21
|
+
// Check if AWS credentials exist locally
|
|
22
|
+
const awsDir = path.join(os.homedir(), '.aws');
|
|
23
|
+
const credentialsFile = readFileSafe(path.join(awsDir, 'credentials'));
|
|
24
|
+
const configFile = readFileSafe(path.join(awsDir, 'config'));
|
|
25
|
+
|
|
26
|
+
const awsConfigured = !!(credentialsFile || configFile);
|
|
27
|
+
|
|
28
|
+
// IAM-001: Scan for wildcard IAM policies in the project
|
|
29
|
+
const policyFindings = await scanIAMPolicies(targetDir);
|
|
30
|
+
findings.push(...policyFindings);
|
|
31
|
+
|
|
32
|
+
// --- Day 3: AWS SDK Live Checks (stubbed for now) ---
|
|
33
|
+
// IAM-003: MFA on root account
|
|
34
|
+
// DRIFT-001: New IAM users without MFA
|
|
35
|
+
// DRIFT-002: CloudTrail status
|
|
36
|
+
|
|
37
|
+
if (awsConfigured) {
|
|
38
|
+
// Placeholder for live AWS API checks
|
|
39
|
+
// These will use AWS SDK v3 when integrated
|
|
40
|
+
/*
|
|
41
|
+
try {
|
|
42
|
+
const mfaFinding = await checkRootMFA();
|
|
43
|
+
if (mfaFinding) findings.push(mfaFinding);
|
|
44
|
+
|
|
45
|
+
const driftFindings = await checkIAMDrift();
|
|
46
|
+
findings.push(...driftFindings);
|
|
47
|
+
|
|
48
|
+
const trailFinding = await checkCloudTrail();
|
|
49
|
+
if (trailFinding) findings.push(trailFinding);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
// AWS API call failed — permissions might be insufficient
|
|
52
|
+
}
|
|
53
|
+
*/
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { findings, awsConfigured };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Scan project files for IAM policy documents with overly permissive rules
|
|
61
|
+
*/
|
|
62
|
+
async function scanIAMPolicies(targetDir) {
|
|
63
|
+
const findings = [];
|
|
64
|
+
|
|
65
|
+
// Look for IAM policy JSON files
|
|
66
|
+
const policyFiles = await findFiles(targetDir, [
|
|
67
|
+
'**/*policy*.json',
|
|
68
|
+
'**/*iam*.json',
|
|
69
|
+
'**/trust-policy*.json',
|
|
70
|
+
'**/role*.json',
|
|
71
|
+
'**/*permissions*.json',
|
|
72
|
+
'**/serverless.yml',
|
|
73
|
+
'**/serverless.yaml',
|
|
74
|
+
'**/template.yml',
|
|
75
|
+
'**/template.yaml',
|
|
76
|
+
'**/sam.yml',
|
|
77
|
+
'**/sam.yaml',
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
for (const policyFile of policyFiles) {
|
|
81
|
+
const content = readFileSafe(policyFile);
|
|
82
|
+
if (!content) continue;
|
|
83
|
+
|
|
84
|
+
const relPath = path.relative(targetDir, policyFile);
|
|
85
|
+
|
|
86
|
+
// Check JSON policy files
|
|
87
|
+
if (policyFile.endsWith('.json')) {
|
|
88
|
+
try {
|
|
89
|
+
const policy = JSON.parse(content);
|
|
90
|
+
const statements = policy.Statement || (policy.PolicyDocument && policy.PolicyDocument.Statement) || [];
|
|
91
|
+
|
|
92
|
+
for (const stmt of statements) {
|
|
93
|
+
const actions = Array.isArray(stmt.Action) ? stmt.Action : [stmt.Action];
|
|
94
|
+
const resources = Array.isArray(stmt.Resource) ? stmt.Resource : [stmt.Resource];
|
|
95
|
+
|
|
96
|
+
const hasWildcardAction = actions.some(a => a === '*');
|
|
97
|
+
const hasWildcardResource = resources.some(r => r === '*');
|
|
98
|
+
|
|
99
|
+
if (hasWildcardAction && hasWildcardResource && stmt.Effect === 'Allow') {
|
|
100
|
+
findings.push({
|
|
101
|
+
rule: 'IAM-001',
|
|
102
|
+
severity: 'CRITICAL',
|
|
103
|
+
passed: false,
|
|
104
|
+
detail: `Wildcard IAM policy (Action:* + Resource:*) found in ${relPath}`,
|
|
105
|
+
file: relPath,
|
|
106
|
+
});
|
|
107
|
+
break; // One finding per file is enough
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// Not valid JSON or unexpected structure
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check YAML serverless/SAM templates for wildcard policies
|
|
116
|
+
if (policyFile.endsWith('.yml') || policyFile.endsWith('.yaml')) {
|
|
117
|
+
if (content.includes('"*"') || content.includes("'*'")) {
|
|
118
|
+
// Rough check — look for Action: "*" and Resource: "*" patterns
|
|
119
|
+
const hasWildcardAction = content.includes('Action') &&
|
|
120
|
+
(content.includes("'*'") || content.includes('"*"'));
|
|
121
|
+
const hasWildcardResource = content.includes('Resource') &&
|
|
122
|
+
(content.includes("'*'") || content.includes('"*"'));
|
|
123
|
+
|
|
124
|
+
if (hasWildcardAction && hasWildcardResource) {
|
|
125
|
+
findings.push({
|
|
126
|
+
rule: 'IAM-001',
|
|
127
|
+
severity: 'CRITICAL',
|
|
128
|
+
passed: false,
|
|
129
|
+
detail: `Wildcard IAM policy detected in ${relPath} (serverless/SAM template)`,
|
|
130
|
+
file: relPath,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return findings;
|
|
138
|
+
}
|