@lateos/npm-scan 1.0.0 → 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.
Files changed (125) hide show
  1. package/README.md +864 -861
  2. package/backend/cra.js +113 -21
  3. package/backend/db.js +18 -10
  4. package/backend/detectors/atk-001-lifecycle.js +5 -5
  5. package/backend/detectors/atk-002-obfusc.js +126 -47
  6. package/backend/detectors/atk-003-creds.js +8 -4
  7. package/backend/detectors/atk-004-persist.js +3 -3
  8. package/backend/detectors/atk-005-exfil.js +8 -4
  9. package/backend/detectors/atk-006-depconf.js +3 -3
  10. package/backend/detectors/atk-007-typosquat.js +64 -10
  11. package/backend/detectors/atk-008-tarball-tamper.js +6 -6
  12. package/backend/detectors/atk-009-dormant-trigger.js +9 -5
  13. package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
  14. package/backend/detectors/atk-011-transitive-prop.js +14 -13
  15. package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
  16. package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
  17. package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
  18. package/backend/detectors/axios-poisoning/index.js +77 -60
  19. package/backend/detectors/config/thresholds.js +48 -3
  20. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
  21. package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
  22. package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
  23. package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
  24. package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
  25. package/backend/detectors/hf-impersonation/index.js +94 -31
  26. package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
  27. package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
  28. package/backend/detectors/hf-impersonation/simhash.js +2 -2
  29. package/backend/detectors/index.js +181 -34
  30. package/backend/detectors/lib/ast-patterns.js +4 -1
  31. package/backend/detectors/lib/entropy-analyzer.js +12 -4
  32. package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
  33. package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
  34. package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
  35. package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
  36. package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
  37. package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
  38. package/backend/detectors/megalodon/index.js +35 -25
  39. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
  40. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
  41. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
  42. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
  43. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
  44. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
  45. package/backend/detectors/mini-shai-hulud/index.js +63 -26
  46. package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
  47. package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
  48. package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
  49. package/backend/detectors/msh-supplement/index.js +78 -63
  50. package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
  51. package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
  52. package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
  53. package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
  54. package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
  55. package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
  56. package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
  57. package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
  58. package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
  59. package/backend/detectors/node-ipc-compromise/index.js +21 -15
  60. package/backend/detectors/tier1-binary-embed.js +109 -41
  61. package/backend/detectors/tier1-cloud-imds.js +57 -37
  62. package/backend/detectors/tier1-encrypted-c2.js +198 -0
  63. package/backend/detectors/tier1-infostealer.js +121 -68
  64. package/backend/detectors/tier1-lifecycle-hook.js +63 -23
  65. package/backend/detectors/tier1-maintainer-compromise.js +157 -0
  66. package/backend/detectors/tier1-metadata-spoof.js +92 -42
  67. package/backend/detectors/tier1-multistage-postinstall.js +46 -19
  68. package/backend/detectors/tier1-obfuscation-heuristics.js +45 -17
  69. package/backend/detectors/tier1-self-propagation.js +115 -0
  70. package/backend/detectors/tier1-slsa-attestation.js +1 -1
  71. package/backend/detectors/tier1-transitive-deps.js +182 -0
  72. package/backend/detectors/tier1-typosquat.js +129 -50
  73. package/backend/detectors/tier1-version-anomaly.js +77 -41
  74. package/backend/detectors/tier1-version-confusion.js +79 -59
  75. package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
  76. package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
  77. package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
  78. package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
  79. package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
  80. package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
  81. package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
  82. package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
  83. package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
  84. package/backend/detectors/trapdoor/index.js +19 -14
  85. package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
  86. package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
  87. package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
  88. package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
  89. package/backend/detectors.test.js +78 -19
  90. package/backend/fetch.js +37 -29
  91. package/backend/index.js +1 -1
  92. package/backend/license.js +20 -4
  93. package/backend/lockfile.js +60 -36
  94. package/backend/pdf.js +107 -28
  95. package/backend/policy.js +183 -56
  96. package/backend/provenance.js +28 -3
  97. package/backend/report.js +136 -70
  98. package/backend/sbom.js +33 -27
  99. package/backend/scripts/analyze-false-positives.js +14 -8
  100. package/backend/scripts/analyze-validation.js +27 -21
  101. package/backend/scripts/detect-false-positives.js +20 -10
  102. package/backend/scripts/fetch-top-packages.js +197 -49
  103. package/backend/scripts/validate-d10-d13.js +103 -0
  104. package/backend/scripts/validate-detectors.js +26 -17
  105. package/backend/siem/cef.js +23 -21
  106. package/backend/siem/ecs.js +3 -3
  107. package/backend/siem/index.js +1 -1
  108. package/backend/siem/qradar.js +3 -3
  109. package/backend/siem/sentinel.js +2 -2
  110. package/backend/tests-d5-enhanced.test.js +13 -12
  111. package/backend/tests-d6-version-anomaly.test.js +17 -8
  112. package/backend/tests-d6.test.js +24 -14
  113. package/backend/tests-d6c.test.js +27 -14
  114. package/backend/tests-d7-obfuscation.test.js +9 -12
  115. package/backend/tests.test.js +182 -83
  116. package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
  117. package/backend/vsix-scan/detectors/burst-publish.js +14 -7
  118. package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
  119. package/backend/vsix-scan/detectors/known-ioc.js +23 -8
  120. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
  121. package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
  122. package/backend/vsix-scan/index.js +97 -41
  123. package/backend/vsix-scan/marketplace-client.js +29 -13
  124. package/cli/cli.js +154 -64
  125. package/package.json +12 -3
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 (s.findings || [])) {
4
+ for (const f of s.findings || []) {
5
5
  const key = f.atk_id || f.id;
6
- if (!atkMap[key]) 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
- { 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' },
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
+ }
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) return;
15
- if (initPromise) return initPromise;
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) stmt.bind(params);
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("SELECT last_insert_rowid()");
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("INSERT INTO scans (package_name, version) VALUES (?, ?)", [pkgName, version]);
62
+ db.run('INSERT INTO scans (package_name, version) VALUES (?, ?)', [pkgName, version]);
57
63
  const scanId = lastId();
