ship-safe 4.2.0 → 4.3.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 +65 -16
- package/cli/__tests__/agents.test.js +226 -0
- package/cli/agents/api-fuzzer.js +111 -0
- package/cli/agents/base-agent.js +262 -253
- package/cli/agents/config-auditor.js +71 -0
- package/cli/agents/html-reporter.js +370 -363
- package/cli/agents/index.js +59 -56
- package/cli/agents/orchestrator.js +44 -1
- package/cli/agents/supabase-rls-agent.js +148 -0
- package/cli/agents/supply-chain-agent.js +356 -274
- package/cli/bin/ship-safe.js +15 -1
- package/cli/commands/audit.js +27 -1
- package/cli/commands/baseline.js +192 -0
- package/cli/index.js +4 -0
- package/cli/utils/autofix-rules.js +74 -0
- package/cli/utils/pdf-generator.js +94 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
<a href="https://github.com/asamassekou10/ship-safe/actions/workflows/ci.yml"><img src="https://github.com/asamassekou10/ship-safe/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
|
|
10
10
|
<a href="https://nodejs.org"><img src="https://img.shields.io/node/v/ship-safe" alt="Node.js version" /></a>
|
|
11
11
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
|
|
12
|
+
<a href="https://github.com/asamassekou10/ship-safe/stargazers"><img src="https://img.shields.io/github/stars/asamassekou10/ship-safe?style=social" alt="GitHub stars" /></a>
|
|
12
13
|
</p>
|
|
13
14
|
|
|
14
15
|
---
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
13 security agents. 50+ attack classes. One command.
|
|
17
18
|
|
|
18
|
-
**Ship Safe v4.
|
|
19
|
+
**Ship Safe v4.3** is an AI-powered security platform that runs 13 specialized agents in parallel against your codebase — scanning for secrets, injection vulnerabilities, auth bypass, SSRF, supply chain attacks, Supabase RLS misconfigs, Docker/Terraform/Kubernetes misconfigs, CI/CD pipeline poisoning, LLM security issues, and more. Context-aware confidence tuning reduces false positives by up to 70%. Baseline support lets teams adopt incrementally — accept existing debt, focus on not making it worse.
|
|
19
20
|
|
|
20
21
|
---
|
|
21
22
|
|
|
@@ -33,6 +34,13 @@ npx ship-safe scan .
|
|
|
33
34
|
|
|
34
35
|
# Security health score (0-100)
|
|
35
36
|
npx ship-safe score .
|
|
37
|
+
|
|
38
|
+
# Accept current findings, only report regressions
|
|
39
|
+
npx ship-safe baseline .
|
|
40
|
+
npx ship-safe audit . --baseline
|
|
41
|
+
|
|
42
|
+
# Environment diagnostics
|
|
43
|
+
npx ship-safe doctor
|
|
36
44
|
```
|
|
37
45
|
|
|
38
46
|

|
|
@@ -49,11 +57,11 @@ npx ship-safe audit .
|
|
|
49
57
|
|
|
50
58
|
```
|
|
51
59
|
════════════════════════════════════════════════════════════
|
|
52
|
-
Ship Safe v4.
|
|
60
|
+
Ship Safe v4.3 — Full Security Audit
|
|
53
61
|
════════════════════════════════════════════════════════════
|
|
54
62
|
|
|
55
63
|
[Phase 1/4] Scanning for secrets... ✔ 49 found
|
|
56
|
-
[Phase 2/4] Running
|
|
64
|
+
[Phase 2/4] Running 13 security agents... ✔ 103 findings
|
|
57
65
|
[Phase 3/4] Auditing dependencies... ✔ 44 CVEs
|
|
58
66
|
[Phase 4/4] Computing security score... ✔ 25/100 F
|
|
59
67
|
|
|
@@ -80,36 +88,44 @@ npx ship-safe audit .
|
|
|
80
88
|
|
|
81
89
|
**What it runs:**
|
|
82
90
|
1. **Secret scan** — 50+ patterns with entropy scoring (API keys, passwords, tokens)
|
|
83
|
-
2. **
|
|
91
|
+
2. **13 security agents** — run in parallel with per-agent timeouts (injection, auth, SSRF, supply chain, config, Supabase RLS, LLM, mobile, git history, CI/CD, API)
|
|
84
92
|
3. **Dependency audit** — npm/pip/bundler CVE scanning
|
|
85
|
-
4. **Score computation** —
|
|
86
|
-
5. **
|
|
87
|
-
6. **
|
|
93
|
+
4. **Score computation** — confidence-weighted scoring across 8 categories (0-100, A-F)
|
|
94
|
+
5. **Context-aware confidence tuning** — downgrades findings in test files, docs, and comments
|
|
95
|
+
6. **Remediation plan** — prioritized fix list grouped by severity
|
|
96
|
+
7. **HTML report** — standalone dark-themed report with code context
|
|
88
97
|
|
|
89
98
|
**Flags:**
|
|
90
99
|
- `--json` — structured JSON output (clean for piping)
|
|
91
100
|
- `--sarif` — SARIF format for GitHub Code Scanning
|
|
101
|
+
- `--csv` — CSV export for spreadsheets
|
|
102
|
+
- `--md` — Markdown report
|
|
92
103
|
- `--html [file]` — custom HTML report path (default: `ship-safe-report.html`)
|
|
104
|
+
- `--compare` — show per-category score delta vs. last scan
|
|
105
|
+
- `--timeout <ms>` — per-agent timeout (default: 30s)
|
|
93
106
|
- `--no-deps` — skip dependency audit
|
|
94
107
|
- `--no-ai` — skip AI classification
|
|
95
108
|
- `--no-cache` — force full rescan (ignore cached results)
|
|
109
|
+
- `--baseline` — only show findings not in the baseline
|
|
110
|
+
- `--pdf [file]` — generate PDF report (requires Chrome/Chromium)
|
|
96
111
|
|
|
97
112
|
---
|
|
98
113
|
|
|
99
|
-
##
|
|
114
|
+
## 13 Security Agents
|
|
100
115
|
|
|
101
116
|
| Agent | Category | What It Detects |
|
|
102
117
|
|-------|----------|-----------------|
|
|
103
|
-
| **InjectionTester** | Code Vulns | SQL/NoSQL injection, command injection, code injection (eval), XSS, path traversal, XXE, ReDoS, prototype pollution |
|
|
104
|
-
| **AuthBypassAgent** | Auth | JWT vulnerabilities (alg:none, weak secrets), cookie security, CSRF, OAuth misconfig, BOLA/IDOR, weak crypto, timing attacks, TLS bypass |
|
|
118
|
+
| **InjectionTester** | Code Vulns | SQL/NoSQL injection, command injection, code injection (eval), XSS, path traversal, XXE, ReDoS, prototype pollution, Python f-string SQL injection, Python subprocess shell injection |
|
|
119
|
+
| **AuthBypassAgent** | Auth | JWT vulnerabilities (alg:none, weak secrets), cookie security, CSRF, OAuth misconfig, BOLA/IDOR, weak crypto, timing attacks, TLS bypass, Django `DEBUG = True`, Flask hardcoded secret keys |
|
|
105
120
|
| **SSRFProber** | SSRF | User input in fetch/axios, cloud metadata endpoints, internal IPs, redirect following |
|
|
106
|
-
| **SupplyChainAudit** | Supply Chain | Typosquatting (Levenshtein distance), git/URL dependencies, wildcard versions, suspicious install scripts |
|
|
107
|
-
| **ConfigAuditor** | Config | Dockerfile (running as root, :latest tags), Terraform (public S3, open SG), Kubernetes (privileged containers), CORS, CSP, Firebase, Nginx |
|
|
121
|
+
| **SupplyChainAudit** | Supply Chain | Typosquatting (Levenshtein distance), git/URL dependencies, wildcard versions, suspicious install scripts, dependency confusion, scoped packages without registry pinning |
|
|
122
|
+
| **ConfigAuditor** | Config | Dockerfile (running as root, :latest tags), Terraform (public S3/RDS, open SG, CloudFront HTTP, Lambda admin, S3 no versioning), Kubernetes (privileged containers, `:latest` tags, missing NetworkPolicy), CORS, CSP, Firebase, Nginx |
|
|
123
|
+
| **SupabaseRLSAgent** | Auth | Supabase Row Level Security — `service_role` key in client code, `CREATE TABLE` without RLS, anon key inserts, unprotected storage operations |
|
|
108
124
|
| **LLMRedTeam** | AI/LLM | OWASP LLM Top 10 — prompt injection, excessive agency, system prompt leakage, unbounded consumption, RAG poisoning |
|
|
109
125
|
| **MobileScanner** | Mobile | OWASP Mobile Top 10 2024 — insecure storage, WebView JS injection, HTTP endpoints, excessive permissions, debug mode |
|
|
110
126
|
| **GitHistoryScanner** | Secrets | Leaked secrets in git commit history (checks if still active in working tree) |
|
|
111
127
|
| **CICDScanner** | CI/CD | OWASP CI/CD Top 10 — pipeline poisoning, unpinned actions, secret logging, self-hosted runners, script injection |
|
|
112
|
-
| **APIFuzzer** | API | Routes without auth, missing input validation, mass assignment, unrestricted file upload, GraphQL introspection, debug endpoints |
|
|
128
|
+
| **APIFuzzer** | API | Routes without auth, missing input validation, mass assignment, unrestricted file upload, GraphQL introspection, debug endpoints, missing rate limiting, OpenAPI spec security issues |
|
|
113
129
|
| **ReconAgent** | Recon | Attack surface discovery — frameworks, languages, auth patterns, databases, cloud providers, IaC, CI/CD pipelines |
|
|
114
130
|
| **ScoringEngine** | Scoring | 8-category weighted scoring with trend tracking |
|
|
115
131
|
|
|
@@ -123,7 +139,7 @@ npx ship-safe audit .
|
|
|
123
139
|
# Full audit with remediation plan + HTML report
|
|
124
140
|
npx ship-safe audit .
|
|
125
141
|
|
|
126
|
-
# Red team:
|
|
142
|
+
# Red team: 13 agents, 50+ attack classes
|
|
127
143
|
npx ship-safe red-team .
|
|
128
144
|
npx ship-safe red-team . --agents injection,auth # Run specific agents
|
|
129
145
|
npx ship-safe red-team . --html report.html # HTML report
|
|
@@ -150,11 +166,35 @@ npx ship-safe agent .
|
|
|
150
166
|
|
|
151
167
|
# Auto-fix hardcoded secrets: rewrite code + write .env
|
|
152
168
|
npx ship-safe remediate .
|
|
169
|
+
npx ship-safe remediate . --all # Also fix agent findings (TLS, debug, XSS, etc.)
|
|
153
170
|
|
|
154
171
|
# Revoke exposed keys — opens provider dashboards
|
|
155
172
|
npx ship-safe rotate .
|
|
156
173
|
```
|
|
157
174
|
|
|
175
|
+
### Baseline Management
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# Accept current findings as baseline
|
|
179
|
+
npx ship-safe baseline .
|
|
180
|
+
|
|
181
|
+
# Audit showing only new findings since baseline
|
|
182
|
+
npx ship-safe audit . --baseline
|
|
183
|
+
|
|
184
|
+
# Show what changed since baseline
|
|
185
|
+
npx ship-safe baseline --diff
|
|
186
|
+
|
|
187
|
+
# Remove baseline
|
|
188
|
+
npx ship-safe baseline --clear
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Diagnostics
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# Environment check — Node.js, git, npm, API keys, cache, version
|
|
195
|
+
npx ship-safe doctor
|
|
196
|
+
```
|
|
197
|
+
|
|
158
198
|
### Infrastructure Commands
|
|
159
199
|
|
|
160
200
|
```bash
|
|
@@ -214,6 +254,10 @@ Ship Safe caches file hashes and findings in `.ship-safe/context.json`. On subse
|
|
|
214
254
|
|
|
215
255
|
The cache is stored in `.ship-safe/` which is automatically excluded from scans.
|
|
216
256
|
|
|
257
|
+
### LLM Response Caching
|
|
258
|
+
|
|
259
|
+
When using AI classification (`--no-ai` to disable), results are cached in `.ship-safe/llm-cache.json` with a 7-day TTL. Repeated scans reuse cached classifications — reducing API costs significantly.
|
|
260
|
+
|
|
217
261
|
---
|
|
218
262
|
|
|
219
263
|
## Smart `.gitignore` Handling
|
|
@@ -247,7 +291,7 @@ Auto-detected from environment variables. No API key required for scanning — A
|
|
|
247
291
|
|
|
248
292
|
## Scoring System
|
|
249
293
|
|
|
250
|
-
Starts at 100. Each finding deducts points by severity and category.
|
|
294
|
+
Starts at 100. Each finding deducts points by severity and category, weighted by confidence level (high: 100%, medium: 60%, low: 30%) to reduce noise from heuristic patterns.
|
|
251
295
|
|
|
252
296
|
**8 Categories** (with weight caps):
|
|
253
297
|
|
|
@@ -306,6 +350,9 @@ jobs:
|
|
|
306
350
|
- name: Full security audit
|
|
307
351
|
run: npx ship-safe audit . --no-ai --json
|
|
308
352
|
|
|
353
|
+
- name: Score delta vs. last scan
|
|
354
|
+
run: npx ship-safe audit . --no-ai --compare
|
|
355
|
+
|
|
309
356
|
- name: Upload SARIF to GitHub Security tab
|
|
310
357
|
run: npx ship-safe audit . --no-ai --sarif > results.sarif
|
|
311
358
|
|
|
@@ -314,6 +361,8 @@ jobs:
|
|
|
314
361
|
sarif_file: results.sarif
|
|
315
362
|
```
|
|
316
363
|
|
|
364
|
+
**Export formats:** `--json`, `--sarif`, `--csv`, `--md`, `--html`, `--pdf`
|
|
365
|
+
|
|
317
366
|
---
|
|
318
367
|
|
|
319
368
|
## Suppress False Positives
|
|
@@ -493,4 +493,230 @@ describe('Orchestrator', async () => {
|
|
|
493
493
|
const deduped = orchestrator.deduplicate(findings);
|
|
494
494
|
assert.equal(deduped.length, 2);
|
|
495
495
|
});
|
|
496
|
+
|
|
497
|
+
it('tunes confidence for test files', () => {
|
|
498
|
+
const orchestrator = new Orchestrator();
|
|
499
|
+
const findings = [
|
|
500
|
+
{ file: '/project/__tests__/foo.test.js', line: 1, rule: 'R1', severity: 'high', confidence: 'high', matched: 'eval(x)' },
|
|
501
|
+
{ file: '/project/src/app.js', line: 1, rule: 'R2', severity: 'high', confidence: 'high', matched: 'eval(x)' },
|
|
502
|
+
];
|
|
503
|
+
const tuned = orchestrator.tuneConfidence(findings);
|
|
504
|
+
assert.equal(tuned[0].confidence, 'low'); // test file → low
|
|
505
|
+
assert.equal(tuned[1].confidence, 'high'); // src file → unchanged
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('tunes confidence for doc files', () => {
|
|
509
|
+
const orchestrator = new Orchestrator();
|
|
510
|
+
const findings = [
|
|
511
|
+
{ file: '/project/README.md', line: 5, rule: 'R1', severity: 'high', confidence: 'high', matched: 'password = "test"' },
|
|
512
|
+
];
|
|
513
|
+
const tuned = orchestrator.tuneConfidence(findings);
|
|
514
|
+
assert.equal(tuned[0].confidence, 'low');
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('tunes confidence for example paths', () => {
|
|
518
|
+
const orchestrator = new Orchestrator();
|
|
519
|
+
const findings = [
|
|
520
|
+
{ file: '/project/examples/demo.js', line: 1, rule: 'R1', severity: 'high', confidence: 'high', matched: 'eval(x)' },
|
|
521
|
+
];
|
|
522
|
+
const tuned = orchestrator.tuneConfidence(findings);
|
|
523
|
+
assert.equal(tuned[0].confidence, 'medium');
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// =============================================================================
|
|
528
|
+
// SUPABASE RLS AGENT
|
|
529
|
+
// =============================================================================
|
|
530
|
+
|
|
531
|
+
describe('SupabaseRLSAgent', () => {
|
|
532
|
+
it('detects service_role key in client code', async () => {
|
|
533
|
+
const { SupabaseRLSAgent } = await import('../agents/supabase-rls-agent.js');
|
|
534
|
+
const agent = new SupabaseRLSAgent();
|
|
535
|
+
const { dir, file } = writeTempFile(`const supabase = createClient(url, SUPABASE_SERVICE_ROLE_KEY);`);
|
|
536
|
+
try {
|
|
537
|
+
const findings = agent.scanFileWithPatterns(file, [
|
|
538
|
+
{ rule: 'SUPABASE_SERVICE_KEY_CLIENT', regex: /SUPABASE_SERVICE_ROLE_KEY|service_role_key|serviceRoleKey|supabaseAdmin/g, severity: 'critical', title: 'test', description: 'test' }
|
|
539
|
+
]);
|
|
540
|
+
assert.ok(findings.some(f => f.rule === 'SUPABASE_SERVICE_KEY_CLIENT'));
|
|
541
|
+
} finally { cleanup(dir); }
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('detects missing RLS on table', async () => {
|
|
545
|
+
const { SupabaseRLSAgent } = await import('../agents/supabase-rls-agent.js');
|
|
546
|
+
const agent = new SupabaseRLSAgent();
|
|
547
|
+
|
|
548
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shipsafe-rls-'));
|
|
549
|
+
const sqlFile = path.join(dir, 'migration.sql');
|
|
550
|
+
fs.writeFileSync(sqlFile, `CREATE TABLE users (id uuid PRIMARY KEY, name text);\nCREATE TABLE posts (id uuid PRIMARY KEY);`);
|
|
551
|
+
const jsFile = path.join(dir, 'app.js');
|
|
552
|
+
fs.writeFileSync(jsFile, 'const x = 1;');
|
|
553
|
+
|
|
554
|
+
try {
|
|
555
|
+
const findings = await agent.analyze({
|
|
556
|
+
rootPath: dir,
|
|
557
|
+
files: [sqlFile, jsFile],
|
|
558
|
+
recon: {},
|
|
559
|
+
options: {},
|
|
560
|
+
});
|
|
561
|
+
// Should flag both tables as missing RLS
|
|
562
|
+
const rlsFindings = findings.filter(f => f.rule === 'SUPABASE_NO_RLS_POLICY');
|
|
563
|
+
assert.ok(rlsFindings.length >= 2, `Expected >=2 RLS findings, got ${rlsFindings.length}`);
|
|
564
|
+
} finally { cleanup(dir); }
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('does not flag tables with RLS enabled', async () => {
|
|
568
|
+
const { SupabaseRLSAgent } = await import('../agents/supabase-rls-agent.js');
|
|
569
|
+
const agent = new SupabaseRLSAgent();
|
|
570
|
+
|
|
571
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shipsafe-rls2-'));
|
|
572
|
+
const sqlFile = path.join(dir, 'migration.sql');
|
|
573
|
+
fs.writeFileSync(sqlFile, `CREATE TABLE users (id uuid PRIMARY KEY);\nALTER TABLE users ENABLE ROW LEVEL SECURITY;`);
|
|
574
|
+
|
|
575
|
+
try {
|
|
576
|
+
const findings = await agent.analyze({
|
|
577
|
+
rootPath: dir,
|
|
578
|
+
files: [sqlFile],
|
|
579
|
+
recon: {},
|
|
580
|
+
options: {},
|
|
581
|
+
});
|
|
582
|
+
const rlsFindings = findings.filter(f => f.rule === 'SUPABASE_NO_RLS_POLICY');
|
|
583
|
+
assert.equal(rlsFindings.length, 0);
|
|
584
|
+
} finally { cleanup(dir); }
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// =============================================================================
|
|
589
|
+
// CONFIG AUDITOR — NEW TERRAFORM/K8S PATTERNS
|
|
590
|
+
// =============================================================================
|
|
591
|
+
|
|
592
|
+
describe('ConfigAuditor (v4.3 patterns)', () => {
|
|
593
|
+
it('detects publicly accessible RDS', async () => {
|
|
594
|
+
const { ConfigAuditor } = await import('../agents/config-auditor.js');
|
|
595
|
+
const agent = new ConfigAuditor();
|
|
596
|
+
const { dir, file } = writeTempFile(`resource "aws_db_instance" "main" {\n publicly_accessible = true\n}`, '.tf');
|
|
597
|
+
try {
|
|
598
|
+
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
599
|
+
assert.ok(findings.some(f => f.rule === 'TERRAFORM_RDS_PUBLIC'));
|
|
600
|
+
} finally { cleanup(dir); }
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it('detects CloudFront allowing HTTP', async () => {
|
|
604
|
+
const { ConfigAuditor } = await import('../agents/config-auditor.js');
|
|
605
|
+
const agent = new ConfigAuditor();
|
|
606
|
+
const { dir, file } = writeTempFile(`viewer_protocol_policy = "allow-all"`, '.tf');
|
|
607
|
+
try {
|
|
608
|
+
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
609
|
+
assert.ok(findings.some(f => f.rule === 'TERRAFORM_CLOUDFRONT_HTTP'));
|
|
610
|
+
} finally { cleanup(dir); }
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it('detects K8s :latest image tag', async () => {
|
|
614
|
+
const { ConfigAuditor } = await import('../agents/config-auditor.js');
|
|
615
|
+
const agent = new ConfigAuditor();
|
|
616
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shipsafe-k8s-'));
|
|
617
|
+
const k8sDir = path.join(dir, 'k8s');
|
|
618
|
+
fs.mkdirSync(k8sDir);
|
|
619
|
+
const file = path.join(k8sDir, 'deployment.yaml');
|
|
620
|
+
fs.writeFileSync(file, `kind: Deployment\nspec:\n containers:\n - image: nginx:latest`);
|
|
621
|
+
try {
|
|
622
|
+
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
623
|
+
assert.ok(findings.some(f => f.rule === 'K8S_LATEST_IMAGE'));
|
|
624
|
+
} finally { cleanup(dir); }
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// =============================================================================
|
|
629
|
+
// API FUZZER — RATE LIMITING & OPENAPI
|
|
630
|
+
// =============================================================================
|
|
631
|
+
|
|
632
|
+
describe('APIFuzzer (v4.3 patterns)', () => {
|
|
633
|
+
it('detects missing rate limiting in Express app', async () => {
|
|
634
|
+
const { APIFuzzer } = await import('../agents/api-fuzzer.js');
|
|
635
|
+
const agent = new APIFuzzer();
|
|
636
|
+
|
|
637
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shipsafe-api-'));
|
|
638
|
+
const file = path.join(dir, 'app.js');
|
|
639
|
+
fs.writeFileSync(file, `import express from 'express';\nconst app = express();\napp.listen(3000);`);
|
|
640
|
+
|
|
641
|
+
try {
|
|
642
|
+
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
643
|
+
assert.ok(findings.some(f => f.rule === 'API_NO_RATE_LIMIT'));
|
|
644
|
+
} finally { cleanup(dir); }
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('does not flag when rate limiter is present', async () => {
|
|
648
|
+
const { APIFuzzer } = await import('../agents/api-fuzzer.js');
|
|
649
|
+
const agent = new APIFuzzer();
|
|
650
|
+
|
|
651
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shipsafe-api2-'));
|
|
652
|
+
const file = path.join(dir, 'app.js');
|
|
653
|
+
fs.writeFileSync(file, `import express from 'express';\nimport rateLimit from 'express-rate-limit';\nconst app = express();\napp.listen(3000);`);
|
|
654
|
+
|
|
655
|
+
try {
|
|
656
|
+
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
657
|
+
assert.ok(!findings.some(f => f.rule === 'API_NO_RATE_LIMIT'));
|
|
658
|
+
} finally { cleanup(dir); }
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it('detects secrets in OpenAPI examples', async () => {
|
|
662
|
+
const { APIFuzzer } = await import('../agents/api-fuzzer.js');
|
|
663
|
+
const agent = new APIFuzzer();
|
|
664
|
+
|
|
665
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shipsafe-oas-'));
|
|
666
|
+
const file = path.join(dir, 'openapi.yaml');
|
|
667
|
+
fs.writeFileSync(file, `openapi: 3.0.0\npaths:\n /users:\n get:\n parameters:\n - name: token\n example: sk-proj-abc123xyz`);
|
|
668
|
+
|
|
669
|
+
try {
|
|
670
|
+
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
671
|
+
assert.ok(findings.some(f => f.rule === 'OPENAPI_EXAMPLE_SECRETS' || f.rule === 'OPENAPI_NO_SECURITY'));
|
|
672
|
+
} finally { cleanup(dir); }
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
// =============================================================================
|
|
677
|
+
// AUTOFIX RULES
|
|
678
|
+
// =============================================================================
|
|
679
|
+
|
|
680
|
+
describe('Autofix Rules', () => {
|
|
681
|
+
it('fixes rejectUnauthorized: false', async () => {
|
|
682
|
+
const { applyAutofix } = await import('../utils/autofix-rules.js');
|
|
683
|
+
const line = ' rejectUnauthorized: false,';
|
|
684
|
+
const fixed = applyAutofix('TLS_REJECT_UNAUTHORIZED', line);
|
|
685
|
+
assert.ok(fixed.includes('rejectUnauthorized: true'));
|
|
686
|
+
assert.ok(!fixed.includes('false'));
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('fixes DEBUG = true', async () => {
|
|
690
|
+
const { applyAutofix } = await import('../utils/autofix-rules.js');
|
|
691
|
+
assert.ok(applyAutofix('DEBUG_MODE_PRODUCTION', 'DEBUG = true').includes('false'));
|
|
692
|
+
assert.ok(applyAutofix('DEBUG_MODE_PRODUCTION', 'DEBUG = True').includes('False'));
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it('fixes shell: true', async () => {
|
|
696
|
+
const { applyAutofix } = await import('../utils/autofix-rules.js');
|
|
697
|
+
const fixed = applyAutofix('CMD_INJECTION_SHELL_TRUE', ' shell: true');
|
|
698
|
+
assert.ok(fixed.includes('shell: false'));
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// =============================================================================
|
|
703
|
+
// CODE CONTEXT
|
|
704
|
+
// =============================================================================
|
|
705
|
+
|
|
706
|
+
describe('Code Context in Findings', () => {
|
|
707
|
+
it('attaches codeContext to findings from scanFileWithPatterns', async () => {
|
|
708
|
+
const { InjectionTester } = await import('../agents/injection-tester.js');
|
|
709
|
+
const agent = new InjectionTester();
|
|
710
|
+
const code = `const x = 1;\nconst y = 2;\nconst z = eval(userInput);\nconst w = 3;\nconst v = 4;`;
|
|
711
|
+
const { dir, file } = writeTempFile(code);
|
|
712
|
+
try {
|
|
713
|
+
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
714
|
+
const evalFinding = findings.find(f => f.rule && f.rule.includes('EVAL'));
|
|
715
|
+
if (evalFinding) {
|
|
716
|
+
assert.ok(evalFinding.codeContext, 'Finding should have codeContext');
|
|
717
|
+
assert.ok(Array.isArray(evalFinding.codeContext));
|
|
718
|
+
assert.ok(evalFinding.codeContext.some(c => c.highlight === true));
|
|
719
|
+
}
|
|
720
|
+
} finally { cleanup(dir); }
|
|
721
|
+
});
|
|
496
722
|
});
|
package/cli/agents/api-fuzzer.js
CHANGED
|
@@ -209,6 +209,49 @@ const PATTERNS = [
|
|
|
209
209
|
description: 'No body size limit configured. Large payloads can cause memory exhaustion.',
|
|
210
210
|
fix: 'Set limit: express.json({ limit: "1mb" })',
|
|
211
211
|
},
|
|
212
|
+
{
|
|
213
|
+
rule: 'API_NO_PAGINATION',
|
|
214
|
+
title: 'API: Query Without Limit',
|
|
215
|
+
regex: /\.find\s*\(\s*\{?\s*\}\s*\)/g,
|
|
216
|
+
severity: 'low',
|
|
217
|
+
cwe: 'CWE-400',
|
|
218
|
+
confidence: 'low',
|
|
219
|
+
description: 'Database query returns all records without limit. Can exhaust memory on large tables.',
|
|
220
|
+
fix: 'Add pagination: .find({}).limit(50).skip(offset)',
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
// OpenAPI/Swagger spec patterns
|
|
225
|
+
const OPENAPI_PATTERNS = [
|
|
226
|
+
{
|
|
227
|
+
rule: 'OPENAPI_NO_SECURITY',
|
|
228
|
+
title: 'OpenAPI: No Security Scheme Defined',
|
|
229
|
+
regex: /(?:openapi|swagger)\s*:\s*["']?[23]\./g,
|
|
230
|
+
severity: 'high',
|
|
231
|
+
cwe: 'CWE-306',
|
|
232
|
+
owasp: 'A07:2021',
|
|
233
|
+
confidence: 'medium',
|
|
234
|
+
description: 'OpenAPI spec detected without security schemes. API endpoints may be unprotected.',
|
|
235
|
+
fix: 'Add securityDefinitions/securitySchemes to your OpenAPI spec',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
rule: 'OPENAPI_HTTP_SERVER',
|
|
239
|
+
title: 'OpenAPI: Non-HTTPS Server URL',
|
|
240
|
+
regex: /url\s*:\s*["']?http:\/\//g,
|
|
241
|
+
severity: 'high',
|
|
242
|
+
cwe: 'CWE-319',
|
|
243
|
+
description: 'OpenAPI spec defines an HTTP (non-HTTPS) server URL. Use HTTPS in production.',
|
|
244
|
+
fix: 'Change server URL to use https://',
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
rule: 'OPENAPI_EXAMPLE_SECRETS',
|
|
248
|
+
title: 'OpenAPI: Secrets in Examples',
|
|
249
|
+
regex: /example\s*:\s*['"]?(?:sk-|sk_live_|ghp_|gho_|AKIA[0-9A-Z]|Bearer\s+ey[A-Za-z0-9])/g,
|
|
250
|
+
severity: 'critical',
|
|
251
|
+
cwe: 'CWE-798',
|
|
252
|
+
description: 'Real API keys or tokens found in OpenAPI spec examples.',
|
|
253
|
+
fix: 'Replace real secrets with placeholder values in examples',
|
|
254
|
+
},
|
|
212
255
|
];
|
|
213
256
|
|
|
214
257
|
export class APIFuzzer extends BaseAgent {
|
|
@@ -227,6 +270,74 @@ export class APIFuzzer extends BaseAgent {
|
|
|
227
270
|
for (const file of codeFiles) {
|
|
228
271
|
findings = findings.concat(this.scanFileWithPatterns(file, PATTERNS));
|
|
229
272
|
}
|
|
273
|
+
|
|
274
|
+
// ── Project-level: Rate limiting check ────────────────────────────────────
|
|
275
|
+
let hasExpressApp = false;
|
|
276
|
+
let hasRateLimiter = false;
|
|
277
|
+
let expressAppFile = null;
|
|
278
|
+
|
|
279
|
+
for (const file of codeFiles) {
|
|
280
|
+
const content = this.readFile(file);
|
|
281
|
+
if (!content) continue;
|
|
282
|
+
if (/(?:express\s*\(\)|app\.listen|createServer)\s*/.test(content)) {
|
|
283
|
+
hasExpressApp = true;
|
|
284
|
+
if (!expressAppFile) expressAppFile = file;
|
|
285
|
+
}
|
|
286
|
+
if (/(?:express-rate-limit|rate-limiter-flexible|@upstash\/ratelimit|limiter|bottleneck|rateLimit)/i.test(content)) {
|
|
287
|
+
hasRateLimiter = true;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (hasExpressApp && !hasRateLimiter && expressAppFile) {
|
|
292
|
+
findings.push(createFinding({
|
|
293
|
+
file: expressAppFile,
|
|
294
|
+
line: 0,
|
|
295
|
+
severity: 'medium',
|
|
296
|
+
category: 'api',
|
|
297
|
+
rule: 'API_NO_RATE_LIMIT',
|
|
298
|
+
title: 'API: No Rate Limiting Detected',
|
|
299
|
+
description: 'HTTP server detected without any rate-limiting library. APIs without rate limits are vulnerable to brute-force and DoS attacks.',
|
|
300
|
+
matched: 'No rate-limiting middleware found',
|
|
301
|
+
confidence: 'medium',
|
|
302
|
+
cwe: 'CWE-307',
|
|
303
|
+
owasp: 'A07:2021',
|
|
304
|
+
fix: 'Add rate limiting: npm i express-rate-limit && app.use(rateLimit({ windowMs: 15*60*1000, max: 100 }))',
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── OpenAPI/Swagger spec scanning ─────────────────────────────────────────
|
|
309
|
+
const specFiles = files.filter(f => {
|
|
310
|
+
const basename = path.basename(f).toLowerCase();
|
|
311
|
+
return /(?:openapi|swagger)\.(json|ya?ml)$/i.test(basename);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
for (const file of specFiles) {
|
|
315
|
+
const specFindings = this.scanFileWithPatterns(file, OPENAPI_PATTERNS);
|
|
316
|
+
// Check if spec has securitySchemes
|
|
317
|
+
const content = this.readFile(file);
|
|
318
|
+
if (content && /(?:openapi|swagger)\s*:\s*["']?[23]\./.test(content)) {
|
|
319
|
+
if (!/securitySchemes|securityDefinitions/i.test(content)) {
|
|
320
|
+
findings.push(createFinding({
|
|
321
|
+
file,
|
|
322
|
+
line: 0,
|
|
323
|
+
severity: 'high',
|
|
324
|
+
category: 'api',
|
|
325
|
+
rule: 'OPENAPI_NO_SECURITY',
|
|
326
|
+
title: 'OpenAPI: No Security Scheme Defined',
|
|
327
|
+
description: 'OpenAPI spec has no securitySchemes/securityDefinitions. API endpoints may be unprotected.',
|
|
328
|
+
matched: 'Missing securitySchemes',
|
|
329
|
+
confidence: 'high',
|
|
330
|
+
cwe: 'CWE-306',
|
|
331
|
+
fix: 'Add securitySchemes with Bearer token, API key, or OAuth2',
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Add other OpenAPI pattern matches (HTTP server, example secrets)
|
|
336
|
+
findings = findings.concat(
|
|
337
|
+
specFindings.filter(f => f.rule !== 'OPENAPI_NO_SECURITY')
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
230
341
|
return findings;
|
|
231
342
|
}
|
|
232
343
|
}
|