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
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
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](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
|
+
}
|