@lateos/npm-scan 0.17.1 → 0.18.1

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.
Files changed (89) hide show
  1. package/.dockerignore +20 -20
  2. package/.husky/pre-commit +1 -1
  3. package/CHANGELOG.md +199 -199
  4. package/LICENSING.md +19 -19
  5. package/README.de.md +708 -708
  6. package/README.fr.md +707 -707
  7. package/README.ja.md +704 -704
  8. package/README.md +826 -826
  9. package/README.zh.md +708 -708
  10. package/SECURITY.md +72 -72
  11. package/backend/cra.js +68 -68
  12. package/backend/db/schema.sql +32 -32
  13. package/backend/db.js +88 -88
  14. package/backend/detectors/atk-001-lifecycle.js +17 -17
  15. package/backend/detectors/atk-002-obfusc.js +261 -261
  16. package/backend/detectors/atk-003-creds.js +13 -13
  17. package/backend/detectors/atk-004-persist.js +13 -13
  18. package/backend/detectors/atk-005-exfil.js +13 -13
  19. package/backend/detectors/atk-006-depconf.js +14 -14
  20. package/backend/detectors/atk-007-typosquat.js +34 -34
  21. package/backend/detectors/atk-008-tarball-tamper.js +91 -91
  22. package/backend/detectors/atk-009-dormant-trigger.js +62 -62
  23. package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
  24. package/backend/detectors/atk-011-transitive-prop.js +76 -76
  25. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
  26. package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
  27. package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
  28. package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
  29. package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
  30. package/backend/detectors/hf-impersonation/index.js +396 -396
  31. package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
  32. package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
  33. package/backend/detectors/hf-impersonation/simhash.js +46 -46
  34. package/backend/detectors/index.js +75 -75
  35. package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
  36. package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
  37. package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
  38. package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
  39. package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
  40. package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
  41. package/backend/detectors/megalodon/index.js +80 -80
  42. package/backend/detectors/megalodon/types.js +9 -9
  43. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
  44. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
  45. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
  46. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
  47. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
  48. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
  49. package/backend/detectors/mini-shai-hulud/index.js +118 -118
  50. package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
  51. package/backend/fetch.js +175 -175
  52. package/backend/index.js +4 -4
  53. package/backend/license.js +89 -89
  54. package/backend/lockfile.js +379 -379
  55. package/backend/pdf.js +245 -245
  56. package/backend/policy.js +193 -193
  57. package/backend/report.js +254 -254
  58. package/backend/sbom.js +66 -66
  59. package/backend/siem/cef.js +32 -32
  60. package/backend/siem/ecs.js +40 -40
  61. package/backend/siem/index.js +18 -18
  62. package/backend/siem/qradar.js +56 -56
  63. package/backend/siem/sentinel.js +27 -27
  64. package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
  65. package/backend/vsix-scan/detectors/burst-publish.js +52 -52
  66. package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
  67. package/backend/vsix-scan/detectors/known-ioc.js +105 -105
  68. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
  69. package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
  70. package/backend/vsix-scan/index.js +183 -183
  71. package/backend/vsix-scan/marketplace-client.js +145 -145
  72. package/backend/vsix-scan/vsix-iocs.json +31 -31
  73. package/cli/cli.js +458 -458
  74. package/deploy/helm/npm-scan/Chart.yaml +21 -21
  75. package/deploy/helm/npm-scan/templates/_helpers.tpl +8 -8
  76. package/deploy/helm/npm-scan/templates/api.yaml +93 -93
  77. package/deploy/helm/npm-scan/templates/ingress.yaml +27 -27
  78. package/deploy/helm/npm-scan/templates/postgresql.yaml +66 -66
  79. package/deploy/helm/npm-scan/templates/secrets.yaml +18 -18
  80. package/deploy/helm/npm-scan/templates/worker.yaml +31 -31
  81. package/deploy/helm/npm-scan/values.byoc.yaml +74 -74
  82. package/deploy/helm/npm-scan/values.yaml +102 -102
  83. package/package.json +57 -57
  84. package/scripts/download-corpus.js +30 -30
  85. package/scripts/gen-mal-corpus.js +34 -34
  86. package/test/fixtures/lockfiles/npm-lock.json +68 -68
  87. package/test/fixtures/lockfiles/pnpm-lock.yaml +117 -117
  88. package/test/fixtures/lockfiles/yarn.lock +103 -103
  89. 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
  }
@@ -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
  }