@lateos/npm-scan 0.15.5 → 0.16.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 (36) hide show
  1. package/README.de.md +11 -2
  2. package/README.fr.md +11 -2
  3. package/README.ja.md +11 -2
  4. package/README.md +28 -2
  5. package/README.zh.md +11 -2
  6. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -0
  7. package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -0
  8. package/backend/detectors/cve-2026-48710-badhost/index.js +15 -0
  9. package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -0
  10. package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -0
  11. package/backend/detectors/index.js +6 -0
  12. package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +24 -0
  13. package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +19 -0
  14. package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +40 -0
  15. package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +31 -0
  16. package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +73 -0
  17. package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +37 -0
  18. package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +49 -0
  19. package/backend/detectors/node-ipc-compromise/d6-bootstrap-resolver.js +40 -0
  20. package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +42 -0
  21. package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +27 -0
  22. package/backend/detectors/node-ipc-compromise/d9-temp-artifact.js +20 -0
  23. package/backend/detectors/node-ipc-compromise/index.js +93 -0
  24. package/backend/detectors/node-ipc-compromise/iocs.json +59 -0
  25. package/backend/detectors/trapdoor/d1-campaign-marker.js +20 -0
  26. package/backend/detectors/trapdoor/d2-payload-fingerprint.js +22 -0
  27. package/backend/detectors/trapdoor/d3-publisher-blocklist.js +10 -0
  28. package/backend/detectors/trapdoor/d4-gists-exfil.js +34 -0
  29. package/backend/detectors/trapdoor/d5-ai-poisoning.js +35 -0
  30. package/backend/detectors/trapdoor/d6-lure-name.js +42 -0
  31. package/backend/detectors/trapdoor/d7-crypto-primitives.js +22 -0
  32. package/backend/detectors/trapdoor/d8-xor-key.js +15 -0
  33. package/backend/detectors/trapdoor/d9-cred-validation.js +32 -0
  34. package/backend/detectors/trapdoor/index.js +77 -0
  35. package/backend/detectors/trapdoor/iocs.json +51 -0
  36. package/package.json +1 -1
