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.
@@ -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
+ };
@@ -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
+ }