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 ADDED
@@ -0,0 +1,210 @@
1
+ # deltarq-scan
2
+
3
+ **Scan your startup's codebase and infrastructure for security gaps in 30 seconds.**
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-6C5CE7.svg)](https://opensource.org/licenses/MIT)
6
+ [![Node.js](https://img.shields.io/badge/Node.js-18%2B-339933.svg)](https://nodejs.org/)
7
+
8
+ ---
9
+
10
+ ```bash
11
+ npx deltarq-scan
12
+ ```
13
+
14
+ `deltarq-scan` is a **local-first, zero-install, read-only** CLI security audit tool. It scans your repository (configurations, environment variables, IAM policies, git history) for critical security gaps that block enterprise deals, fail SOC 2 audits, or expose you to immediate breach.
15
+
16
+ ---
17
+
18
+ ## What it checks
19
+
20
+ | Rule | Severity | What it catches |
21
+ |------|----------|----------------|
22
+ | `IAM-001` | 🔴 CRITICAL | Wildcard IAM policies (`Action: *`, `Resource: *`) |
23
+ | `DB-001` | 🔴 CRITICAL | Postgres with no SSL on public endpoints |
24
+ | `INFRA-001` | 🔴 CRITICAL | Docker containers running as root |
25
+ | `INFRA-004` | 🔴 CRITICAL | Insecure CI/CD workflow (Dangerous `pull_request_target` in GitHub Actions) |
26
+ | `IAM-002` | 🟠 HIGH | Hardcoded Cloud/API Secrets (AWS, Stripe, Slack, GitHub keys in `.env`) |
27
+ | `INFRA-002` | 🟠 HIGH | Database ports exposed to all interfaces |
28
+ | `INFRA-003` | 🟠 HIGH | Missing package lockfile (Deterministic builds/SOC 2 control) |
29
+ | `LOG-001` | 🟠 HIGH | No structured audit trail / logging |
30
+ | `IAM-003` | 🟡 MEDIUM | No MFA on AWS root account |
31
+ | `DB-002` | 🟡 MEDIUM | No connection pool limits |
32
+ | `GIT-001` | 🟡 MEDIUM | Secrets leaked in git history |
33
+
34
+ ---
35
+
36
+ ## How it works
37
+
38
+ 1. **Runs entirely on your machine** — scans `.env`, `Dockerfile`, `docker-compose.yml`, IAM policies
39
+ 2. **Generates a security score** — 0-100 with grade (A-F) and enterprise readiness flag
40
+ 3. **Shows a rich terminal report** — color-coded findings with plain English explanations
41
+ 4. **Optionally uploads anonymous metadata** — only boolean pass/fail data, no secrets ever leave your machine
42
+
43
+ ```
44
+ ────────────────────────────────────────────────────────
45
+ DELTARQ Security Scanner v0.1.0
46
+ Scanning: /Users/you/my-startup
47
+ ────────────────────────────────────────────────────────
48
+
49
+ ✓ Detecting project type... FastAPI + PostgreSQL
50
+ ⚠ Scanning configuration files... 3 issues found
51
+ ✓ Scanning IAM configuration... 1 issue found
52
+ ✓ Scanning database config... No issues
53
+ ✓ Scanning git history... Clean
54
+
55
+ ────────────────────────────────────────────────────────
56
+ YOUR SECURITY SCORE: 34/100 [F — Critical Exposure]
57
+ ────────────────────────────────────────────────────────
58
+
59
+ 🔴 CRITICAL IAM-001 Wildcard IAM policy detected
60
+ → Any compromised service = full account takeover
61
+
62
+ 🔴 CRITICAL DB-001 Postgres unencrypted on public host
63
+ → User data exposed to interception in transit
64
+
65
+ 🟠 HIGH LOG-001 No audit trail detected
66
+ → You cannot detect or investigate a breach
67
+
68
+ ────────────────────────────────────────────────────────
69
+ ENTERPRISE READINESS: ✗ Not Ready
70
+ SOC 2 Gap Count: 3 controls failing
71
+ ────────────────────────────────────────────────────────
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Usage
77
+
78
+ ```bash
79
+ # Scan current directory
80
+ npx deltarq-scan
81
+
82
+ # Scan a specific project
83
+ npx deltarq-scan ./my-project
84
+
85
+ # Scan without upload prompt
86
+ npx deltarq-scan --no-upload
87
+
88
+ # Output JSON (for CI/CD pipelines)
89
+ npx deltarq-scan --json
90
+
91
+ # Verbose mode (see upload payload preview)
92
+ npx deltarq-scan --verbose
93
+ ```
94
+
95
+ ---
96
+
97
+ ## What data is uploaded?
98
+
99
+ **Only if you consent.** And only this:
100
+
101
+ ```json
102
+ {
103
+ "scan_id": "uuid",
104
+ "score": 34,
105
+ "grade": "F",
106
+ "enterprise_ready": false,
107
+ "findings": [
108
+ { "rule": "IAM-001", "severity": "CRITICAL", "passed": false }
109
+ ]
110
+ }
111
+ ```
112
+
113
+ **What is NEVER uploaded:**
114
+ - ❌ No `.env` values
115
+ - ❌ No AWS keys or secrets
116
+ - ❌ No file contents
117
+ - ❌ No IP addresses
118
+ - ❌ No company name (unless you opt in on dashboard)
119
+
120
+ ---
121
+
122
+ ## Privacy & Security
123
+
124
+ - **Open source** — read every line of code before running
125
+ - **Read-only** — never modifies your files
126
+ - **Local-first** — all scanning happens on your machine
127
+ - **Consent-gated** — nothing leaves without your explicit `Y`
128
+ - **MIT Licensed** — use it however you want
129
+
130
+ ---
131
+
132
+ ## Development & Local Testing
133
+
134
+ ### 1. Setup
135
+ ```bash
136
+ # Clone the repository
137
+ git clone https://github.com/deltarq/deltarq-scan.git
138
+ cd deltarq-scan
139
+
140
+ # Install dependencies
141
+ npm install
142
+ ```
143
+
144
+ ### 2. Run Scanner Locally
145
+ ```bash
146
+ # Scan the current directory
147
+ node bin/deltarq-scan.js
148
+
149
+ # Scan mock test fixtures (no upload prompt)
150
+ node bin/deltarq-scan.js ./tests/fixtures --no-upload
151
+ ```
152
+
153
+ ### 3. Test Global CLI Command Locally (`npm link`)
154
+ Before publishing to the npm registry, you can test the global `npx deltarq-scan` command in any directory on your computer by linking it:
155
+ ```bash
156
+ # 1. In the deltarq-scan project root, run:
157
+ npm link
158
+
159
+ # 2. Go to any other directory/project folder and run:
160
+ npx deltarq-scan
161
+ # or simply:
162
+ deltarq-scan
163
+ ```
164
+
165
+ ### 4. Test Full Upload Pipeline Locally
166
+ You can spin up a local Express server and test uploader integration directly.
167
+
168
+ **Terminal 1: Start Dashboard API Server**
169
+ ```bash
170
+ npm run server
171
+ ```
172
+
173
+ **Terminal 2: Run Scan & Upload to Local Server**
174
+ * **Windows (PowerShell)**:
175
+ ```powershell
176
+ $env:DELTARQ_API_URL="http://localhost:3000/v1/scans"; $env:DELTARQ_DASHBOARD_URL="http://localhost:3000"; node bin/deltarq-scan.js ./tests/fixtures
177
+ ```
178
+ * **Mac/Linux (Bash)**:
179
+ ```bash
180
+ DELTARQ_API_URL="http://localhost:3000/v1/scans" DELTARQ_DASHBOARD_URL="http://localhost:3000" node bin/deltarq-scan.js ./tests/fixtures
181
+ ```
182
+
183
+ ### 5. Run Automated Unit Tests
184
+ ```bash
185
+ # Run Vitest test runner
186
+ npm test
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Production Deployment
192
+
193
+ ### 1. Database Setup (Supabase)
194
+ Create a project on [Supabase](https://supabase.com) and copy the SQL code inside [supabase_schema.sql](supabase_schema.sql) into the **Supabase SQL Editor**, then click **Run**. This instantiates the `scans` and `leads` tables with pre-configured Row-Level Security (RLS) rules.
195
+
196
+ ### 2. Host Backend & Dashboard (Vercel)
197
+ Deploy the unified Express server and static dashboard using the Vercel CLI from the root folder:
198
+ ```bash
199
+ vercel --prod
200
+ ```
201
+ Be sure to set the following **Environment Variables** on your Vercel Dashboard:
202
+ - `DATA_LAYER_ENDPOINT` = your-supabase-project-url
203
+ - `DATA_LAYER_ANON_KEY` = your-supabase-public-anon-key (or `DATA_LAYER_SERVICE_ROLE_KEY` for higher privilege)
204
+ - `DELTARQ_DASHBOARD_URL` = your-deployed-vercel-domain (e.g. `https://deltarq-scan.vercel.app`)
205
+
206
+ ---
207
+
208
+ ## License
209
+
210
+ MIT — [DELTARQ](https://deltarq.io)
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * DELTARQ Security Scanner — CLI Entry Point
5
+ *
6
+ * Usage:
7
+ * npx deltarq-scan [target-dir] [options]
8
+ *
9
+ * Options:
10
+ * --no-upload Skip the dashboard upload prompt
11
+ * --json Output raw JSON instead of terminal report
12
+ * --verbose Show detailed output including upload payload preview
13
+ * --help Show this help message
14
+ */
15
+
16
+ import { resolveTargetDir } from '../src/utils/fileUtils.js';
17
+ import { detectProjectType } from '../src/utils/detect.js';
18
+ import { runFileScanner } from '../src/scanner/fileScanner.js';
19
+ import { runGitScanner } from '../src/scanner/gitScanner.js';
20
+ import { runDbScanner } from '../src/scanner/dbScanner.js';
21
+ import { runAwsScanner } from '../src/scanner/awsScanner.js';
22
+ import { calculateScore } from '../src/engine/aggregator.js';
23
+ import { anonymizeScanResults } from '../src/engine/anonymizer.js';
24
+ import { printBanner, printScanPhase, printReport } from '../src/output/terminal.js';
25
+ import { uploadWithConsent } from '../src/output/uploader.js';
26
+ import chalk from 'chalk';
27
+ import ora from 'ora';
28
+ import fs from 'fs';
29
+
30
+ // Parse CLI arguments
31
+ const args = process.argv.slice(2);
32
+ const flags = {
33
+ noUpload: args.includes('--no-upload'),
34
+ json: args.includes('--json'),
35
+ verbose: args.includes('--verbose'),
36
+ help: args.includes('--help') || args.includes('-h'),
37
+ };
38
+ const targetArg = args.find(a => !a.startsWith('--') && !a.startsWith('-'));
39
+
40
+ // Help message
41
+ if (flags.help) {
42
+ console.log(`
43
+ ${chalk.hex('#6C5CE7').bold('DELTARQ Security Scanner')} ${chalk.dim('v0.1.0')}
44
+
45
+ ${chalk.white('Local-first CLI security audit tool for startups.')}
46
+ ${chalk.white('Scans your infrastructure for SOC 2 readiness gaps.')}
47
+
48
+ ${chalk.white.bold('Usage:')}
49
+ npx deltarq-scan [target-dir] [options]
50
+
51
+ ${chalk.white.bold('Options:')}
52
+ ${chalk.green('--no-upload')} Skip the dashboard upload prompt
53
+ ${chalk.green('--json')} Output raw JSON instead of terminal report
54
+ ${chalk.green('--verbose')} Show detailed output and upload payload preview
55
+ ${chalk.green('--help, -h')} Show this help message
56
+
57
+ ${chalk.white.bold('Examples:')}
58
+ ${chalk.dim('npx deltarq-scan')} ${chalk.dim('# Scan current directory')}
59
+ ${chalk.dim('npx deltarq-scan ./my-project')} ${chalk.dim('# Scan specific directory')}
60
+ ${chalk.dim('npx deltarq-scan --no-upload')} ${chalk.dim('# Scan without upload prompt')}
61
+ ${chalk.dim('npx deltarq-scan --json')} ${chalk.dim('# Output JSON report')}
62
+
63
+ ${chalk.dim('Learn more: https://github.com/deltarq/deltarq-scan')}
64
+ `);
65
+ process.exit(0);
66
+ }
67
+
68
+ // Main scan flow
69
+ async function main() {
70
+ const targetDir = resolveTargetDir(targetArg);
71
+
72
+ if (!fs.existsSync(targetDir) || !fs.statSync(targetDir).isDirectory()) {
73
+ console.error(chalk.red.bold(`\n ✗ Error: Target path "${targetDir}" is not a valid directory.\n`));
74
+ process.exit(1);
75
+ }
76
+
77
+ const allFindings = [];
78
+
79
+ // Print banner (unless JSON mode)
80
+ if (!flags.json) {
81
+ printBanner(targetDir);
82
+ }
83
+
84
+ // Phase 1: Detect project type
85
+ const spinner = !flags.json ? ora({ text: 'Detecting project type...', indent: 3 }).start() : null;
86
+ const projectInfo = detectProjectType(targetDir);
87
+
88
+ if (!flags.json) {
89
+ spinner.stop();
90
+ printScanPhase('Detecting project type...', projectInfo.label, 'ok');
91
+ }
92
+
93
+ // Phase 2: File Scanner (IAM, DB, Docker, Logging)
94
+ if (!flags.json) {
95
+ const fileSpinner = ora({ text: 'Scanning configuration files...', indent: 3 }).start();
96
+ var fileFindings = await runFileScanner(targetDir);
97
+ fileSpinner.stop();
98
+
99
+ const fileIssues = fileFindings.filter(f => !f.passed).length;
100
+ printScanPhase(
101
+ 'Scanning configuration files...',
102
+ fileIssues > 0 ? `${fileIssues} issue${fileIssues > 1 ? 's' : ''} found` : 'No issues',
103
+ fileIssues > 0 ? 'warning' : 'ok'
104
+ );
105
+ } else {
106
+ var fileFindings = await runFileScanner(targetDir);
107
+ }
108
+ allFindings.push(...fileFindings);
109
+
110
+ // Phase 3: AWS Scanner
111
+ if (!flags.json) {
112
+ const awsSpinner = ora({ text: 'Scanning IAM configuration...', indent: 3 }).start();
113
+ var { findings: awsFindings, awsConfigured } = await runAwsScanner(targetDir);
114
+ awsSpinner.stop();
115
+
116
+ if (!awsConfigured && awsFindings.length === 0) {
117
+ printScanPhase('Scanning IAM configuration...', 'AWS not configured — skipped cloud checks', 'skip');
118
+ } else {
119
+ const awsIssues = awsFindings.filter(f => !f.passed).length;
120
+ printScanPhase(
121
+ 'Scanning IAM configuration...',
122
+ awsIssues > 0 ? `${awsIssues} issue${awsIssues > 1 ? 's' : ''} found` : 'No issues',
123
+ awsIssues > 0 ? 'warning' : 'ok'
124
+ );
125
+ }
126
+ } else {
127
+ var { findings: awsFindings } = await runAwsScanner(targetDir);
128
+ }
129
+ allFindings.push(...awsFindings);
130
+
131
+ // Phase 4: Database Scanner
132
+ if (!flags.json) {
133
+ const dbSpinner = ora({ text: 'Scanning database config...', indent: 3 }).start();
134
+ var dbFindings = await runDbScanner(targetDir);
135
+ dbSpinner.stop();
136
+
137
+ const dbIssues = dbFindings.filter(f => !f.passed).length;
138
+ printScanPhase(
139
+ 'Scanning database config...',
140
+ dbIssues > 0 ? `${dbIssues} issue${dbIssues > 1 ? 's' : ''} found` : 'No issues',
141
+ dbIssues > 0 ? 'warning' : 'ok'
142
+ );
143
+ } else {
144
+ var dbFindings = await runDbScanner(targetDir);
145
+ }
146
+ allFindings.push(...dbFindings);
147
+
148
+ // Phase 5: Git Scanner
149
+ if (!flags.json) {
150
+ const gitSpinner = ora({ text: 'Scanning git repository...', indent: 3 }).start();
151
+ var gitFindings = await runGitScanner(targetDir);
152
+ gitSpinner.stop();
153
+
154
+ const gitIssues = gitFindings.filter(f => !f.passed).length;
155
+ printScanPhase(
156
+ 'Scanning git repository...',
157
+ gitIssues > 0 ? 'Warning detected' : 'Clean',
158
+ gitIssues > 0 ? 'warning' : 'ok'
159
+ );
160
+ } else {
161
+ var gitFindings = await runGitScanner(targetDir);
162
+ }
163
+ allFindings.push(...gitFindings);
164
+
165
+ // Phase 6: Calculate score
166
+ const scoreResult = calculateScore(allFindings);
167
+
168
+ // JSON output mode
169
+ if (flags.json) {
170
+ const anonymous = anonymizeScanResults(allFindings, scoreResult, projectInfo);
171
+ console.log(JSON.stringify(anonymous, null, 2));
172
+ process.exit(scoreResult.score < 40 ? 1 : 0);
173
+ return;
174
+ }
175
+
176
+ // Phase 7: Print the report
177
+ printReport(allFindings, scoreResult, projectInfo);
178
+
179
+ // Phase 8: Upload prompt
180
+ const anonymousPayload = anonymizeScanResults(allFindings, scoreResult, projectInfo);
181
+ await uploadWithConsent(anonymousPayload, {
182
+ noUpload: flags.noUpload,
183
+ verbose: flags.verbose,
184
+ });
185
+
186
+ // Exit with non-zero if critical exposure
187
+ process.exit(scoreResult.score < 40 ? 1 : 0);
188
+ }
189
+
190
+ // Run
191
+ main().catch((err) => {
192
+ console.error(chalk.red.bold('\n ✗ Scanner encountered an error:\n'));
193
+ console.error(chalk.red(` ${err.message}`));
194
+ if (flags.verbose) {
195
+ console.error(chalk.dim(`\n ${err.stack}`));
196
+ }
197
+ process.exit(2);
198
+ });
package/logo.png ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "deltarq-scan",
3
+ "version": "0.1.0",
4
+ "description": "Local-first CLI security audit tool for startups — scan your infrastructure for SOC 2 readiness gaps",
5
+ "type": "module",
6
+ "bin": {
7
+ "deltarq-scan": "bin/deltarq-scan.js"
8
+ },
9
+ "main": "bin/deltarq-scan.js",
10
+ "scripts": {
11
+ "scan": "node bin/deltarq-scan.js",
12
+ "test": "vitest run",
13
+ "server": "node server.js"
14
+ },
15
+ "keywords": [
16
+ "security",
17
+ "audit",
18
+ "soc2",
19
+ "compliance",
20
+ "scanner",
21
+ "cli",
22
+ "devops",
23
+ "infrastructure"
24
+ ],
25
+ "author": "DELTARQ",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "@supabase/supabase-js": "^2.108.2",
29
+ "chalk": "^5.3.0",
30
+ "cli-table3": "^0.6.5",
31
+ "clsx": "^2.1.1",
32
+ "cors": "^2.8.6",
33
+ "dotenv": "^17.4.2",
34
+ "express": "^5.2.1",
35
+ "framer-motion": "^12.40.0",
36
+ "glob": "^10.4.5",
37
+ "js-yaml": "^4.1.0",
38
+ "lucide-react": "^1.21.0",
39
+ "ora": "^8.1.1",
40
+ "recharts": "^3.8.1",
41
+ "tailwind-merge": "^3.6.0",
42
+ "uuid": "^10.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "vitest": "^2.1.0"
46
+ },
47
+ "engines": {
48
+ "node": ">=18.0.0"
49
+ }
50
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Scoring Engine — Aggregates scan findings into a security score
3
+ * Weighted scoring: CRITICAL=40, HIGH=20, MEDIUM=10, DRIFT=5
4
+ */
5
+
6
+ const SEVERITY_WEIGHTS = {
7
+ CRITICAL: 40,
8
+ HIGH: 20,
9
+ MEDIUM: 10,
10
+ DRIFT: 5,
11
+ };
12
+
13
+ /**
14
+ * Calculate the overall security score from findings
15
+ * @param {Array<{ rule: string, severity: string, passed: boolean }>} findings
16
+ * @returns {{ score: number, grade: string, label: string, enterpriseReady: boolean, breakdown: Object }}
17
+ */
18
+ export function calculateScore(findings) {
19
+ const failedFindings = findings.filter(f => !f.passed);
20
+
21
+ let deductions = 0;
22
+ const breakdown = {
23
+ CRITICAL: 0,
24
+ HIGH: 0,
25
+ MEDIUM: 0,
26
+ DRIFT: 0,
27
+ };
28
+
29
+ // Deduplicate by rule ID — only count each rule once
30
+ const seenRules = new Set();
31
+ for (const f of failedFindings) {
32
+ if (seenRules.has(f.rule)) continue;
33
+ seenRules.add(f.rule);
34
+
35
+ const weight = SEVERITY_WEIGHTS[f.severity] || 0;
36
+ deductions += weight;
37
+ breakdown[f.severity] = (breakdown[f.severity] || 0) + 1;
38
+ }
39
+
40
+ const score = Math.max(0, 100 - deductions);
41
+
42
+ let grade, label;
43
+ if (score >= 90) {
44
+ grade = 'A';
45
+ label = 'Excellent';
46
+ } else if (score >= 80) {
47
+ grade = 'B';
48
+ label = 'Acceptable';
49
+ } else if (score >= 60) {
50
+ grade = 'C';
51
+ label = 'At Risk';
52
+ } else if (score >= 40) {
53
+ grade = 'D';
54
+ label = 'Vulnerable';
55
+ } else {
56
+ grade = 'F';
57
+ label = 'Critical Exposure';
58
+ }
59
+
60
+ const criticalCount = breakdown.CRITICAL || 0;
61
+ const enterpriseReady = score >= 85 && criticalCount === 0;
62
+
63
+ return {
64
+ score,
65
+ grade,
66
+ label,
67
+ enterpriseReady,
68
+ breakdown,
69
+ totalFindings: failedFindings.length,
70
+ uniqueRules: seenRules.size,
71
+ deductions,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Get the SOC 2 compliance gap count
77
+ * Each failed unique rule = 1 control gap
78
+ */
79
+ export function getComplianceGapCount(findings) {
80
+ const failedRules = new Set(
81
+ findings.filter(f => !f.passed).map(f => f.rule)
82
+ );
83
+ return failedRules.size;
84
+ }
85
+
86
+ /**
87
+ * Estimate remediation effort based on findings
88
+ */
89
+ export function estimateRemediationEffort(findings) {
90
+ const gaps = getComplianceGapCount(findings);
91
+ if (gaps === 0) return 'None needed';
92
+ if (gaps <= 2) return '1–2 days';
93
+ if (gaps <= 5) return '3–5 days without guidance';
94
+ if (gaps <= 8) return '1–2 weeks without guidance';
95
+ return '2+ weeks — consider professional audit';
96
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Anonymizer — Strips all sensitive data from scan findings
3
+ * Outputs only boolean pass/fail metadata + scores
4
+ * NEVER includes raw values, secrets, file contents, or IPs
5
+ */
6
+
7
+ import { v4 as uuidv4 } from 'uuid';
8
+
9
+ /**
10
+ * Create an anonymous scan report from raw findings
11
+ * @param {Array} findings - Raw scan findings
12
+ * @param {{ score: number, grade: string, enterpriseReady: boolean }} scoreResult - Score calculation
13
+ * @param {{ type: string }} projectInfo - Detected project type
14
+ * @returns {Object} Anonymous JSON safe for upload
15
+ */
16
+ export function anonymizeScanResults(findings, scoreResult, projectInfo) {
17
+ // Deduplicate findings by rule ID (keep first occurrence)
18
+ const seenRules = new Set();
19
+ const uniqueFindings = [];
20
+ for (const f of findings) {
21
+ if (!seenRules.has(f.rule)) {
22
+ seenRules.add(f.rule);
23
+ uniqueFindings.push(f);
24
+ }
25
+ }
26
+
27
+ return {
28
+ scan_id: uuidv4(),
29
+ timestamp: new Date().toISOString(),
30
+ scanner_version: '0.1.0',
31
+ project_type: projectInfo.type || 'unknown',
32
+
33
+ // Score metadata only
34
+ score: scoreResult.score,
35
+ grade: scoreResult.grade,
36
+ enterprise_ready: scoreResult.enterpriseReady,
37
+
38
+ // Findings — ONLY rule ID, severity, and pass/fail boolean
39
+ // NO file paths, NO secret values, NO detail strings
40
+ findings: uniqueFindings.map(f => ({
41
+ rule: f.rule,
42
+ severity: f.severity,
43
+ passed: f.passed,
44
+ })),
45
+
46
+ // Aggregate stats
47
+ drift_detected: uniqueFindings.some(f => f.severity === 'DRIFT' && !f.passed),
48
+ compliance_gap_count: uniqueFindings.filter(f => !f.passed).length,
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Validate that the anonymous payload contains no secrets
54
+ * Extra safety check before upload
55
+ */
56
+ export function validateNoSecrets(payload) {
57
+ const json = JSON.stringify(payload);
58
+
59
+ // Pattern checks — these should NEVER appear in upload payload
60
+ const forbiddenPatterns = [
61
+ /AKIA[0-9A-Z]{16}/, // AWS Access Key ID
62
+ /[0-9a-zA-Z/+]{40}/, // AWS Secret Key (40 char base64)
63
+ /-----BEGIN.*PRIVATE KEY-----/, // PEM private keys
64
+ /postgres:\/\//, // DB connection strings
65
+ /mysql:\/\//,
66
+ /mongodb:\/\//,
67
+ /password\s*[:=]\s*\S+/i, // Password patterns
68
+ ];
69
+
70
+ for (const pattern of forbiddenPatterns) {
71
+ if (pattern.test(json)) {
72
+ throw new Error(
73
+ `SAFETY CHECK FAILED: Upload payload may contain sensitive data matching pattern: ${pattern.source}. Upload aborted.`
74
+ );
75
+ }
76
+ }
77
+
78
+ return true;
79
+ }