@@ -0,0 +1,73 @@
1
+ const IIFE_END_PATTERN = /}\)\(\);\s*$/;
2
+ const SIZE_DIFFERENTIAL_THRESHOLD = 50 * 1024;
3
+
4
+ export function scanCjsPayloadInjection(allFiles) {
5
+ const matches = [];
6
+
7
+ let cjsContent = null;
8
+ let mjsContent = null;
9
+ let cjsPath = null;
10
+ let mjsPath = null;
11
+
12
+ for (const file of allFiles) {
13
+ const path = file.path?.replace(/\\/g, '/') || '';
14
+ if (path.endsWith('node-ipc.cjs')) {
15
+ cjsContent = file.content || '';
16
+ cjsPath = path;
17
+ }
18
+ if (path.endsWith('node-ipc.mjs')) {
19
+ mjsContent = file.content || '';
20
+ mjsPath = path;
21
+ }
22
+ }
23
+
24
+ if (cjsContent && !mjsContent) {
25
+ matches.push({
26
+ file: cjsPath,
27
+ finding: 'cjs-present-no-esm',
28
+ detail: 'node-ipc.cjs present but node-ipc.mjs not found — unable to cross-reference size',
29
+ });
30
+ }
31
+
32
+ if (cjsContent && mjsContent) {
33
+ const cjsSize = Buffer.byteLength(cjsContent, 'utf8');
34
+ const mjsSize = Buffer.byteLength(mjsContent, 'utf8');
35
+ const sizeDiff = cjsSize - mjsSize;
36
+
37
+ if (sizeDiff > SIZE_DIFFERENTIAL_THRESHOLD) {
38
+ matches.push({
39
+ file: cjsPath,
40
+ finding: 'size-anomaly',
41
+ cjsSize,
42
+ mjsSize,
43
+ sizeDiff,
44
+ detail: `CJS (${cjsSize} bytes) exceeds ESM (${mjsSize} bytes) by ${sizeDiff} bytes — potential injected payload`,
45
+ });
46
+ }
47
+
48
+ if (IIFE_END_PATTERN.test(cjsContent.trim())) {
49
+ const trimmed = cjsContent.trim();
50
+ const iifeMatch = trimmed.match(IIFE_END_PATTERN);
51
+ if (iifeMatch) {
52
+ matches.push({
53
+ file: cjsPath,
54
+ finding: 'iife-suffix',
55
+ detail: 'node-ipc.cjs ends with IIFE pattern — potential obfuscated payload appended after module closure',
56
+ });
57
+ }
58
+ }
59
+ }
60
+
61
+ if (cjsContent && IIFE_END_PATTERN.test(cjsContent.trim())) {
62
+ const alreadyReported = matches.some(m => m.finding === 'iife-suffix');
63
+ if (!alreadyReported) {
64
+ matches.push({
65
+ file: cjsPath,
66
+ finding: 'iife-suffix',
67
+ detail: 'node-ipc.cjs ends with IIFE pattern — potential obfuscated payload appended after module closure',
68
+ });
69
+ }
70
+ }
71
+
72
+ return { triggered: matches.length > 0, matches };
73
+ }
@@ -0,0 +1,37 @@
1
+ import { createHash } from 'crypto';
2
+
3
+ const INJECTED_PAYLOAD_HASH = '3427a90c8cb9af764445448648176e120ebc6af0a538158340cf6220de4d01b7';
4
+
5
+ const IIFE_BOUNDARY = /}\)\(\);\s*$/;
6
+
7
+ export function scanInjectedPayloadHash(allFiles) {
8
+ const matches = [];
9
+
10
+ for (const file of allFiles) {
11
+ const path = file.path?.replace(/\\/g, '/') || '';
12
+ if (!path.endsWith('node-ipc.cjs')) continue;
13
+
14
+ const content = file.content || '';
15
+
16
+ if (content.includes(INJECTED_PAYLOAD_HASH)) {
17
+ matches.push({
18
+ file: path,
19
+ finding: 'hash-string-present',
20
+ sha256: INJECTED_PAYLOAD_HASH,
21
+ detail: 'Known injected payload SHA-256 found within node-ipc.cjs content',
22
+ });
23
+ }
24
+
25
+ const fileHash = createHash('sha256').update(content, 'utf8').digest('hex');
26
+ if (fileHash === INJECTED_PAYLOAD_HASH) {
27
+ matches.push({
28
+ file: path,
29
+ finding: 'file-hash-match',
30
+ sha256: fileHash,
31
+ detail: 'node-ipc.cjs SHA-256 matches known injected payload hash',
32
+ });
33
+ }
34
+ }
35
+
36
+ return { triggered: matches.length > 0, matches };
37
+ }
@@ -0,0 +1,49 @@
1
+ const PUBLIC_RESOLVERS = new Set([
2
+ '1.1.1.1',
3
+ '8.8.8.8',
4
+ '8.8.4.4',
5
+ '9.9.9.9',
6
+ ]);
7
+
8
+ const IP_PATTERN = /setServers\(\s*\[?\s*['"`](\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})['"`]/;
9
+
10
+ export function scanDnsC2Pattern(allFiles, pkgJson) {
11
+ const matches = [];
12
+
13
+ const sources = [];
14
+
15
+ const scripts = pkgJson?.scripts || {};
16
+ for (const [hook, content] of Object.entries(scripts)) {
17
+ if (/preinstall|install|postinstall|prepare/.test(hook)) {
18
+ sources.push({ file: `script:${hook}`, content });
19
+ }
20
+ }
21
+
22
+ for (const file of allFiles) {
23
+ const path = file.path || '';
24
+ if (!path.endsWith('.js') && !path.endsWith('.mjs') && !path.endsWith('.cjs')) continue;
25
+ sources.push({ file: path, content: file.content || '' });
26
+ }
27
+
28
+ for (const { file, content } of sources) {
29
+ const hasDnsResolver = /\bdns\.promises\s*\.\s*Resolver\b/.test(content);
30
+ if (!hasDnsResolver) continue;
31
+
32
+ const ipMatch = content.match(IP_PATTERN);
33
+ if (!ipMatch) continue;
34
+
35
+ const customIP = ipMatch[1];
36
+ if (PUBLIC_RESOLVERS.has(customIP)) continue;
37
+
38
+ const hasResolveTxt = /\bresolveTxt\b/.test(content);
39
+
40
+ matches.push({
41
+ file,
42
+ customResolverIP: customIP,
43
+ hasResolveTxt,
44
+ detail: `Custom DNS resolver at ${customIP}${hasResolveTxt ? ' with resolveTxt() — TXT tunneling fingerprint' : ''}`,
45
+ });
46
+ }
47
+
48
+ return { triggered: matches.length > 0, matches };
49
+ }
@@ -0,0 +1,40 @@
1
+ const BOOTSTRAP_DOMAIN = /sh\.azurestaticprovider\.net/i;
2
+ const C2_IP = /37\.16\.75\.69/;
3
+
4
+ export function scanBootstrapResolver(allFiles, pkgJson) {
5
+ const matches = [];
6
+
7
+ const sources = [];
8
+
9
+ const scripts = pkgJson?.scripts || {};
10
+ for (const [hook, content] of Object.entries(scripts)) {
11
+ sources.push({ file: `script:${hook}`, content });
12
+ }
13
+
14
+ for (const file of allFiles) {
15
+ const path = file.path || '';
16
+ sources.push({ file: path, content: file.content || '' });
17
+ }
18
+
19
+ for (const { file, content } of sources) {
20
+ if (BOOTSTRAP_DOMAIN.test(content)) {
21
+ matches.push({
22
+ file,
23
+ finding: 'c2-domain',
24
+ value: 'sh.azurestaticprovider.net',
25
+ detail: 'Bootstrap resolver domain — lookalike, not a Microsoft domain',
26
+ });
27
+ }
28
+
29
+ if (C2_IP.test(content)) {
30
+ matches.push({
31
+ file,
32
+ finding: 'c2-ip',
33
+ value: '37.16.75.69',
34
+ detail: 'Known C2 IP address for DNS TXT tunneling',
35
+ });
36
+ }
37
+ }
38
+
39
+ return { triggered: matches.length > 0, matches };
40
+ }
@@ -0,0 +1,42 @@
1
+ const EXFIL_ZONE = /bt\.node\.js/i;
2
+ const RESOLVE_TXT_DYNAMIC = /\bresolveTxt\s*\(/;
3
+
4
+ export function scanDnsTxtExfil(allFiles, pkgJson) {
5
+ const matches = [];
6
+
7
+ const sources = [];
8
+
9
+ const scripts = pkgJson?.scripts || {};
10
+ for (const [hook, content] of Object.entries(scripts)) {
11
+ if (/preinstall|install|postinstall|prepare/.test(hook)) {
12
+ sources.push({ file: `script:${hook}`, content });
13
+ }
14
+ }
15
+
16
+ for (const file of allFiles) {
17
+ const path = file.path || '';
18
+ if (!path.endsWith('.js') && !path.endsWith('.mjs') && !path.endsWith('.cjs')) continue;
19
+ sources.push({ file: path, content: file.content || '' });
20
+ }
21
+
22
+ for (const { file, content } of sources) {
23
+ if (EXFIL_ZONE.test(content)) {
24
+ matches.push({
25
+ file,
26
+ finding: 'exfil-zone',
27
+ value: 'bt.node.js',
28
+ detail: 'DNS TXT exfiltration zone reference found',
29
+ });
30
+ }
31
+
32
+ if (RESOLVE_TXT_DYNAMIC.test(content)) {
33
+ matches.push({
34
+ file,
35
+ finding: 'resolve-txt',
36
+ detail: 'resolveTxt() call detected — potential DNS TXT tunneling',
37
+ });
38
+ }
39
+ }
40
+
41
+ return { triggered: matches.length > 0, matches };
42
+ }
@@ -0,0 +1,27 @@
1
+ export function scanRuntimeTrigger(allFiles, pkgJson) {
2
+ const matches = [];
3
+
4
+ const sources = [];
5
+
6
+ const scripts = pkgJson?.scripts || {};
7
+ for (const [hook, content] of Object.entries(scripts)) {
8
+ sources.push({ file: `script:${hook}`, content });
9
+ }
10
+
11
+ for (const file of allFiles) {
12
+ const path = file.path || '';
13
+ if (!path.endsWith('.js') && !path.endsWith('.mjs') && !path.endsWith('.cjs')) continue;
14
+ sources.push({ file: path, content: file.content || '' });
15
+ }
16
+
17
+ for (const { file, content } of sources) {
18
+ if (/\bsetImmediate\s*\(/.test(content)) {
19
+ matches.push({
20
+ file,
21
+ detail: 'setImmediate() call found — node-ipc malware fires at require() time, not via postinstall',
22
+ });
23
+ }
24
+ }
25
+
26
+ return { triggered: matches.length > 0, matches };
27
+ }
@@ -0,0 +1,20 @@
1
+ const NT_DIR_PATTERN = /~\/(nt-(?:[\w-]+))\/.*\.tar\.gz/;
2
+
3
+ export function scanTempArtifact(allFiles) {
4
+ const matches = [];
5
+
6
+ for (const file of allFiles) {
7
+ const path = file.path || '';
8
+
9
+ const artifactMatch = path.match(NT_DIR_PATTERN);
10
+ if (artifactMatch) {
11
+ matches.push({
12
+ file: path,
13
+ dirName: artifactMatch[1],
14
+ detail: `Staging artifact found in ~/${artifactMatch[1]}/ — exfil may have been interrupted`,
15
+ });
16
+ }
17
+ }
18
+
19
+ return { triggered: matches.length > 0, matches };
20
+ }
@@ -0,0 +1,93 @@
1
+ import { scanVersionBlocklist } from './d1-version-blocklist.js';
2
+ import { scanTarballHash } from './d2-tarball-hash.js';
3
+ import { scanCjsPayloadInjection } from './d3-cjs-payload-injection.js';
4
+ import { scanInjectedPayloadHash } from './d4-injected-payload-hash.js';
5
+ import { scanDnsC2Pattern } from './d5-dns-c2-pattern.js';
6
+ import { scanBootstrapResolver } from './d6-bootstrap-resolver.js';
7
+ import { scanDnsTxtExfil } from './d7-dns-txt-exfil.js';
8
+ import { scanRuntimeTrigger } from './d8-runtime-trigger.js';
9
+ import { scanTempArtifact } from './d9-temp-artifact.js';
10
+ import { scanUnauthorizedPublisher } from './d10-unauthorized-publisher.js';
11
+ import { scanBlastRadius } from './d11-blast-radius.js';
12
+
13
+ const RULE_SEVERITY = {
14
+ D1: 'critical',
15
+ D2: 'critical',
16
+ D3: 'critical',
17
+ D4: 'critical',
18
+ D5: 'critical',
19
+ D6: 'critical',
20
+ D7: 'critical',
21
+ D8: 'info',
22
+ D9: 'critical',
23
+ D10: 'critical',
24
+ D11: 'critical',
25
+ };
26
+
27
+ const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low', 'info', 'none'];
28
+
29
+ function highestSeverity(severities) {
30
+ for (const s of SEVERITY_ORDER) {
31
+ if (severities.includes(s)) return s;
32
+ }
33
+ return 'none';
34
+ }
35
+
36
+ function buildRemediation(triggered) {
37
+ const lines = [];
38
+ if (triggered.includes('D1') || triggered.includes('D11')) {
39
+ lines.push('Pin node-ipc to safe version: 9.1.5 or 12.0.0');
40
+ }
41
+ if (triggered.includes('D9')) {
42
+ lines.push('PRESERVE ~/nt-*/ artifacts for incident response');
43
+ }
44
+ if (triggered.includes('D5') || triggered.includes('D6') || triggered.includes('D7')) {
45
+ lines.push('Review DNS egress logs for sh.azurestaticprovider.net and 37.16.75.69 post May 14, 2026');
46
+ }
47
+ lines.push('Rotate all CI/CD secrets and OIDC tokens');
48
+ lines.push('Audit maintainer email domain expiry for all critical dependencies');
49
+ return lines.join('. ');
50
+ }
51
+
52
+ export async function scan(pkgJson, files = [], registryMeta = null, allFiles = null) {
53
+ const fileList = allFiles || files || [];
54
+
55
+ const results = {
56
+ D1: scanVersionBlocklist(pkgJson, registryMeta),
57
+ D2: scanTarballHash(fileList),
58
+ D3: scanCjsPayloadInjection(fileList),
59
+ D4: scanInjectedPayloadHash(fileList),
60
+ D5: scanDnsC2Pattern(fileList, pkgJson),
61
+ D6: scanBootstrapResolver(fileList, pkgJson),
62
+ D7: scanDnsTxtExfil(fileList, pkgJson),
63
+ D8: scanRuntimeTrigger(fileList, pkgJson),
64
+ D9: scanTempArtifact(fileList),
65
+ D10: scanUnauthorizedPublisher(pkgJson, registryMeta),
66
+ D11: scanBlastRadius(fileList),
67
+ };
68
+
69
+ const triggered = Object.entries(results)
70
+ .filter(([_, r]) => r.triggered)
71
+ .map(([id]) => id);
72
+
73
+ if (triggered.length === 0) return [];
74
+
75
+ const severity = highestSeverity(triggered.map(id => RULE_SEVERITY[id]));
76
+
77
+ const evidence = {
78
+ campaign: 'NODE_IPC_COMPROMISE',
79
+ triggeredRules: triggered,
80
+ details: Object.fromEntries(
81
+ Object.entries(results).filter(([_, r]) => r.triggered)
82
+ ),
83
+ };
84
+
85
+ return [{
86
+ id: 'NODE_IPC_COMPROMISE',
87
+ severity,
88
+ title: 'node-ipc supply chain compromise (May 14, 2026)',
89
+ description: `${triggered.length} signal(s): ${triggered.join(', ')}`,
90
+ evidence: JSON.stringify(evidence),
91
+ mitigation: buildRemediation(triggered),
92
+ }];
93
+ }
@@ -0,0 +1,59 @@
1
+ {
2
+ "lastUpdated": "2026-05-28T00:00:00.000Z",
3
+ "campaign": {
4
+ "id": "node-ipc-compromise",
5
+ "description": "node-ipc supply chain compromise (May 14, 2026) — 3 malicious versions (9.1.6, 9.2.3, 12.0.1) published via expired maintainer email domain takeover. 80KB credential stealer delivered via DNS TXT tunneling.",
6
+ "firstObserved": "2026-05-14T00:00:00.000Z",
7
+ "attribution": {
8
+ "npmPublisher": "atiertant",
9
+ "c2Domain": "sh.azurestaticprovider.net",
10
+ "c2IP": "37.16.75.69",
11
+ "exfilZone": "bt.node.js"
12
+ }
13
+ },
14
+ "versions": {
15
+ "9.1.6": {
16
+ "tarballSha256": "449e4265979b5fdb2d3446c021af437e815debd66de7da2fe54f1ad93cbcc75e",
17
+ "safePin": "9.1.5",
18
+ "semverRanges": ["~9.1.x", "^9.1", "^9"]
19
+ },
20
+ "9.2.3": {
21
+ "tarballSha256": "c2f4dc64aec4631540a568e88932b61daebbfb7e8281b812fa01b7215f9be9ea",
22
+ "safePin": "9.1.5",
23
+ "semverRanges": ["~9.2.x", "^9.2"]
24
+ },
25
+ "12.0.1": {
26
+ "tarballSha256": "78a82d93b4f580835f5823b85a3d9ee1f03a15ee6f0e01b4eac86252a7002981",
27
+ "safePin": "12.0.0",
28
+ "semverRanges": ["~12.0.x", "^12"]
29
+ }
30
+ },
31
+ "iocs": [
32
+ {
33
+ "type": "publisherAccount",
34
+ "value": "atiertant",
35
+ "ecosystem": "npm",
36
+ "notes": "Unauthorized publisher — no prior history on node-ipc, exclusively used for malicious versions."
37
+ },
38
+ {
39
+ "type": "c2Domain",
40
+ "value": "sh.azurestaticprovider.net",
41
+ "notes": "Bootstrap resolver domain (lookalike, not Microsoft). DNS TXT tunneling C2."
42
+ },
43
+ {
44
+ "type": "c2IP",
45
+ "value": "37.16.75.69",
46
+ "notes": "C2 IP address for DNS TXT tunneling."
47
+ },
48
+ {
49
+ "type": "exfilZone",
50
+ "value": "bt.node.js",
51
+ "notes": "DNS TXT exfiltration zone. Queries encode stolen data into subdomain labels."
52
+ },
53
+ {
54
+ "type": "payloadHash",
55
+ "value": "3427a90c8cb9af764445448648176e120ebc6af0a538158340cf6220de4d01b7",
56
+ "notes": "SHA-256 of injected payload blob within node-ipc.cjs."
57
+ }
58
+ ]
59
+ }
@@ -0,0 +1,20 @@
1
+ const TARGET_EXTENSIONS = ['.md', '.sh', '.json'];
2
+ const TARGET_FILENAMES = new Set(['.cursorrules', 'CLAUDE.md', 'README.md', 'package.json']);
3
+
4
+ export function scanCampaignMarker(allFiles) {
5
+ const matches = [];
6
+ for (const file of allFiles) {
7
+ const path = file.path || '';
8
+ const content = file.content || '';
9
+ const basename = path.split(/[\\/]/).pop();
10
+ const ext = path.includes('.') ? '.' + path.split('.').pop() : '';
11
+
12
+ const isTarget = TARGET_FILENAMES.has(basename) || TARGET_EXTENSIONS.includes(ext);
13
+ if (!isTarget) continue;
14
+
15
+ if (content.includes('P-2024-001')) {
16
+ matches.push({ file: path });
17
+ }
18
+ }
19
+ return { triggered: matches.length > 0, matches };
20
+ }
@@ -0,0 +1,22 @@
1
+ export function scanPayloadFingerprint(allFiles) {
2
+ const matches = [];
3
+ for (const file of allFiles) {
4
+ const path = file.path || '';
5
+ const content = file.content || '';
6
+ const basename = path.split(/[\\/]/).pop();
7
+
8
+ const byteSize = Buffer.byteLength(content, 'utf8');
9
+
10
+ if (basename === 'trap-core.js') {
11
+ matches.push({ file: path, matchType: 'filename', byteSize });
12
+ }
13
+
14
+ if (byteSize === 48485) {
15
+ const alreadyMatched = matches.some(m => m.file === path);
16
+ if (!alreadyMatched) {
17
+ matches.push({ file: path, matchType: 'byteSize', byteSize });
18
+ }
19
+ }
20
+ }
21
+ return { triggered: matches.length > 0, matches };
22
+ }
@@ -0,0 +1,10 @@
1
+ export function scanPublisherBlocklist(pkgJson, registryMeta) {
2
+ const publisherAccount = registryMeta?.versions?.[pkgJson?.version]?._npmUser?.name
3
+ || registryMeta?.versions?.[Object.keys(registryMeta.versions || {})[0]]?._npmUser?.name
4
+ || null;
5
+
6
+ if (publisherAccount === 'asdxzxc') {
7
+ return { triggered: true, publisher: publisherAccount };
8
+ }
9
+ return { triggered: false, publisher: publisherAccount };
10
+ }
@@ -0,0 +1,34 @@
1
+ const CRED_PATH_PATTERNS = /\.aws\/|id_rsa|\.env|keystore|credentials|\.npmrc|\.ssh\//i;
2
+ const C2_PATTERNS = [/ddjidd564\.github\.io/i, /gist\.github\.com/i];
3
+
4
+ function scanContent(content, filePath) {
5
+ const matches = [];
6
+ const hasC2 = C2_PATTERNS.some(p => p.test(content));
7
+ if (!hasC2) return matches;
8
+
9
+ const hasCredPath = CRED_PATH_PATTERNS.test(content);
10
+ if (hasCredPath) {
11
+ matches.push({ file: filePath });
12
+ }
13
+ return matches;
14
+ }
15
+
16
+ export function scanGistsExfil(allFiles, pkgJson) {
17
+ const matches = [];
18
+
19
+ const scripts = pkgJson?.scripts || {};
20
+ for (const [hook, scriptContent] of Object.entries(scripts)) {
21
+ if (/preinstall|install|postinstall|prepare/.test(hook)) {
22
+ matches.push(...scanContent(scriptContent, `script:${hook}`));
23
+ }
24
+ }
25
+
26
+ for (const file of allFiles) {
27
+ const path = file.path || '';
28
+ if (path.endsWith('.js') || path.endsWith('.mjs') || path.endsWith('.cjs')) {
29
+ matches.push(...scanContent(file.content || '', path));
30
+ }
31
+ }
32
+
33
+ return { triggered: matches.length > 0, matches };
34
+ }
@@ -0,0 +1,35 @@
1
+ const ZERO_WIDTH_RANGES = [
2
+ [0x200B, 0x200D],
3
+ [0xFEFF, 0xFEFF],
4
+ ];
5
+
6
+ function isZeroWidthChar(code) {
7
+ return ZERO_WIDTH_RANGES.some(([lo, hi]) => code >= lo && code <= hi);
8
+ }
9
+
10
+ const TARGET_FILES = /(^|[\\/])(\.cursorrules|CLAUDE\.md)$/i;
11
+
12
+ export function scanAIPoisoning(allFiles) {
13
+ const matches = [];
14
+
15
+ for (const file of allFiles) {
16
+ const path = file.path?.replace(/\\/g, '/') || '';
17
+ if (!TARGET_FILES.test(path)) continue;
18
+
19
+ const content = file.content || '';
20
+ const found = [];
21
+
22
+ for (let i = 0; i < content.length; i++) {
23
+ const code = content.charCodeAt(i);
24
+ if (isZeroWidthChar(code)) {
25
+ found.push({ char: `U+${code.toString(16).toUpperCase()}`, position: i });
26
+ }
27
+ }
28
+
29
+ if (found.length > 0) {
30
+ matches.push({ file: path, zeroWidthChars: found, count: found.length });
31
+ }
32
+ }
33
+
34
+ return { triggered: matches.length > 0, matches };
35
+ }
@@ -0,0 +1,42 @@
1
+ const LURE_PATTERNS = [
2
+ /solidity/i,
3
+ /defi/i,
4
+ /solana/i,
5
+ /sui\b/i,
6
+ /move-lang/i,
7
+ /^eth-/i,
8
+ /prompt-engineering/i,
9
+ /token-usage/i,
10
+ /dev-env-bootstrap/i,
11
+ ];
12
+
13
+ export function scanLureName(pkgJson, registryMeta) {
14
+ const pkgName = pkgJson?.name || '';
15
+ const matchedPattern = LURE_PATTERNS.find(p => p.test(pkgName));
16
+ if (!matchedPattern) return { triggered: false };
17
+
18
+ const timeMap = registryMeta?.time || {};
19
+ const versions = Object.keys(timeMap).filter(v => v !== 'created' && v !== 'modified');
20
+ const firstVersion = versions.length > 0
21
+ ? versions.sort((a, b) => new Date(timeMap[a]) - new Date(timeMap[b]))[0]
22
+ : null;
23
+
24
+ if (!firstVersion) return { triggered: false };
25
+
26
+ const firstPubDate = new Date(timeMap[firstVersion]);
27
+ const now = new Date();
28
+ const daysSinceFirstPub = (now - firstPubDate) / (1000 * 60 * 60 * 24);
29
+
30
+ if (daysSinceFirstPub < 30 && versions.length <= 2) {
31
+ return {
32
+ triggered: true,
33
+ packageName: pkgName,
34
+ matchedPattern: matchedPattern.source,
35
+ firstPublished: timeMap[firstVersion],
36
+ ageDays: Math.round(daysSinceFirstPub),
37
+ versionCount: versions.length,
38
+ };
39
+ }
40
+
41
+ return { triggered: false };
42
+ }
@@ -0,0 +1,22 @@
1
+ export function scanCryptoPrimitives(allFiles, pkgJson) {
2
+ const matches = [];
3
+
4
+ const scripts = pkgJson?.scripts || {};
5
+ const scriptEntries = Object.entries(scripts)
6
+ .filter(([hook]) => /preinstall|install|postinstall|prepare/.test(hook))
7
+ .map(([hook, content]) => ({ file: `script:${hook}`, content }));
8
+
9
+ const jsFiles = allFiles
10
+ .filter(f => f.path?.endsWith('.js') || f.path?.endsWith('.mjs') || f.path?.endsWith('.cjs'))
11
+ .map(f => ({ file: f.path, content: f.content || '' }));
12
+
13
+ for (const { file, content } of [...scriptEntries, ...jsFiles]) {
14
+ const hasFernet = /Fernet/i.test(content);
15
+ const hasECDH = /\bECDH\b|\bcreateECDH\b/i.test(content);
16
+ if (hasFernet && hasECDH) {
17
+ matches.push({ file });
18
+ }
19
+ }
20
+
21
+ return { triggered: matches.length > 0, matches };
22
+ }
@@ -0,0 +1,15 @@
1
+ export function scanXorKey(allFiles) {
2
+ const matches = [];
3
+ for (const file of allFiles) {
4
+ const path = file.path?.replace(/\\/g, '/') || '';
5
+ const isLockFile = /(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|pnpm-lock\.yml|Cargo\.lock|Cargo\.toml)/i.test(path);
6
+ const isBundled = /\.node$|vendor|native/i.test(path);
7
+ if (!isLockFile && !isBundled) continue;
8
+
9
+ const content = file.content || '';
10
+ if (content.includes('cargo-build-helper-2026')) {
11
+ matches.push({ file: path });
12
+ }
13
+ }
14
+ return { triggered: matches.length > 0, matches };
15
+ }