@lateos/npm-scan 0.16.4 → 0.16.5
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/.dockerignore +20 -20
- package/.husky/pre-commit +1 -1
- package/CHANGELOG.md +199 -199
- package/LICENSING.md +19 -19
- package/README.de.md +708 -708
- package/README.fr.md +707 -707
- package/README.ja.md +704 -704
- package/README.md +826 -826
- package/README.zh.md +708 -708
- package/SECURITY.md +72 -72
- package/backend/cra.js +68 -68
- package/backend/db/schema.sql +32 -32
- package/backend/db.js +88 -88
- package/backend/detectors/atk-001-lifecycle.js +17 -17
- package/backend/detectors/atk-002-obfusc.js +261 -261
- package/backend/detectors/atk-003-creds.js +13 -13
- package/backend/detectors/atk-004-persist.js +13 -13
- package/backend/detectors/atk-005-exfil.js +13 -13
- package/backend/detectors/atk-006-depconf.js +14 -14
- package/backend/detectors/atk-007-typosquat.js +34 -34
- package/backend/detectors/atk-008-tarball-tamper.js +91 -91
- package/backend/detectors/atk-009-dormant-trigger.js +62 -62
- package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
- package/backend/detectors/atk-011-transitive-prop.js +76 -76
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
- package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
- package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
- package/backend/detectors/hf-impersonation/index.js +396 -396
- package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
- package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
- package/backend/detectors/hf-impersonation/simhash.js +46 -46
- package/backend/detectors/index.js +75 -44
- package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
- package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
- package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
- package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
- package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
- package/backend/detectors/megalodon/index.js +80 -80
- package/backend/detectors/megalodon/types.js +9 -9
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
- package/backend/detectors/mini-shai-hulud/index.js +118 -118
- package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
- package/backend/detectors/tier1-binary-embed.js +219 -0
- package/backend/detectors/tier1-infostealer.js +280 -0
- package/backend/detectors/tier1-lifecycle-hook.js +176 -0
- package/backend/detectors/tier1-metadata-spoof.js +180 -0
- package/backend/detectors/tier1-typosquat.js +219 -0
- package/backend/fetch.js +175 -175
- package/backend/index.js +4 -4
- package/backend/license.js +89 -89
- package/backend/lockfile.js +379 -379
- package/backend/pdf.js +245 -245
- package/backend/policy.js +193 -176
- package/backend/report.js +254 -254
- package/backend/sbom.js +66 -66
- package/backend/siem/cef.js +32 -32
- package/backend/siem/ecs.js +40 -40
- package/backend/siem/index.js +18 -18
- package/backend/siem/qradar.js +56 -56
- package/backend/siem/sentinel.js +27 -27
- package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
- package/backend/vsix-scan/detectors/burst-publish.js +52 -52
- package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
- package/backend/vsix-scan/detectors/known-ioc.js +105 -105
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
- package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
- package/backend/vsix-scan/index.js +183 -183
- package/backend/vsix-scan/marketplace-client.js +145 -145
- package/backend/vsix-scan/vsix-iocs.json +31 -31
- package/cli/cli.js +458 -458
- package/deploy/helm/npm-scan/Chart.yaml +21 -21
- package/deploy/helm/npm-scan/templates/_helpers.tpl +8 -8
- package/deploy/helm/npm-scan/templates/api.yaml +93 -93
- package/deploy/helm/npm-scan/templates/ingress.yaml +27 -27
- package/deploy/helm/npm-scan/templates/postgresql.yaml +66 -66
- package/deploy/helm/npm-scan/templates/secrets.yaml +18 -18
- package/deploy/helm/npm-scan/templates/worker.yaml +31 -31
- package/deploy/helm/npm-scan/values.byoc.yaml +74 -74
- package/deploy/helm/npm-scan/values.yaml +102 -102
- package/package.json +57 -57
- package/scripts/download-corpus.js +30 -30
- package/scripts/gen-mal-corpus.js +34 -34
- package/scripts/generate-campaign-fixtures.js +170 -0
- package/src/config/top-5000.json +87 -0
- package/test/fixtures/lockfiles/npm-lock.json +68 -68
- package/test/fixtures/lockfiles/pnpm-lock.yaml +117 -117
- package/test/fixtures/lockfiles/yarn.lock +103 -103
- package/test/fixtures/mock-data.js +69 -69
package/SECURITY.md
CHANGED
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
# Security Policy
|
|
2
|
-
|
|
3
|
-
## Supported Versions
|
|
4
|
-
|
|
5
|
-
Only the **latest published minor version** on npm receives security patches. Keep `@lateos/npm-scan` up to date:
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm update -g @lateos/npm-scan
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
| Version | Supported |
|
|
12
|
-
|---------|-----------|
|
|
13
|
-
| 0.9.x | ✅ Active |
|
|
14
|
-
| < 0.9 | ❌ |
|
|
15
|
-
|
|
16
|
-
## Reporting a Vulnerability
|
|
17
|
-
|
|
18
|
-
Use **GitHub Private Vulnerability Reporting**:
|
|
19
|
-
|
|
20
|
-
1. Go to [github.com/lateos-ai/npm-scan/security/advisories/new](https://github.com/lateos-ai/npm-scan/security/advisories/new)
|
|
21
|
-
2. Describe the vulnerability in detail (ideally with a proof of concept)
|
|
22
|
-
3. Allow **72 hours** for an initial acknowledgment
|
|
23
|
-
|
|
24
|
-
For encrypted follow-up outside of GitHub, use our PGP key:
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
Fingerprint: 1BC6 998B 879B BDE0 D778 629E D9CF F5EF 1F7C 557B
|
|
28
|
-
Key ID: 1F7C557B
|
|
29
|
-
Email: leo@lateos.ai
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Scope
|
|
33
|
-
|
|
34
|
-
**In scope:**
|
|
35
|
-
- Detector logic (ATK-001 through ATK-011)
|
|
36
|
-
- Code execution in the scanner engine (`backend/fetch.js`, `cli/cli.js`)
|
|
37
|
-
- CI/CD pipeline and publish process (provenance bypass, supply chain)
|
|
38
|
-
- Configuration injection via `policy.yaml` or command-line flags
|
|
39
|
-
|
|
40
|
-
**Out of scope:**
|
|
41
|
-
- CVEs in third-party dependencies — report upstream
|
|
42
|
-
- Vulnerabilities in the npm registry itself — report to npm
|
|
43
|
-
- Malicious packages detected by the scanner (that's working as designed)
|
|
44
|
-
|
|
45
|
-
## Security Practices
|
|
46
|
-
|
|
47
|
-
`@lateos/npm-scan` follows these practices to protect its own supply chain:
|
|
48
|
-
|
|
49
|
-
- **Sigstore provenance** on every npm publish — verifiable via `npm view @lateos/npm-scan provenance`
|
|
50
|
-
- **Self-scanning in CI** — every commit scans the project's own `package-lock.json` for the full ATK taxonomy
|
|
51
|
-
- **SBOM per release** — CycloneDX and SPDX 2.3 Bill of Materials published with every version
|
|
52
|
-
- **2FA** enforced on the npm publisher account
|
|
53
|
-
- **Docker multi-arch images** signed and pushed via CI, not manually
|
|
54
|
-
- **All code public** — no security-by-obscurity
|
|
55
|
-
|
|
56
|
-
## Self-Scanning
|
|
57
|
-
|
|
58
|
-
As a supply chain security scanner, `@lateos/npm-scan` dogfoods its own detectors. Every CI run executes:
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
npx @lateos/npm-scan scan-lockfile --fail-on medium
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
If a future update to a dependency triggers one of our detectors (e.g., typosquat, obfuscated lifecycle script), the build **fails** before the change reaches npm.
|
|
65
|
-
|
|
66
|
-
## Safe Harbor
|
|
67
|
-
|
|
68
|
-
We consider security research conducted under this policy as authorized and will not pursue legal action against researchers who:
|
|
69
|
-
|
|
70
|
-
- Report vulnerabilities through GitHub Private Vulnerability Reporting
|
|
71
|
-
- Do not access or modify user data beyond what's necessary to demonstrate the vulnerability
|
|
72
|
-
- Do not exploit the vulnerability beyond demonstrating it
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Only the **latest published minor version** on npm receives security patches. Keep `@lateos/npm-scan` up to date:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm update -g @lateos/npm-scan
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
| Version | Supported |
|
|
12
|
+
|---------|-----------|
|
|
13
|
+
| 0.9.x | ✅ Active |
|
|
14
|
+
| < 0.9 | ❌ |
|
|
15
|
+
|
|
16
|
+
## Reporting a Vulnerability
|
|
17
|
+
|
|
18
|
+
Use **GitHub Private Vulnerability Reporting**:
|
|
19
|
+
|
|
20
|
+
1. Go to [github.com/lateos-ai/npm-scan/security/advisories/new](https://github.com/lateos-ai/npm-scan/security/advisories/new)
|
|
21
|
+
2. Describe the vulnerability in detail (ideally with a proof of concept)
|
|
22
|
+
3. Allow **72 hours** for an initial acknowledgment
|
|
23
|
+
|
|
24
|
+
For encrypted follow-up outside of GitHub, use our PGP key:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Fingerprint: 1BC6 998B 879B BDE0 D778 629E D9CF F5EF 1F7C 557B
|
|
28
|
+
Key ID: 1F7C557B
|
|
29
|
+
Email: leo@lateos.ai
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Scope
|
|
33
|
+
|
|
34
|
+
**In scope:**
|
|
35
|
+
- Detector logic (ATK-001 through ATK-011)
|
|
36
|
+
- Code execution in the scanner engine (`backend/fetch.js`, `cli/cli.js`)
|
|
37
|
+
- CI/CD pipeline and publish process (provenance bypass, supply chain)
|
|
38
|
+
- Configuration injection via `policy.yaml` or command-line flags
|
|
39
|
+
|
|
40
|
+
**Out of scope:**
|
|
41
|
+
- CVEs in third-party dependencies — report upstream
|
|
42
|
+
- Vulnerabilities in the npm registry itself — report to npm
|
|
43
|
+
- Malicious packages detected by the scanner (that's working as designed)
|
|
44
|
+
|
|
45
|
+
## Security Practices
|
|
46
|
+
|
|
47
|
+
`@lateos/npm-scan` follows these practices to protect its own supply chain:
|
|
48
|
+
|
|
49
|
+
- **Sigstore provenance** on every npm publish — verifiable via `npm view @lateos/npm-scan provenance`
|
|
50
|
+
- **Self-scanning in CI** — every commit scans the project's own `package-lock.json` for the full ATK taxonomy
|
|
51
|
+
- **SBOM per release** — CycloneDX and SPDX 2.3 Bill of Materials published with every version
|
|
52
|
+
- **2FA** enforced on the npm publisher account
|
|
53
|
+
- **Docker multi-arch images** signed and pushed via CI, not manually
|
|
54
|
+
- **All code public** — no security-by-obscurity
|
|
55
|
+
|
|
56
|
+
## Self-Scanning
|
|
57
|
+
|
|
58
|
+
As a supply chain security scanner, `@lateos/npm-scan` dogfoods its own detectors. Every CI run executes:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx @lateos/npm-scan scan-lockfile --fail-on medium
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
If a future update to a dependency triggers one of our detectors (e.g., typosquat, obfuscated lifecycle script), the build **fails** before the change reaches npm.
|
|
65
|
+
|
|
66
|
+
## Safe Harbor
|
|
67
|
+
|
|
68
|
+
We consider security research conducted under this policy as authorized and will not pursue legal action against researchers who:
|
|
69
|
+
|
|
70
|
+
- Report vulnerabilities through GitHub Private Vulnerability Reporting
|
|
71
|
+
- Do not access or modify user data beyond what's necessary to demonstrate the vulnerability
|
|
72
|
+
- Do not exploit the vulnerability beyond demonstrating it
|
|
73
73
|
- Act in good faith to improve the security of the project
|
package/backend/cra.js
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
export function generateCRA(scans) {
|
|
2
|
-
const atkMap = {};
|
|
3
|
-
for (const s of scans) {
|
|
4
|
-
for (const f of (s.findings || [])) {
|
|
5
|
-
const key = f.atk_id || f.id;
|
|
6
|
-
if (!atkMap[key]) atkMap[key] = [];
|
|
7
|
-
atkMap[key].push({ ...f, package_name: s.package_name, version: s.version });
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const CRA_ARTICLES = [
|
|
12
|
-
{ article: 'Art. 7', title: 'Secure by default configuration', atkId: 'ATK-001', desc: 'Lifecycle hooks used for insecure defaults' },
|
|
13
|
-
{ article: 'Art. 7', title: 'Secure by default configuration', atkId: 'ATK-010', desc: 'Anti-analysis in default state' },
|
|
14
|
-
{ article: 'Art. 10(1)', title: 'Vulnerability disclosure', atkId: 'ATK-008', desc: 'Tarball integrity prevents disclosure accuracy' },
|
|
15
|
-
{ article: 'Art. 10(2)', title: 'Known vulnerability reporting', atkId: 'ATK-006', desc: 'Dependency confusion undermines visibility' },
|
|
16
|
-
{ article: 'Art. 11', title: 'Software Bill of Materials', atkId: 'ATK-008', desc: 'Integrity of SBOM entries must be verified' },
|
|
17
|
-
{ article: 'Art. 11', title: 'Software Bill of Materials', atkId: 'ATK-006', desc: 'SBOM must reflect actual dependency graph' },
|
|
18
|
-
{ article: 'Annex I(1.1)', title: 'No known exploitable vulnerabilities', atkId: 'ATK-009', desc: 'Conditional triggers may activate known vulns' },
|
|
19
|
-
{ article: 'Annex I(1.3)', title: 'Least privilege', atkId: 'ATK-003', desc: 'Credential harvesting violates least privilege' },
|
|
20
|
-
{ article: 'Annex I(1.5)', title: 'Limited attack surface', atkId: 'ATK-002', desc: 'Obfuscation increases attack surface' },
|
|
21
|
-
{ article: 'Annex I(1.5)', title: 'Limited attack surface', atkId: 'ATK-004', desc: 'Persistence mechanisms expand attack surface' },
|
|
22
|
-
{ article: 'Annex I(1.5)', title: 'Limited attack surface', atkId: 'ATK-005', desc: 'Network exfiltration expands attack surface' },
|
|
23
|
-
{ article: 'Annex I(2.1)', title: 'Protection against unauthorized access', atkId: 'ATK-003', desc: 'Credential harvesting enables unauthorized access' },
|
|
24
|
-
{ article: 'Annex I(2.3)', title: 'Data integrity', atkId: 'ATK-008', desc: 'Tarball tampering violates data integrity' },
|
|
25
|
-
{ article: 'Annex I(2.3)', title: 'Data integrity', atkId: 'ATK-011', desc: 'Propagation attacks compromise data integrity' },
|
|
26
|
-
{ article: 'Annex I(3.2)', title: 'Incident detection and reporting', atkId: 'ATK-009', desc: 'Conditional triggers evade incident detection' },
|
|
27
|
-
{ article: 'Annex I(3.2)', title: 'Incident detection and reporting', atkId: 'ATK-010', desc: 'Sandbox evasion defeats incident detection' },
|
|
28
|
-
{ article: 'Annex I(3.3)', title: 'Supply chain security monitoring', atkId: 'ATK-011', desc: 'Propagation requires SC monitoring' },
|
|
29
|
-
{ article: 'Annex I(3.3)', title: 'Supply chain security monitoring', atkId: 'ATK-007', desc: 'Typosquatting undermines SC trust' },
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
let rows = '';
|
|
33
|
-
for (const { article, title, atkId, desc } of CRA_ARTICLES) {
|
|
34
|
-
const findings = atkMap[atkId] || [];
|
|
35
|
-
const status = findings.length > 0 ? 'fail' : 'pass';
|
|
36
|
-
const colorClass = status === 'pass' ? 'pass' : 'fail';
|
|
37
|
-
const label = findings.length > 0 ? `${findings.length} finding(s)` : 'No findings';
|
|
38
|
-
rows += `<tr><td>${article}</td><td>${title}</td><td>${desc}</td><td class="nist-${colorClass}">${label}</td><td>${atkId}</td></tr>`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return `<h2>EU CRA Compliance Summary</h2>
|
|
42
|
-
<table>
|
|
43
|
-
<thead><tr><th>CRA Article</th><th>Requirement</th><th>Relevance</th><th>Status</th><th>ATK</th></tr></thead>
|
|
44
|
-
<tbody>${rows}</tbody>
|
|
45
|
-
</table>`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function generateCRAHTML(scans) {
|
|
49
|
-
const body = generateCRA(scans);
|
|
50
|
-
return `<!DOCTYPE html>
|
|
51
|
-
<html lang="en">
|
|
52
|
-
<head><meta charset="UTF-8"><title>EU CRA Compliance Report</title>
|
|
53
|
-
<style>
|
|
54
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 960px; margin: 0 auto; padding: 20px; background: #0d1117; color: #c9d1d9; }
|
|
55
|
-
h1, h2 { color: #58a6ff; }
|
|
56
|
-
table { width: 100%; border-collapse: collapse; margin: 12px 0; }
|
|
57
|
-
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #30363d; }
|
|
58
|
-
th { background: #161b22; }
|
|
59
|
-
.nist-pass { background: #1b3a1b; color: #7ee787; }
|
|
60
|
-
.nist-fail { background: #3a1b1b; color: #ff7b72; }
|
|
61
|
-
.meta { color: #8b949e; font-size: 13px; margin-top: 30px; }
|
|
62
|
-
</style></head>
|
|
63
|
-
<body>
|
|
64
|
-
<h1>npm-scan EU CRA Compliance Report</h1>
|
|
65
|
-
<p>Generated ${new Date().toISOString()} | npm-scan v${process.env.npm_package_version || '0.4.0'}</p>
|
|
66
|
-
${body}
|
|
67
|
-
<p class="meta">EU Cyber Resilience Act (Regulation 2023/2841) mapped to ATK findings.</p>
|
|
68
|
-
</body></html>`;
|
|
1
|
+
export function generateCRA(scans) {
|
|
2
|
+
const atkMap = {};
|
|
3
|
+
for (const s of scans) {
|
|
4
|
+
for (const f of (s.findings || [])) {
|
|
5
|
+
const key = f.atk_id || f.id;
|
|
6
|
+
if (!atkMap[key]) atkMap[key] = [];
|
|
7
|
+
atkMap[key].push({ ...f, package_name: s.package_name, version: s.version });
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const CRA_ARTICLES = [
|
|
12
|
+
{ article: 'Art. 7', title: 'Secure by default configuration', atkId: 'ATK-001', desc: 'Lifecycle hooks used for insecure defaults' },
|
|
13
|
+
{ article: 'Art. 7', title: 'Secure by default configuration', atkId: 'ATK-010', desc: 'Anti-analysis in default state' },
|
|
14
|
+
{ article: 'Art. 10(1)', title: 'Vulnerability disclosure', atkId: 'ATK-008', desc: 'Tarball integrity prevents disclosure accuracy' },
|
|
15
|
+
{ article: 'Art. 10(2)', title: 'Known vulnerability reporting', atkId: 'ATK-006', desc: 'Dependency confusion undermines visibility' },
|
|
16
|
+
{ article: 'Art. 11', title: 'Software Bill of Materials', atkId: 'ATK-008', desc: 'Integrity of SBOM entries must be verified' },
|
|
17
|
+
{ article: 'Art. 11', title: 'Software Bill of Materials', atkId: 'ATK-006', desc: 'SBOM must reflect actual dependency graph' },
|
|
18
|
+
{ article: 'Annex I(1.1)', title: 'No known exploitable vulnerabilities', atkId: 'ATK-009', desc: 'Conditional triggers may activate known vulns' },
|
|
19
|
+
{ article: 'Annex I(1.3)', title: 'Least privilege', atkId: 'ATK-003', desc: 'Credential harvesting violates least privilege' },
|
|
20
|
+
{ article: 'Annex I(1.5)', title: 'Limited attack surface', atkId: 'ATK-002', desc: 'Obfuscation increases attack surface' },
|
|
21
|
+
{ article: 'Annex I(1.5)', title: 'Limited attack surface', atkId: 'ATK-004', desc: 'Persistence mechanisms expand attack surface' },
|
|
22
|
+
{ article: 'Annex I(1.5)', title: 'Limited attack surface', atkId: 'ATK-005', desc: 'Network exfiltration expands attack surface' },
|
|
23
|
+
{ article: 'Annex I(2.1)', title: 'Protection against unauthorized access', atkId: 'ATK-003', desc: 'Credential harvesting enables unauthorized access' },
|
|
24
|
+
{ article: 'Annex I(2.3)', title: 'Data integrity', atkId: 'ATK-008', desc: 'Tarball tampering violates data integrity' },
|
|
25
|
+
{ article: 'Annex I(2.3)', title: 'Data integrity', atkId: 'ATK-011', desc: 'Propagation attacks compromise data integrity' },
|
|
26
|
+
{ article: 'Annex I(3.2)', title: 'Incident detection and reporting', atkId: 'ATK-009', desc: 'Conditional triggers evade incident detection' },
|
|
27
|
+
{ article: 'Annex I(3.2)', title: 'Incident detection and reporting', atkId: 'ATK-010', desc: 'Sandbox evasion defeats incident detection' },
|
|
28
|
+
{ article: 'Annex I(3.3)', title: 'Supply chain security monitoring', atkId: 'ATK-011', desc: 'Propagation requires SC monitoring' },
|
|
29
|
+
{ article: 'Annex I(3.3)', title: 'Supply chain security monitoring', atkId: 'ATK-007', desc: 'Typosquatting undermines SC trust' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
let rows = '';
|
|
33
|
+
for (const { article, title, atkId, desc } of CRA_ARTICLES) {
|
|
34
|
+
const findings = atkMap[atkId] || [];
|
|
35
|
+
const status = findings.length > 0 ? 'fail' : 'pass';
|
|
36
|
+
const colorClass = status === 'pass' ? 'pass' : 'fail';
|
|
37
|
+
const label = findings.length > 0 ? `${findings.length} finding(s)` : 'No findings';
|
|
38
|
+
rows += `<tr><td>${article}</td><td>${title}</td><td>${desc}</td><td class="nist-${colorClass}">${label}</td><td>${atkId}</td></tr>`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return `<h2>EU CRA Compliance Summary</h2>
|
|
42
|
+
<table>
|
|
43
|
+
<thead><tr><th>CRA Article</th><th>Requirement</th><th>Relevance</th><th>Status</th><th>ATK</th></tr></thead>
|
|
44
|
+
<tbody>${rows}</tbody>
|
|
45
|
+
</table>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function generateCRAHTML(scans) {
|
|
49
|
+
const body = generateCRA(scans);
|
|
50
|
+
return `<!DOCTYPE html>
|
|
51
|
+
<html lang="en">
|
|
52
|
+
<head><meta charset="UTF-8"><title>EU CRA Compliance Report</title>
|
|
53
|
+
<style>
|
|
54
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 960px; margin: 0 auto; padding: 20px; background: #0d1117; color: #c9d1d9; }
|
|
55
|
+
h1, h2 { color: #58a6ff; }
|
|
56
|
+
table { width: 100%; border-collapse: collapse; margin: 12px 0; }
|
|
57
|
+
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #30363d; }
|
|
58
|
+
th { background: #161b22; }
|
|
59
|
+
.nist-pass { background: #1b3a1b; color: #7ee787; }
|
|
60
|
+
.nist-fail { background: #3a1b1b; color: #ff7b72; }
|
|
61
|
+
.meta { color: #8b949e; font-size: 13px; margin-top: 30px; }
|
|
62
|
+
</style></head>
|
|
63
|
+
<body>
|
|
64
|
+
<h1>npm-scan EU CRA Compliance Report</h1>
|
|
65
|
+
<p>Generated ${new Date().toISOString()} | npm-scan v${process.env.npm_package_version || '0.4.0'}</p>
|
|
66
|
+
${body}
|
|
67
|
+
<p class="meta">EU Cyber Resilience Act (Regulation 2023/2841) mapped to ATK findings.</p>
|
|
68
|
+
</body></html>`;
|
|
69
69
|
}
|
package/backend/db/schema.sql
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
-- SQLite schema for local CLI mode (free tier)
|
|
2
|
-
-- Tables: scans, findings (ATK-linked)
|
|
3
|
-
|
|
4
|
-
CREATE TABLE IF NOT EXISTS scans (
|
|
5
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6
|
-
package_name TEXT NOT NULL,
|
|
7
|
-
version TEXT,
|
|
8
|
-
scanned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
9
|
-
status TEXT DEFAULT 'completed',
|
|
10
|
-
sbom_json TEXT
|
|
11
|
-
);
|
|
12
|
-
|
|
13
|
-
CREATE TABLE IF NOT EXISTS findings (
|
|
14
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
15
|
-
scan_id INTEGER NOT NULL,
|
|
16
|
-
atk_id TEXT NOT NULL,
|
|
17
|
-
severity TEXT CHECK (severity IN ('info', 'low', 'medium', 'high', 'critical')),
|
|
18
|
-
description TEXT,
|
|
19
|
-
evidence TEXT,
|
|
20
|
-
mitigation TEXT,
|
|
21
|
-
FOREIGN KEY (scan_id) REFERENCES scans(id) ON DELETE CASCADE
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
-- View for reports
|
|
25
|
-
CREATE VIEW IF NOT EXISTS scan_findings AS
|
|
26
|
-
SELECT s.*, f.* FROM scans s
|
|
27
|
-
JOIN findings f ON s.id = f.scan_id;
|
|
28
|
-
|
|
29
|
-
-- Indexes
|
|
30
|
-
CREATE INDEX IF NOT EXISTS idx_scans_package ON scans(package_name);
|
|
31
|
-
CREATE INDEX IF NOT EXISTS idx_findings_atk ON findings(atk_id);
|
|
32
|
-
CREATE INDEX IF NOT EXISTS idx_findings_severity ON findings(severity);
|
|
1
|
+
-- SQLite schema for local CLI mode (free tier)
|
|
2
|
+
-- Tables: scans, findings (ATK-linked)
|
|
3
|
+
|
|
4
|
+
CREATE TABLE IF NOT EXISTS scans (
|
|
5
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6
|
+
package_name TEXT NOT NULL,
|
|
7
|
+
version TEXT,
|
|
8
|
+
scanned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
9
|
+
status TEXT DEFAULT 'completed',
|
|
10
|
+
sbom_json TEXT
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
CREATE TABLE IF NOT EXISTS findings (
|
|
14
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
15
|
+
scan_id INTEGER NOT NULL,
|
|
16
|
+
atk_id TEXT NOT NULL,
|
|
17
|
+
severity TEXT CHECK (severity IN ('info', 'low', 'medium', 'high', 'critical')),
|
|
18
|
+
description TEXT,
|
|
19
|
+
evidence TEXT,
|
|
20
|
+
mitigation TEXT,
|
|
21
|
+
FOREIGN KEY (scan_id) REFERENCES scans(id) ON DELETE CASCADE
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
-- View for reports
|
|
25
|
+
CREATE VIEW IF NOT EXISTS scan_findings AS
|
|
26
|
+
SELECT s.*, f.* FROM scans s
|
|
27
|
+
JOIN findings f ON s.id = f.scan_id;
|
|
28
|
+
|
|
29
|
+
-- Indexes
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_scans_package ON scans(package_name);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_findings_atk ON findings(atk_id);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_findings_severity ON findings(severity);
|
package/backend/db.js
CHANGED
|
@@ -1,89 +1,89 @@
|
|
|
1
|
-
import initSqlJs from 'sql.js';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const DB_PATH = path.join(process.cwd(), 'npm-scan.db');
|
|
8
|
-
const SCHEMA_PATH = path.join(__dirname, 'db', 'schema.sql');
|
|
9
|
-
|
|
10
|
-
let db = null;
|
|
11
|
-
let initPromise = null;
|
|
12
|
-
|
|
13
|
-
async function ensureInit() {
|
|
14
|
-
if (db) return;
|
|
15
|
-
if (initPromise) return initPromise;
|
|
16
|
-
initPromise = (async () => {
|
|
17
|
-
const SQL = await initSqlJs();
|
|
18
|
-
if (fs.existsSync(DB_PATH)) {
|
|
19
|
-
db = new SQL.Database(fs.readFileSync(DB_PATH));
|
|
20
|
-
} else {
|
|
21
|
-
db = new SQL.Database();
|
|
22
|
-
}
|
|
23
|
-
if (fs.existsSync(SCHEMA_PATH)) {
|
|
24
|
-
db.run(fs.readFileSync(SCHEMA_PATH, 'utf8'));
|
|
25
|
-
}
|
|
26
|
-
})();
|
|
27
|
-
return initPromise;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function queryAll(sql, params = []) {
|
|
31
|
-
const stmt = db.prepare(sql);
|
|
32
|
-
if (params.length) stmt.bind(params);
|
|
33
|
-
const rows = [];
|
|
34
|
-
while (stmt.step()) {
|
|
35
|
-
rows.push(stmt.getAsObject());
|
|
36
|
-
}
|
|
37
|
-
stmt.free();
|
|
38
|
-
return rows;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function queryOne(sql, params = []) {
|
|
42
|
-
return queryAll(sql, params)[0] || null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function lastId() {
|
|
46
|
-
const r = db.exec("SELECT last_insert_rowid()");
|
|
47
|
-
return Number(r[0].values[0][0]);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function persist() {
|
|
51
|
-
fs.writeFileSync(DB_PATH, Buffer.from(db.export()));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function saveScan(pkgName, version = 'latest', findings = []) {
|
|
55
|
-
await ensureInit();
|
|
56
|
-
db.run("INSERT INTO scans (package_name, version) VALUES (?, ?)", [pkgName, version]);
|
|
57
|
-
const scanId = lastId();
|
|
58
|
-
const stmt = db.prepare("INSERT INTO findings (scan_id, atk_id, severity, description, evidence) VALUES (?, ?, ?, ?, ?)");
|
|
59
|
-
for (const f of findings) {
|
|
60
|
-
stmt.run([scanId, f.id, f.severity, f.title || f.description, f.evidence || '']);
|
|
61
|
-
}
|
|
62
|
-
stmt.free();
|
|
63
|
-
persist();
|
|
64
|
-
return scanId;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function getRecentScans(limit = 10) {
|
|
68
|
-
await ensureInit();
|
|
69
|
-
return queryAll("SELECT * FROM scans ORDER BY scanned_at DESC LIMIT ?", [limit]);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export async function getFindings(scanId) {
|
|
73
|
-
await ensureInit();
|
|
74
|
-
return queryAll("SELECT * FROM findings WHERE scan_id = ?", [scanId]);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export async function getScan(scanId) {
|
|
78
|
-
await ensureInit();
|
|
79
|
-
return queryOne("SELECT * FROM scans WHERE id = ?", [scanId]);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export async function close() {
|
|
83
|
-
if (db) {
|
|
84
|
-
persist();
|
|
85
|
-
db.close();
|
|
86
|
-
db = null;
|
|
87
|
-
initPromise = null;
|
|
88
|
-
}
|
|
1
|
+
import initSqlJs from 'sql.js';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const DB_PATH = path.join(process.cwd(), 'npm-scan.db');
|
|
8
|
+
const SCHEMA_PATH = path.join(__dirname, 'db', 'schema.sql');
|
|
9
|
+
|
|
10
|
+
let db = null;
|
|
11
|
+
let initPromise = null;
|
|
12
|
+
|
|
13
|
+
async function ensureInit() {
|
|
14
|
+
if (db) return;
|
|
15
|
+
if (initPromise) return initPromise;
|
|
16
|
+
initPromise = (async () => {
|
|
17
|
+
const SQL = await initSqlJs();
|
|
18
|
+
if (fs.existsSync(DB_PATH)) {
|
|
19
|
+
db = new SQL.Database(fs.readFileSync(DB_PATH));
|
|
20
|
+
} else {
|
|
21
|
+
db = new SQL.Database();
|
|
22
|
+
}
|
|
23
|
+
if (fs.existsSync(SCHEMA_PATH)) {
|
|
24
|
+
db.run(fs.readFileSync(SCHEMA_PATH, 'utf8'));
|
|
25
|
+
}
|
|
26
|
+
})();
|
|
27
|
+
return initPromise;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function queryAll(sql, params = []) {
|
|
31
|
+
const stmt = db.prepare(sql);
|
|
32
|
+
if (params.length) stmt.bind(params);
|
|
33
|
+
const rows = [];
|
|
34
|
+
while (stmt.step()) {
|
|
35
|
+
rows.push(stmt.getAsObject());
|
|
36
|
+
}
|
|
37
|
+
stmt.free();
|
|
38
|
+
return rows;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function queryOne(sql, params = []) {
|
|
42
|
+
return queryAll(sql, params)[0] || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function lastId() {
|
|
46
|
+
const r = db.exec("SELECT last_insert_rowid()");
|
|
47
|
+
return Number(r[0].values[0][0]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function persist() {
|
|
51
|
+
fs.writeFileSync(DB_PATH, Buffer.from(db.export()));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function saveScan(pkgName, version = 'latest', findings = []) {
|
|
55
|
+
await ensureInit();
|
|
56
|
+
db.run("INSERT INTO scans (package_name, version) VALUES (?, ?)", [pkgName, version]);
|
|
57
|
+
const scanId = lastId();
|
|
58
|
+
const stmt = db.prepare("INSERT INTO findings (scan_id, atk_id, severity, description, evidence) VALUES (?, ?, ?, ?, ?)");
|
|
59
|
+
for (const f of findings) {
|
|
60
|
+
stmt.run([scanId, f.id, f.severity, f.title || f.description, f.evidence || '']);
|
|
61
|
+
}
|
|
62
|
+
stmt.free();
|
|
63
|
+
persist();
|
|
64
|
+
return scanId;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function getRecentScans(limit = 10) {
|
|
68
|
+
await ensureInit();
|
|
69
|
+
return queryAll("SELECT * FROM scans ORDER BY scanned_at DESC LIMIT ?", [limit]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function getFindings(scanId) {
|
|
73
|
+
await ensureInit();
|
|
74
|
+
return queryAll("SELECT * FROM findings WHERE scan_id = ?", [scanId]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function getScan(scanId) {
|
|
78
|
+
await ensureInit();
|
|
79
|
+
return queryOne("SELECT * FROM scans WHERE id = ?", [scanId]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function close() {
|
|
83
|
+
if (db) {
|
|
84
|
+
persist();
|
|
85
|
+
db.close();
|
|
86
|
+
db = null;
|
|
87
|
+
initPromise = null;
|
|
88
|
+
}
|
|
89
89
|
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
export async function scan(pkgJson, files = []) {
|
|
2
|
-
const findings = [];
|
|
3
|
-
const scripts = pkgJson.scripts || {};
|
|
4
|
-
const suspicious = Object.keys(scripts).filter(s => /pre|post|install/i.test(s));
|
|
5
|
-
if (suspicious.length) {
|
|
6
|
-
const content = suspicious.map(s => scripts[s]).join(' ');
|
|
7
|
-
if (/curl|wget|sh |bash |\.sh|exfil|steal|pwn|c2|pastebin/i.test(content)) {
|
|
8
|
-
findings.push({
|
|
9
|
-
id: 'ATK-001',
|
|
10
|
-
severity: 'high',
|
|
11
|
-
title: 'Malicious lifecycle scripts',
|
|
12
|
-
description: 'Suspicious install hooks',
|
|
13
|
-
evidence: suspicious.join(', ')
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return findings;
|
|
1
|
+
export async function scan(pkgJson, files = []) {
|
|
2
|
+
const findings = [];
|
|
3
|
+
const scripts = pkgJson.scripts || {};
|
|
4
|
+
const suspicious = Object.keys(scripts).filter(s => /pre|post|install/i.test(s));
|
|
5
|
+
if (suspicious.length) {
|
|
6
|
+
const content = suspicious.map(s => scripts[s]).join(' ');
|
|
7
|
+
if (/curl|wget|sh |bash |\.sh|exfil|steal|pwn|c2|pastebin/i.test(content)) {
|
|
8
|
+
findings.push({
|
|
9
|
+
id: 'ATK-001',
|
|
10
|
+
severity: 'high',
|
|
11
|
+
title: 'Malicious lifecycle scripts',
|
|
12
|
+
description: 'Suspicious install hooks',
|
|
13
|
+
evidence: suspicious.join(', ')
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return findings;
|
|
18
18
|
}
|