@lateos/npm-scan 0.18.3 → 1.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/CHANGELOG.md +32 -0
- package/README.md +864 -826
- package/VALIDATION.md +92 -0
- package/backend/cra.js +113 -21
- package/backend/db/pg-schema.sql +155 -0
- package/backend/db.js +18 -10
- package/backend/detectors/atk-001-lifecycle.js +5 -5
- package/backend/detectors/atk-002-obfusc.js +126 -47
- package/backend/detectors/atk-003-creds.js +8 -4
- package/backend/detectors/atk-004-persist.js +3 -3
- package/backend/detectors/atk-005-exfil.js +8 -4
- package/backend/detectors/atk-006-depconf.js +3 -3
- package/backend/detectors/atk-007-typosquat.js +64 -10
- package/backend/detectors/atk-008-tarball-tamper.js +6 -6
- package/backend/detectors/atk-009-dormant-trigger.js +9 -5
- package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
- package/backend/detectors/atk-011-transitive-prop.js +14 -13
- package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
- package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
- package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
- package/backend/detectors/axios-poisoning/index.js +77 -60
- package/backend/detectors/config/thresholds.js +111 -0
- package/backend/detectors/config/whitelist.json +74 -0
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
- package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
- package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
- package/backend/detectors/hf-impersonation/index.js +94 -31
- package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
- package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
- package/backend/detectors/hf-impersonation/simhash.js +2 -2
- package/backend/detectors/index.js +184 -31
- package/backend/detectors/lib/ast-patterns.js +24 -0
- package/backend/detectors/lib/entropy-analyzer.js +32 -0
- package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
- package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
- package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
- package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
- package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
- package/backend/detectors/megalodon/index.js +35 -25
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
- package/backend/detectors/mini-shai-hulud/index.js +63 -26
- package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
- package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
- package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
- package/backend/detectors/msh-supplement/index.js +78 -63
- package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
- package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
- package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
- package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
- package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
- package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
- package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
- package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
- package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
- package/backend/detectors/node-ipc-compromise/index.js +21 -15
- package/backend/detectors/tier1-binary-embed.js +138 -41
- package/backend/detectors/tier1-cloud-imds.js +57 -37
- package/backend/detectors/tier1-encrypted-c2.js +198 -0
- package/backend/detectors/tier1-infostealer.js +121 -68
- package/backend/detectors/tier1-lifecycle-hook.js +63 -23
- package/backend/detectors/tier1-maintainer-compromise.js +157 -0
- package/backend/detectors/tier1-metadata-spoof.js +92 -42
- package/backend/detectors/tier1-multistage-postinstall.js +46 -19
- package/backend/detectors/tier1-obfuscation-heuristics.js +184 -0
- package/backend/detectors/tier1-self-propagation.js +115 -0
- package/backend/detectors/tier1-slsa-attestation.js +12 -0
- package/backend/detectors/tier1-transitive-deps.js +182 -0
- package/backend/detectors/tier1-typosquat.js +129 -50
- package/backend/detectors/tier1-version-anomaly.js +223 -0
- package/backend/detectors/tier1-version-confusion.js +79 -59
- package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
- package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
- package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
- package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
- package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
- package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
- package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
- package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
- package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
- package/backend/detectors/trapdoor/index.js +19 -14
- package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
- package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
- package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
- package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
- package/backend/detectors.test.js +147 -0
- package/backend/fetch.js +37 -29
- package/backend/index.js +1 -1
- package/backend/license.js +20 -4
- package/backend/lockfile.js +60 -36
- package/backend/pdf.js +107 -28
- package/backend/policy.js +183 -56
- package/backend/provenance.js +28 -3
- package/backend/report.js +136 -70
- package/backend/sbom.js +33 -27
- package/backend/scripts/analyze-false-positives.js +152 -0
- package/backend/scripts/analyze-validation.js +157 -0
- package/backend/scripts/detect-false-positives.js +103 -0
- package/backend/scripts/fetch-top-packages.js +277 -0
- package/backend/scripts/validate-d10-d13.js +103 -0
- package/backend/scripts/validate-detectors.js +151 -0
- package/backend/siem/cef.js +23 -21
- package/backend/siem/ecs.js +3 -3
- package/backend/siem/index.js +1 -1
- package/backend/siem/qradar.js +3 -3
- package/backend/siem/sentinel.js +2 -2
- package/backend/tests-d5-enhanced.test.js +47 -0
- package/backend/tests-d6-version-anomaly.test.js +67 -0
- package/backend/tests-d6.test.js +126 -0
- package/backend/tests-d6c.test.js +119 -0
- package/backend/tests-d7-obfuscation.test.js +88 -0
- package/backend/tests.test.js +997 -0
- package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
- package/backend/vsix-scan/detectors/burst-publish.js +14 -7
- package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
- package/backend/vsix-scan/detectors/known-ioc.js +23 -8
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
- package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
- package/backend/vsix-scan/index.js +97 -41
- package/backend/vsix-scan/marketplace-client.js +29 -13
- package/cli/cli.js +154 -64
- package/package.json +36 -10
- package/.dockerignore +0 -20
- package/.husky/pre-commit +0 -1
- package/SECURITY.md +0 -73
- package/deploy/helm/npm-scan/Chart.yaml +0 -22
- package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
- package/deploy/helm/npm-scan/templates/api.yaml +0 -94
- package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
- package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
- package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
- package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
- package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
- package/deploy/helm/npm-scan/values.yaml +0 -103
- package/scripts/download-corpus.js +0 -30
- package/scripts/gen-mal-corpus.js +0 -35
- package/scripts/generate-campaign-fixtures.js +0 -170
- package/src/config/top-5000.json +0 -87
- package/test/fixtures/lockfiles/npm-lock.json +0 -69
- package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
- package/test/fixtures/lockfiles/yarn.lock +0 -104
- package/test/fixtures/mock-data.js +0 -69
package/VALIDATION.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# npm-scan Validation & Calibration Report
|
|
2
|
+
**Date**: 2026-06-03
|
|
3
|
+
**Detectors Validated**: TIER1-VERSION-ANOMALY, TIER1-OBFUSCATION-HEURISTICS, TIER1-LIFECYCLE-HOOK, TIER1-BINARY-EMBED, TIER1-TYPOSQUAT, TIER1-INFOSTEALER
|
|
4
|
+
**Campaigns Tested**: 3 real May 2026 attack vectors
|
|
5
|
+
**Packages Analyzed**: 7 (validation) + 1,000 (calibration)
|
|
6
|
+
|
|
7
|
+
## Campaign Detection Rates
|
|
8
|
+
|
|
9
|
+
| Campaign | Total | Detected | Rate | Expected | Matched | Match% |
|
|
10
|
+
|---|---|---|---|---|---|---|
|
|
11
|
+
| 176-Package Dependency Confusion | 3 | 3 | 100.0% | 7 | 5 | 71.4% |
|
|
12
|
+
| Mini Shai-Hulud (Obfuscated) | 2 | 2 | 100.0% | 5 | 3 | 60.0% |
|
|
13
|
+
| Bitwarden CLI Impersonation | 2 | 2 | 100.0% | 5 | 3 | 60.0% |
|
|
14
|
+
|
|
15
|
+
Every campaign package triggered at least one expected detector. Expected-match rate accounts for detectors that require file content (binary embed, infostealer exact patterns) not present in fixture metadata.
|
|
16
|
+
|
|
17
|
+
## Detector Performance (Validation)
|
|
18
|
+
|
|
19
|
+
| Detector | Hits | Expected | Precision | Avg Confidence |
|
|
20
|
+
|---|---|---|---|---|
|
|
21
|
+
| TIER1-LIFECYCLE-HOOK | 4 | 4 | 100.0% | 92.5 |
|
|
22
|
+
| TIER1-VERSION-ANOMALY | 3 | 3 | 100.0% | 92.0 |
|
|
23
|
+
| TIER1-OBFUSCATION-HEURISTICS | 2 | 2 | 100.0% | 80.0 |
|
|
24
|
+
| TIER1-TYPOSQUAT | 4 | 2 | 50.0% | 68.8 |
|
|
25
|
+
|
|
26
|
+
## Threshold Calibration
|
|
27
|
+
|
|
28
|
+
**Pre-calibration**: Global confidence threshold at 70
|
|
29
|
+
**Post-calibration**: Per-detector thresholds from analysis:
|
|
30
|
+
|
|
31
|
+
| Detector | Flag | Warn | Calibration Basis |
|
|
32
|
+
|---|---|---|---|
|
|
33
|
+
| TIER1-TYPOSQUAT | 85 | 70 | 46 edit-distance=1 FPs on scoped sub-packages eliminated at 85 |
|
|
34
|
+
| TIER1-OBFUSCATION-HEURISTICS | 75 | 60 | Bundlers/transpilers exempt via whitelist |
|
|
35
|
+
| TIER1-VERSION-ANOMALY | 72 | 60 | Sentinel patterns always flag at 92 |
|
|
36
|
+
| TIER1-BINARY-EMBED | 80 | 65 | Cross-platform binary sets rare in legit packages |
|
|
37
|
+
| TIER1-LIFECYCLE-HOOK | 65 | 50 | Moderate threshold for hooks |
|
|
38
|
+
| TIER1-INFOSTEALER | 72 | 55 | Pattern-based C2 signatures |
|
|
39
|
+
| TIER1-METADATA-SPOOF | 70 | 55 | Namespace/repo URL spoofing |
|
|
40
|
+
| TIER1-VERSION-CONFUSION | 75 | 60 | High-version heuristics |
|
|
41
|
+
| TIER1-CLOUD-IMDS | 80 | 65 | IMDS targeting rarely legitimate |
|
|
42
|
+
| TIER1-MULTISTAGE-POSTINSTALL | 75 | 60 | Two-stage download+exec |
|
|
43
|
+
| TIER1-SLSA-ATTESTATION | 85 | 70 | Placeholder |
|
|
44
|
+
|
|
45
|
+
**False Positive Calibration on Top 1,000 npm Packages**:
|
|
46
|
+
- Threshold 70: 47 FPs (4.7%) — all TIER1-TYPOSQUAT edit-distance=1 on scoped sub-packages
|
|
47
|
+
- Threshold 76: 2 FPs (0.2%) — @commitlint/read + preact (both whitelisted)
|
|
48
|
+
- Threshold 85: **0 FPs (0.0%)** — well under 2% target
|
|
49
|
+
|
|
50
|
+
**Whitelist Additions** (10 packages, 4 detectors):
|
|
51
|
+
- Bundlers/minifiers (webpack, terser, uglify-js, browserify, rollup, esbuild) → TIER1-OBFUSCATION-HEURISTICS
|
|
52
|
+
- Transpilers (typescript, @babel/core) → TIER1-OBFUSCATION-HEURISTICS
|
|
53
|
+
- Utility libs (lodash, underscore, crypto-js) → TIER1-OBFUSCATION-HEURISTICS
|
|
54
|
+
- Date lib (moment) → TIER1-BINARY-EMBED
|
|
55
|
+
- Scoped packages (preact, @commitlint/read) → TYPOSQUAT_VPMDHAJ / TIER1-TYPOSQUAT
|
|
56
|
+
|
|
57
|
+
## Campaign Coverage Analysis
|
|
58
|
+
|
|
59
|
+
### Campaign 1: Dependency Confusion (sentinel versions)
|
|
60
|
+
- TIER1-VERSION-ANOMALY catches all three (99.99.99/11.11.11/10.10.10) at 92% confidence
|
|
61
|
+
- TIER1-LIFECYCLE-HOOK fires on postinstall/preinstall scripts at 70-100%
|
|
62
|
+
- TIER1-BINARY-EMBED does not fire (no binary files in fixture data)
|
|
63
|
+
- Additional: TIER1-VERSION-CONFUSION fires at 85/65/65 (enhanced coverage)
|
|
64
|
+
|
|
65
|
+
### Campaign 2: Mini Shai-Hulud (obfuscation)
|
|
66
|
+
- TIER1-OBFUSCATION-HEURISTICS fires on both packages at 90% and 70%
|
|
67
|
+
- TIER1-LIFECYCLE-HOOK fires on @antv/core at 100%
|
|
68
|
+
- TIER1-INFOSTEALER does not fire (fixture scripts lack exact pattern signatures)
|
|
69
|
+
- Additional: TIER1-TYPOSQUAT fires at 75-100%, MINI_SHAI_HULUD campaign detector fires
|
|
70
|
+
|
|
71
|
+
### Campaign 3: Bitwarden Impersonation
|
|
72
|
+
- TIER1-LIFECYCLE-HOOK fires on second wave at 100%
|
|
73
|
+
- TIER1-TYPOSQUAT fires at 50% (below flag threshold of 85)
|
|
74
|
+
- TIER1-OBFUSCATION-HEURISTICS does not fire on first wave (script not sufficiently obfuscated)
|
|
75
|
+
- Additional: TRAPDOOR and TYPOSQUAT_VPMDHAJ detectors fire on second wave
|
|
76
|
+
|
|
77
|
+
## Test Suite
|
|
78
|
+
- 690 total tests (671 pass, 0 fail, 19 skip)
|
|
79
|
+
- Existing corpus tests (33 malicious + 50 clean) all pass with no regressions
|
|
80
|
+
- 15 new validation tests added (D5: 3, D6: 6, D7: 6)
|
|
81
|
+
|
|
82
|
+
## Recommendations
|
|
83
|
+
|
|
84
|
+
1. **Ship D6 + D7 as production Tier 1**: Detection rates and false positive rates justify GA
|
|
85
|
+
2. **Implement D8 (SLSA) when npm registry API stabilizes** (~Q4 2026)
|
|
86
|
+
3. **Add dynamic whitelist refresh**: Fetch top 1,000 packages monthly; re-calibrate annually
|
|
87
|
+
4. **Monitor typosquat FP rate**: 46 FPs eliminated at threshold 85; lower threshold increases FP risk
|
|
88
|
+
|
|
89
|
+
**Validation Artifacts**:
|
|
90
|
+
- `detection-rates.json`: Per-campaign, per-detector metrics
|
|
91
|
+
- `false-positives.jsonl`: Flagged packages from top 1K npm (0.0% FP rate at threshold 85)
|
|
92
|
+
- `fp-analysis.json`: Detector-level FP analysis and recommendations
|
package/backend/cra.js
CHANGED
|
@@ -1,32 +1,124 @@
|
|
|
1
1
|
export function generateCRA(scans) {
|
|
2
2
|
const atkMap = {};
|
|
3
3
|
for (const s of scans) {
|
|
4
|
-
for (const f of
|
|
4
|
+
for (const f of s.findings || []) {
|
|
5
5
|
const key = f.atk_id || f.id;
|
|
6
|
-
if (!atkMap[key])
|
|
6
|
+
if (!atkMap[key]) {
|
|
7
|
+
atkMap[key] = [];
|
|
8
|
+
}
|
|
7
9
|
atkMap[key].push({ ...f, package_name: s.package_name, version: s.version });
|
|
8
10
|
}
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
const CRA_ARTICLES = [
|
|
12
|
-
{
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
{
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
14
|
+
{
|
|
15
|
+
article: 'Art. 7',
|
|
16
|
+
title: 'Secure by default configuration',
|
|
17
|
+
atkId: 'ATK-001',
|
|
18
|
+
desc: 'Lifecycle hooks used for insecure defaults',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
article: 'Art. 7',
|
|
22
|
+
title: 'Secure by default configuration',
|
|
23
|
+
atkId: 'ATK-010',
|
|
24
|
+
desc: 'Anti-analysis in default state',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
article: 'Art. 10(1)',
|
|
28
|
+
title: 'Vulnerability disclosure',
|
|
29
|
+
atkId: 'ATK-008',
|
|
30
|
+
desc: 'Tarball integrity prevents disclosure accuracy',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
article: 'Art. 10(2)',
|
|
34
|
+
title: 'Known vulnerability reporting',
|
|
35
|
+
atkId: 'ATK-006',
|
|
36
|
+
desc: 'Dependency confusion undermines visibility',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
article: 'Art. 11',
|
|
40
|
+
title: 'Software Bill of Materials',
|
|
41
|
+
atkId: 'ATK-008',
|
|
42
|
+
desc: 'Integrity of SBOM entries must be verified',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
article: 'Art. 11',
|
|
46
|
+
title: 'Software Bill of Materials',
|
|
47
|
+
atkId: 'ATK-006',
|
|
48
|
+
desc: 'SBOM must reflect actual dependency graph',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
article: 'Annex I(1.1)',
|
|
52
|
+
title: 'No known exploitable vulnerabilities',
|
|
53
|
+
atkId: 'ATK-009',
|
|
54
|
+
desc: 'Conditional triggers may activate known vulns',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
article: 'Annex I(1.3)',
|
|
58
|
+
title: 'Least privilege',
|
|
59
|
+
atkId: 'ATK-003',
|
|
60
|
+
desc: 'Credential harvesting violates least privilege',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
article: 'Annex I(1.5)',
|
|
64
|
+
title: 'Limited attack surface',
|
|
65
|
+
atkId: 'ATK-002',
|
|
66
|
+
desc: 'Obfuscation increases attack surface',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
article: 'Annex I(1.5)',
|
|
70
|
+
title: 'Limited attack surface',
|
|
71
|
+
atkId: 'ATK-004',
|
|
72
|
+
desc: 'Persistence mechanisms expand attack surface',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
article: 'Annex I(1.5)',
|
|
76
|
+
title: 'Limited attack surface',
|
|
77
|
+
atkId: 'ATK-005',
|
|
78
|
+
desc: 'Network exfiltration expands attack surface',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
article: 'Annex I(2.1)',
|
|
82
|
+
title: 'Protection against unauthorized access',
|
|
83
|
+
atkId: 'ATK-003',
|
|
84
|
+
desc: 'Credential harvesting enables unauthorized access',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
article: 'Annex I(2.3)',
|
|
88
|
+
title: 'Data integrity',
|
|
89
|
+
atkId: 'ATK-008',
|
|
90
|
+
desc: 'Tarball tampering violates data integrity',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
article: 'Annex I(2.3)',
|
|
94
|
+
title: 'Data integrity',
|
|
95
|
+
atkId: 'ATK-011',
|
|
96
|
+
desc: 'Propagation attacks compromise data integrity',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
article: 'Annex I(3.2)',
|
|
100
|
+
title: 'Incident detection and reporting',
|
|
101
|
+
atkId: 'ATK-009',
|
|
102
|
+
desc: 'Conditional triggers evade incident detection',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
article: 'Annex I(3.2)',
|
|
106
|
+
title: 'Incident detection and reporting',
|
|
107
|
+
atkId: 'ATK-010',
|
|
108
|
+
desc: 'Sandbox evasion defeats incident detection',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
article: 'Annex I(3.3)',
|
|
112
|
+
title: 'Supply chain security monitoring',
|
|
113
|
+
atkId: 'ATK-011',
|
|
114
|
+
desc: 'Propagation requires SC monitoring',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
article: 'Annex I(3.3)',
|
|
118
|
+
title: 'Supply chain security monitoring',
|
|
119
|
+
atkId: 'ATK-007',
|
|
120
|
+
desc: 'Typosquatting undermines SC trust',
|
|
121
|
+
},
|
|
30
122
|
];
|
|
31
123
|
|
|
32
124
|
let rows = '';
|
|
@@ -66,4 +158,4 @@ th { background: #161b22; }
|
|
|
66
158
|
${body}
|
|
67
159
|
<p class="meta">EU Cyber Resilience Act (Regulation 2023/2841) mapped to ATK findings.</p>
|
|
68
160
|
</body></html>`;
|
|
69
|
-
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
-- PostgreSQL schema for hosted/team tier (premium)
|
|
2
|
+
-- Extends the SQLite schema with teams, users, RBAC, audit logs, webhooks
|
|
3
|
+
|
|
4
|
+
-- Extensions
|
|
5
|
+
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
6
|
+
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
|
7
|
+
|
|
8
|
+
-- Teams / Organizations
|
|
9
|
+
CREATE TABLE IF NOT EXISTS teams (
|
|
10
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
11
|
+
name TEXT NOT NULL,
|
|
12
|
+
slug TEXT UNIQUE NOT NULL,
|
|
13
|
+
license_edition TEXT NOT NULL DEFAULT 'community',
|
|
14
|
+
license_key TEXT,
|
|
15
|
+
license_expires_at TIMESTAMPTZ,
|
|
16
|
+
max_seats INTEGER NOT NULL DEFAULT 5,
|
|
17
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
18
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
-- Users
|
|
22
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
23
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
24
|
+
email TEXT UNIQUE NOT NULL,
|
|
25
|
+
name TEXT NOT NULL,
|
|
26
|
+
password_hash TEXT NOT NULL,
|
|
27
|
+
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
|
|
28
|
+
role TEXT NOT NULL CHECK (role IN ('admin', 'editor', 'viewer')) DEFAULT 'viewer',
|
|
29
|
+
last_login_at TIMESTAMPTZ,
|
|
30
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
-- Scans (extends SQLite scans with team ownership)
|
|
34
|
+
CREATE TABLE IF NOT EXISTS scans (
|
|
35
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
36
|
+
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
|
|
37
|
+
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
38
|
+
package_name TEXT NOT NULL,
|
|
39
|
+
version TEXT,
|
|
40
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
41
|
+
CHECK (status IN ('pending', 'fetching', 'analyzing', 'completed', 'failed')),
|
|
42
|
+
sbom_json JSONB,
|
|
43
|
+
findings_summary JSONB,
|
|
44
|
+
duration_ms INTEGER,
|
|
45
|
+
scanned_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
-- Findings
|
|
49
|
+
CREATE TABLE IF NOT EXISTS findings (
|
|
50
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
51
|
+
scan_id UUID NOT NULL REFERENCES scans(id) ON DELETE CASCADE,
|
|
52
|
+
atk_id TEXT NOT NULL,
|
|
53
|
+
severity TEXT NOT NULL CHECK (severity IN ('info', 'low', 'medium', 'high', 'critical')),
|
|
54
|
+
title TEXT,
|
|
55
|
+
description TEXT,
|
|
56
|
+
evidence TEXT,
|
|
57
|
+
mitigation TEXT,
|
|
58
|
+
file_path TEXT,
|
|
59
|
+
line_number INTEGER
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
-- Indexes
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_scans_team ON scans(team_id);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_scans_package ON scans(package_name);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_scans_status ON scans(status);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_scans_created ON scans(scanned_at DESC);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_findings_scan ON findings(scan_id);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_findings_atk ON findings(atk_id);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_findings_severity ON findings(severity);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_users_team ON users(team_id);
|
|
71
|
+
|
|
72
|
+
-- Audit log
|
|
73
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
74
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
75
|
+
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
|
76
|
+
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
77
|
+
action TEXT NOT NULL,
|
|
78
|
+
resource_type TEXT NOT NULL,
|
|
79
|
+
resource_id TEXT,
|
|
80
|
+
details JSONB,
|
|
81
|
+
ip_address INET,
|
|
82
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_audit_team ON audit_log(team_id, created_at DESC);
|
|
86
|
+
|
|
87
|
+
-- Webhooks
|
|
88
|
+
CREATE TABLE IF NOT EXISTS webhooks (
|
|
89
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
90
|
+
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
|
91
|
+
url TEXT NOT NULL,
|
|
92
|
+
secret TEXT NOT NULL DEFAULT encode(gen_random_bytes(32), 'hex'),
|
|
93
|
+
events TEXT[] NOT NULL DEFAULT '{}',
|
|
94
|
+
active BOOLEAN NOT NULL DEFAULT true,
|
|
95
|
+
last_triggered_at TIMESTAMPTZ,
|
|
96
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_webhooks_team ON webhooks(team_id);
|
|
100
|
+
|
|
101
|
+
-- API keys
|
|
102
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
103
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
104
|
+
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
|
105
|
+
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
106
|
+
name TEXT NOT NULL,
|
|
107
|
+
key_hash TEXT NOT NULL,
|
|
108
|
+
scopes TEXT[] NOT NULL DEFAULT '{}',
|
|
109
|
+
last_used_at TIMESTAMPTZ,
|
|
110
|
+
expires_at TIMESTAMPTZ,
|
|
111
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_team ON api_keys(team_id);
|
|
115
|
+
|
|
116
|
+
-- Session tokens
|
|
117
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
118
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
119
|
+
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
120
|
+
token_hash TEXT NOT NULL,
|
|
121
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
122
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|
|
126
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
|
|
127
|
+
|
|
128
|
+
-- Materialized view: package risk aggregation
|
|
129
|
+
CREATE MATERIALIZED VIEW IF NOT EXISTS package_risk AS
|
|
130
|
+
SELECT
|
|
131
|
+
s.package_name,
|
|
132
|
+
s.version,
|
|
133
|
+
COUNT(DISTINCT f.id) AS finding_count,
|
|
134
|
+
COUNT(DISTINCT f.id) FILTER (WHERE f.severity IN ('high', 'critical')) AS high_crit_count,
|
|
135
|
+
ARRAY_AGG(DISTINCT f.atk_id) AS atk_ids,
|
|
136
|
+
MAX(s.scanned_at) AS last_scanned
|
|
137
|
+
FROM scans s
|
|
138
|
+
JOIN findings f ON f.scan_id = s.id
|
|
139
|
+
WHERE s.status = 'completed'
|
|
140
|
+
GROUP BY s.package_name, s.version;
|
|
141
|
+
|
|
142
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_package_risk_pkg ON package_risk(package_name, version);
|
|
143
|
+
|
|
144
|
+
-- Function: touch updated_at
|
|
145
|
+
CREATE OR REPLACE FUNCTION touch_updated_at()
|
|
146
|
+
RETURNS TRIGGER AS $$
|
|
147
|
+
BEGIN
|
|
148
|
+
NEW.updated_at = NOW();
|
|
149
|
+
RETURN NEW;
|
|
150
|
+
END;
|
|
151
|
+
$$ LANGUAGE plpgsql;
|
|
152
|
+
|
|
153
|
+
CREATE TRIGGER trg_teams_updated_at
|
|
154
|
+
BEFORE UPDATE ON teams
|
|
155
|
+
FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
|
package/backend/db.js
CHANGED
|
@@ -11,8 +11,12 @@ let db = null;
|
|
|
11
11
|
let initPromise = null;
|
|
12
12
|
|
|
13
13
|
async function ensureInit() {
|
|
14
|
-
if (db)
|
|
15
|
-
|
|
14
|
+
if (db) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (initPromise) {
|
|
18
|
+
return initPromise;
|
|
19
|
+
}
|
|
16
20
|
initPromise = (async () => {
|
|
17
21
|
const SQL = await initSqlJs();
|
|
18
22
|
if (fs.existsSync(DB_PATH)) {
|
|
@@ -29,7 +33,9 @@ async function ensureInit() {
|
|
|
29
33
|
|
|
30
34
|
function queryAll(sql, params = []) {
|
|
31
35
|
const stmt = db.prepare(sql);
|
|
32
|
-
if (params.length)
|
|
36
|
+
if (params.length) {
|
|
37
|
+
stmt.bind(params);
|
|
38
|
+
}
|
|
33
39
|
const rows = [];
|
|
34
40
|
while (stmt.step()) {
|
|
35
41
|
rows.push(stmt.getAsObject());
|
|
@@ -43,7 +49,7 @@ function queryOne(sql, params = []) {
|
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
function lastId() {
|
|
46
|
-
const r = db.exec(
|
|
52
|
+
const r = db.exec('SELECT last_insert_rowid()');
|
|
47
53
|
return Number(r[0].values[0][0]);
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -53,9 +59,11 @@ function persist() {
|
|
|
53
59
|
|
|
54
60
|
export async function saveScan(pkgName, version = 'latest', findings = []) {
|
|
55
61
|
await ensureInit();
|
|
56
|
-
db.run(
|
|
62
|
+
db.run('INSERT INTO scans (package_name, version) VALUES (?, ?)', [pkgName, version]);
|
|
57
63
|
const scanId = lastId();
|
|
58
|
-
const stmt = db.prepare(
|
|
64
|
+
const stmt = db.prepare(
|
|
65
|
+
'INSERT INTO findings (scan_id, atk_id, severity, description, evidence) VALUES (?, ?, ?, ?, ?)'
|
|
66
|
+
);
|
|
59
67
|
for (const f of findings) {
|
|
60
68
|
stmt.run([scanId, f.id, f.severity, f.title || f.description, f.evidence || '']);
|
|
61
69
|
}
|
|
@@ -66,17 +74,17 @@ export async function saveScan(pkgName, version = 'latest', findings = []) {
|
|
|
66
74
|
|
|
67
75
|
export async function getRecentScans(limit = 10) {
|
|
68
76
|
await ensureInit();
|
|
69
|
-
return queryAll(
|
|
77
|
+
return queryAll('SELECT * FROM scans ORDER BY scanned_at DESC LIMIT ?', [limit]);
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
export async function getFindings(scanId) {
|
|
73
81
|
await ensureInit();
|
|
74
|
-
return queryAll(
|
|
82
|
+
return queryAll('SELECT * FROM findings WHERE scan_id = ?', [scanId]);
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
export async function getScan(scanId) {
|
|
78
86
|
await ensureInit();
|
|
79
|
-
return queryOne(
|
|
87
|
+
return queryOne('SELECT * FROM scans WHERE id = ?', [scanId]);
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
export async function close() {
|
|
@@ -86,4 +94,4 @@ export async function close() {
|
|
|
86
94
|
db = null;
|
|
87
95
|
initPromise = null;
|
|
88
96
|
}
|
|
89
|
-
}
|
|
97
|
+
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
export async function scan(pkgJson,
|
|
1
|
+
export async function scan(pkgJson, _files = []) {
|
|
2
2
|
const findings = [];
|
|
3
3
|
const scripts = pkgJson.scripts || {};
|
|
4
|
-
const suspicious = Object.keys(scripts).filter(s => /pre|post|install/i.test(s));
|
|
4
|
+
const suspicious = Object.keys(scripts).filter((s) => /pre|post|install/i.test(s));
|
|
5
5
|
if (suspicious.length) {
|
|
6
|
-
const content = suspicious.map(s => scripts[s]).join(' ');
|
|
6
|
+
const content = suspicious.map((s) => scripts[s]).join(' ');
|
|
7
7
|
if (/curl|wget|sh |bash |\.sh|exfil|steal|pwn|c2|pastebin/i.test(content)) {
|
|
8
8
|
findings.push({
|
|
9
9
|
id: 'ATK-001',
|
|
10
10
|
severity: 'high',
|
|
11
11
|
title: 'Malicious lifecycle scripts',
|
|
12
12
|
description: 'Suspicious install hooks',
|
|
13
|
-
evidence: suspicious.join(', ')
|
|
13
|
+
evidence: suspicious.join(', '),
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
return findings;
|
|
18
|
-
}
|
|
18
|
+
}
|