58
- const stmt = db.prepare("INSERT INTO findings (scan_id, atk_id, severity, description, evidence) VALUES (?, ?, ?, ?, ?)");
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("SELECT * FROM scans ORDER BY scanned_at DESC LIMIT ?", [limit]);
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("SELECT * FROM findings WHERE scan_id = ?", [scanId]);
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("SELECT * FROM scans WHERE id = ?", [scanId]);
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, files = []) {
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
+ }
@@ -1,12 +1,43 @@
1
- const DIST_BUILD_PATTERNS = [/\/dist\//, /\/build\//, /\/bundle/, /\/min\//, /\.min\.js$/, /\.bundled?\.js$/];
2
- const TEST_FIXTURE_PATTERNS = [/\/test\//, /\/tests\//, /\/__tests__\//, /\/spec\//, /\.test\.js$/, /\.spec\.js$/, /fixtures?/];
1
+ const DIST_BUILD_PATTERNS = [
2
+ /\/dist\//,
3
+ /\/build\//,
4
+ /\/bundle/,
5
+ /\/min\//,
6
+ /\.min\.js$/,
7
+ /\.bundled?\.js$/,
8
+ ];
9
+ const TEST_FIXTURE_PATTERNS = [
10
+ /\/test\//,
11
+ /\/tests\//,
12
+ /\/__tests__\//,
13
+ /\/spec\//,
14
+ /\.test\.js$/,
15
+ /\.spec\.js$/,
16
+ /fixtures?/,
17
+ ];
3
18
  const KNOWN_SAFE_DOMAINS = [
4
- 'registry.npmjs.org', 'cdn.jsdelivr.net', 'unpkg.com', 'cdn.skypack.dev',
5
- 'esm.sh', 'deno.land', 'raw.githubusercontent.com', 'github.com',
6
- 'npmjs.com', 'nodejs.org', 'v8.dev', 'typescriptlang.org'
19
+ 'registry.npmjs.org',
20
+ 'cdn.jsdelivr.net',
21
+ 'unpkg.com',
22
+ 'cdn.skypack.dev',
23
+ 'esm.sh',
24
+ 'deno.land',
25
+ 'raw.githubusercontent.com',
26
+ 'github.com',
27
+ 'npmjs.com',
28
+ 'nodejs.org',
29
+ 'v8.dev',
30
+ 'typescriptlang.org',
7
31
  ];
8
32
 
9
- const LIFECYCLE_SCRIPT_NAMES = ['install', 'postinstall', 'preinstall', 'prepare', 'prepack', 'postpack'];
33
+ const LIFECYCLE_SCRIPT_NAMES = [
34
+ 'install',
35
+ 'postinstall',
36
+ 'preinstall',
37
+ 'prepare',
38
+ 'prepack',
39
+ 'postpack',
40
+ ];
10
41
 
11
42
  function extractUrlDomain(code) {
12
43
  const urlMatch = code.match(/https?:\/\/([^/'"\s]+)/);
@@ -14,22 +45,26 @@ function extractUrlDomain(code) {
14
45
  }
15
46
 
16
47
  function isDistOrBuild(filePath) {
17
- return DIST_BUILD_PATTERNS.some(p => p.test(filePath));
48
+ return DIST_BUILD_PATTERNS.some((p) => p.test(filePath));
18
49
  }
19
50
 
20
51
  function isTestOrFixture(filePath) {
21
- return TEST_FIXTURE_PATTERNS.some(p => p.test(filePath));
52
+ return TEST_FIXTURE_PATTERNS.some((p) => p.test(filePath));
22
53
  }
23
54
 
24
55
  function isKnownSafeDomain(domain) {
25
- if (!domain) return false;
26
- return KNOWN_SAFE_DOMAINS.some(safe => domain === safe || domain.endsWith('.' + safe));
56
+ if (!domain) {
57
+ return false;
58
+ }
59
+ return KNOWN_SAFE_DOMAINS.some((safe) => domain === safe || domain.endsWith('.' + safe));
27
60
  }
28
61
 
29
62
  function locateLine(code, pattern) {
30
63
  const lines = code.split('\n');
31
64
  for (let i = 0; i < lines.length; i++) {
32
- if (pattern.test(lines[i])) return i + 1;
65
+ if (pattern.test(lines[i])) {
66
+ return i + 1;
67
+ }
33
68
  }
34
69
  return null;
35
70
  }
@@ -40,65 +75,96 @@ function decodePreview(code) {
40
75
  try {
41
76
  const decoded = atob(b64Match[1]);
42
77
  return decoded.length > 80 ? decoded.slice(0, 80) + '...' : decoded;
43
- } catch {}
78
+ } catch {
79
+ /* ignore decode errors */
80
+ }
44
81
  }
45
-
82
+
46
83
  const hexMatch = code.match(/Buffer\.from\(['"]([0-9a-fA-F]+)['"],\s*['"]hex['"]\)/);
47
84
  if (hexMatch) {
48
85
  try {
49
86
  const decoded = Buffer.from(hexMatch[1], 'hex').toString();
50
87
  return decoded.length > 80 ? decoded.slice(0, 80) + '...' : decoded;
51
- } catch {}
88
+ } catch {
89
+ /* ignore decode errors */
90
+ }
52
91
  }
53
-
92
+
54
93
  const btoaMatch = code.match(/btoa\(['"]([A-Za-z0-9+/=]{10,})['"]\)/);
55
94
  if (btoaMatch) {
56
95
  try {
57
96
  const decoded = atob(btoaMatch[1]);
58
97
  return decoded.length > 80 ? decoded.slice(0, 80) + '...' : decoded;
59
- } catch {}
98
+ } catch {
99
+ /* ignore decode errors */
100
+ }
60
101
  }
61
-
102
+
62
103
  return null;
63
104
  }
64
105
 
65
106
  function detectEncodingType(code) {
66
- if (/Buffer\.from\(['"][0-9a-fA-F]+['"],\s*['"]hex['"]\)/.test(code)) return 'hex';
67
- if (/atob\(/.test(code)) return 'base64';
68
- if (/btoa\(/.test(code)) return 'base64';
69
- if (/Buffer\.from\([A-Za-z0-9+/=]{10,}/.test(code)) return 'base64';
70
- if (/String\.fromCharCode\(/.test(code)) return 'charcode';
71
- if (/btoa\(.*btoa\(|atob\(.*atob\(/.test(code)) return 'double-base64';
107
+ if (/Buffer\.from\(['"][0-9a-fA-F]+['"],\s*['"]hex['"]\)/.test(code)) {
108
+ return 'hex';
109
+ }
110
+ if (/atob\(/.test(code)) {
111
+ return 'base64';
112
+ }
113
+ if (/btoa\(/.test(code)) {
114
+ return 'base64';
115
+ }
116
+ if (/Buffer\.from\([A-Za-z0-9+/=]{10,}/.test(code)) {
117
+ return 'base64';
118
+ }
119
+ if (/String\.fromCharCode\(/.test(code)) {
120
+ return 'charcode';
121
+ }
122
+ if (/btoa\(.*btoa\(|atob\(.*atob\(/.test(code)) {
123
+ return 'double-base64';
124
+ }
72
125
  return 'unknown';
73
126
  }
74
127
 
75
128
  function isFileInLifecycleScript(filePath, pkgJson) {
76
- if (!pkgJson?.scripts) return false;
77
-
129
+ if (!pkgJson?.scripts) {
130
+ return false;
131
+ }
132
+
78
133
  const scripts = pkgJson.scripts;
79
134
  const fileName = filePath.split('/').pop();
80
- const normalizedPath = filePath.replace(/^node_modules\//, '').replace(/^dist\//, '').replace(/^build\//, '');
81
-
135
+ const normalizedPath = filePath
136
+ .replace(/^node_modules\//, '')
137
+ .replace(/^dist\//, '')
138
+ .replace(/^build\//, '');
139
+
82
140
  for (const scriptName of LIFECYCLE_SCRIPT_NAMES) {
83
141
  const scriptValue = scripts[scriptName];
84
- if (!scriptValue) continue;
85
-
86
- if (scriptValue.includes(filePath)) return true;
87
- if (scriptValue.includes(fileName)) return true;
88
- if (scriptValue.includes(normalizedPath)) return true;
89
-
142
+ if (!scriptValue) {
143
+ continue;
144
+ }
145
+
146
+ if (scriptValue.includes(filePath)) {
147
+ return true;
148
+ }
149
+ if (scriptValue.includes(fileName)) {
150
+ return true;
151
+ }
152
+ if (scriptValue.includes(normalizedPath)) {
153
+ return true;
154
+ }
155
+
90
156
  const scriptFileMatch = scriptValue.match(/[^\s'"]+\.js$/);
91
- if (scriptFileMatch && filePath.endsWith(scriptFileMatch[0])) return true;
157
+ if (scriptFileMatch && filePath.endsWith(scriptFileMatch[0])) {
158
+ return true;
159
+ }
92
160
  }
93
-
161
+
94
162
  return false;
95
163
  }
96
164
 
97
165
  function isLikelyLifecycleFileName(filePath) {
98
166
  const name = filePath.split('/').pop().replace(/\.js$/, '');
99
- return LIFECYCLE_SCRIPT_NAMES.includes(name) ||
100
- name === 'setup' ||
101
- name === 'install-helper';
167
+ return LIFECYCLE_SCRIPT_NAMES.includes(name) || name === 'setup' || name === 'install-helper';
102
168
  }
103
169
 
104
170
  function createEvidence(code, filePath, pattern, pkgJson) {
@@ -106,8 +172,9 @@ function createEvidence(code, filePath, pattern, pkgJson) {
106
172
  const line = locateLine(code, pattern);
107
173
  const decodedPreview = decodePreview(code);
108
174
  const destinationHost = extractUrlDomain(code);
109
- const lifecycleHook = isFileInLifecycleScript(filePath, pkgJson) || isLikelyLifecycleFileName(filePath);
110
-
175
+ const lifecycleHook =
176
+ isFileInLifecycleScript(filePath, pkgJson) || isLikelyLifecycleFileName(filePath);
177
+
111
178
  return {
112
179
  file: filePath,
113
180
  line: line,
@@ -121,7 +188,7 @@ function createEvidence(code, filePath, pattern, pkgJson) {
121
188
  export async function scan(pkgJson, files = []) {
122
189
  const findings = [];
123
190
  const pkgName = pkgJson?.name || '';
124
- const selfName = pkgName.replace(/^@/, '').replace(/\//, '-');
191
+ const _selfName = pkgName.replace(/^@/, '').replace(/\//, '-');
125
192
 
126
193
  for (const f of files) {
127
194
  const code = f.content;
@@ -137,15 +204,23 @@ export async function scan(pkgJson, files = []) {
137
204
  if (hasEval) {
138
205
  const hexDecode = /Buffer\.from\(['"`][0-9a-f]+['"`],\s*['"]hex['"]/.test(code);
139
206
  const b64Decode = /atob\(|Buffer\.from\([A-Za-z0-9+/=]{10,}/.test(code);
140
- const b64UrlDecode = /try\s*\{[^}]*atob\s*\(/s.test(code) || /btoa\(.*\)\s*[^;]*\.replace\(/s.test(code);
207
+ const b64UrlDecode =
208
+ /try\s*\{[^}]*atob\s*\(/s.test(code) || /btoa\(.*\)\s*[^;]*\.replace\(/s.test(code);
141
209
 
142
210
  if (hexDecode || b64Decode || b64UrlDecode) {
143
- const evidence = createEvidence(code, filePath, /eval\(|new Function\(|\bFunction\('/, pkgJson);
211
+ const evidence = createEvidence(
212
+ code,
213
+ filePath,
214
+ /eval\(|new Function\(|\bFunction\('/,
215
+ pkgJson
216
+ );
144
217
  findings.push({
145
218
  id: 'ATK-002',
146
219
  severity: 'medium',
147
220
  title: 'Obfuscated payload',
148
- description: hexDecode ? 'Eval with hex-decoded payload' : 'Eval with base64-decoded payload',
221
+ description: hexDecode
222
+ ? 'Eval with hex-decoded payload'
223
+ : 'Eval with base64-decoded payload',
149
224
  evidence: evidence,
150
225
  context: {
151
226
  file_path: filePath,
@@ -184,8 +259,12 @@ export async function scan(pkgJson, files = []) {
184
259
  }
185
260
  }
186
261
 
187
- if (/atob\(|Buffer\.from/.test(code) && /url|fetch|curl|http\.request|https\.request/.test(code)) {
188
- const isNetworkObfusc = /atob\(.*(https?:\/\/|\\x|http).*\)/s.test(code) ||
262
+ if (
263
+ /atob\(|Buffer\.from/.test(code) &&
264
+ /url|fetch|curl|http\.request|https\.request/.test(code)
265
+ ) {
266
+ const isNetworkObfusc =
267
+ /atob\(.*(https?:\/\/|\\x|http).*\)/s.test(code) ||
189
268
  /Buffer\.from\(['"`][0-9a-f]+['"`],\s*['"]hex['"].*fetch\(|fetch\(.*atob\(/s.test(code);
190
269
  if (isNetworkObfusc) {
191
270
  const evidence = createEvidence(code, filePath, /atob\(|Buffer\.from/, pkgJson);
@@ -259,4 +338,4 @@ export async function scan(pkgJson, files = []) {
259
338
  }
260
339
 
261
340
  return findings;
262
- }
341
+ }
@@ -1,14 +1,18 @@
1
1
  export async function scan(pkgJson, files = []) {
2
2
  const findings = [];
3
- const code = files.map(f => f.content).join('\n');
4
- if (/process\.env\.(NPM_TOKEN|GIT_TOKEN|AWS_SECRET|AWS_ACCESS|SSH_KEY)|\.npmrc|\.ssh\/id_rsa|readFile.*\.ssh/.test(code)) {
3
+ const code = files.map((f) => f.content).join('\n');
4
+ if (
5
+ /process\.env\.(NPM_TOKEN|GIT_TOKEN|AWS_SECRET|AWS_ACCESS|SSH_KEY)|\.npmrc|\.ssh\/id_rsa|readFile.*\.ssh/.test(
6
+ code
7
+ )
8
+ ) {
5
9
  findings.push({
6
10
  id: 'ATK-003',
7
11
  severity: 'high',
8
12
  title: 'Credential harvesting',
9
13
  description: 'Env vars or .npmrc/SSH key access',
10
- evidence: 'credential pattern match'
14
+ evidence: 'credential pattern match',
11
15
  });
12
16
  }
13
17
  return findings;
14
- }
18
+ }
@@ -1,14 +1,14 @@
1
1
  export async function scan(pkgJson, files = []) {
2
2
  const findings = [];
3
- const code = files.map(f => f.content).join('\n');
3
+ const code = files.map((f) => f.content).join('\n');
4
4
  if (/mkdir.*(\.vscode|\.claude|\.cursor)/.test(code)) {
5
5
  findings.push({
6
6
  id: 'ATK-004',
7
7
  severity: 'high',
8
8
  title: 'Persistence via editor configs',
9
9
  description: 'Creates .vscode/.claude/.cursor dirs',
10
- evidence: 'mkdir pattern match'
10
+ evidence: 'mkdir pattern match',
11
11
  });
12
12
  }
13
13
  return findings;
14
- }
14
+ }
@@ -1,14 +1,18 @@
1
1
  export async function scan(pkgJson, files = []) {
2
2
  const findings = [];
3
- const code = files.map(f => f.content).join('\n');
4
- if (/curl.*(-d|--data|--data-binary)|github\.com\/.*keys|pastebin|dns\.resolve.*\.com|exfil/.test(code.toLowerCase())) {
3
+ const code = files.map((f) => f.content).join('\n');
4
+ if (
5
+ /curl.*(-d|--data|--data-binary)|github\.com\/.*keys|pastebin|dns\.resolve.*\.com|exfil/.test(
6
+ code.toLowerCase()
7
+ )
8
+ ) {
5
9
  findings.push({
6
10
  id: 'ATK-005',
7
11
  severity: 'critical',
8
12
  title: 'Network exfiltration',
9
13
  description: 'Suspicious network calls: curl data exfil, pastebin, dns tunneling',
10
- evidence: 'network exfil pattern'
14
+ evidence: 'network exfil pattern',
11
15
  });
12
16
  }
13
17
  return findings;
14
- }
18
+ }
@@ -1,15 +1,15 @@
1
1
  export async function scan(pkgJson) {
2
2
  const findings = [];
3
3
  const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
4
- const squat = Object.keys(deps).filter(d => /squat|confus|typo/i.test(d.toLowerCase()));
4
+ const squat = Object.keys(deps).filter((d) => /squat|confus|typo/i.test(d.toLowerCase()));
5
5
  if (squat.length) {
6
6
  findings.push({
7
7
  id: 'ATK-006',
8
8
  severity: 'medium',
9
9
  title: 'Dependency confusion',
10
10
  description: 'Suspicious dependency names',
11
- evidence: squat.join(', ')
11
+ evidence: squat.join(', '),
12
12
  });
13
13
  }
14
14
  return findings;
15
- }
15
+ }