@lateos/npm-scan 1.0.0 → 1.1.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.
- package/README.de.md +3 -98
- package/README.fr.md +3 -98
- package/README.ja.md +3 -98
- package/README.md +2 -122
- package/README.zh.md +3 -98
- package/backend/cra.js +113 -21
- package/backend/db.js +18 -10
- package/backend/detectors/atk-001-lifecycle.js +5 -5
- package/backend/detectors/atk-002-obfusc.js +126 -47
- package/backend/detectors/atk-003-creds.js +8 -4
- package/backend/detectors/atk-004-persist.js +3 -3
- package/backend/detectors/atk-005-exfil.js +8 -4
- package/backend/detectors/atk-006-depconf.js +3 -3
- package/backend/detectors/atk-007-typosquat.js +64 -10
- package/backend/detectors/atk-008-tarball-tamper.js +6 -6
- package/backend/detectors/atk-009-dormant-trigger.js +9 -5
- package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
- package/backend/detectors/atk-011-transitive-prop.js +14 -13
- package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
- package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
- package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
- package/backend/detectors/axios-poisoning/index.js +77 -60
- package/backend/detectors/config/thresholds.js +48 -3
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
- package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
- package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
- package/backend/detectors/hf-impersonation/index.js +94 -31
- package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
- package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
- package/backend/detectors/hf-impersonation/simhash.js +2 -2
- package/backend/detectors/index.js +181 -34
- package/backend/detectors/lib/ast-patterns.js +4 -1
- package/backend/detectors/lib/entropy-analyzer.js +12 -4
- package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
- package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
- package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
- package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
- package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
- package/backend/detectors/megalodon/index.js +35 -25
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
- package/backend/detectors/mini-shai-hulud/index.js +63 -26
- package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
- package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
- package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
- package/backend/detectors/msh-supplement/index.js +78 -63
- package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
- package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
- package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
- package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
- package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
- package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
- package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
- package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
- package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
- package/backend/detectors/node-ipc-compromise/index.js +21 -15
- package/backend/detectors/tier1-binary-embed.js +109 -41
- package/backend/detectors/tier1-cloud-imds.js +57 -37
- package/backend/detectors/tier1-encrypted-c2.js +198 -0
- package/backend/detectors/tier1-infostealer.js +121 -68
- package/backend/detectors/tier1-lifecycle-hook.js +63 -23
- package/backend/detectors/tier1-maintainer-compromise.js +157 -0
- package/backend/detectors/tier1-metadata-spoof.js +92 -42
- package/backend/detectors/tier1-multistage-postinstall.js +46 -19
- package/backend/detectors/tier1-obfuscation-heuristics.js +45 -17
- package/backend/detectors/tier1-self-propagation.js +115 -0
- package/backend/detectors/tier1-slsa-attestation.js +1 -1
- package/backend/detectors/tier1-transitive-deps.js +182 -0
- package/backend/detectors/tier1-typosquat.js +129 -50
- package/backend/detectors/tier1-version-anomaly.js +77 -41
- package/backend/detectors/tier1-version-confusion.js +79 -59
- package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
- package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
- package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
- package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
- package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
- package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
- package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
- package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
- package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
- package/backend/detectors/trapdoor/index.js +19 -14
- package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
- package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
- package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
- package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
- package/backend/detectors.test.js +78 -19
- package/backend/fetch.js +37 -29
- package/backend/index.js +1 -1
- package/backend/license.js +20 -4
- package/backend/lockfile.js +60 -36
- package/backend/pdf.js +107 -28
- package/backend/policy.js +183 -56
- package/backend/provenance.js +28 -3
- package/backend/report.js +136 -70
- package/backend/sbom.js +33 -27
- package/backend/scripts/analyze-false-positives.js +14 -8
- package/backend/scripts/analyze-validation.js +27 -21
- package/backend/scripts/detect-false-positives.js +20 -10
- package/backend/scripts/fetch-top-packages.js +197 -49
- package/backend/scripts/validate-d10-d13.js +103 -0
- package/backend/scripts/validate-detectors.js +26 -17
- package/backend/siem/cef.js +23 -21
- package/backend/siem/ecs.js +3 -3
- package/backend/siem/index.js +1 -1
- package/backend/siem/qradar.js +3 -3
- package/backend/siem/sentinel.js +2 -2
- package/backend/tests-d5-enhanced.test.js +13 -12
- package/backend/tests-d6-version-anomaly.test.js +17 -8
- package/backend/tests-d6.test.js +24 -14
- package/backend/tests-d6c.test.js +27 -14
- package/backend/tests-d7-obfuscation.test.js +9 -12
- package/backend/tests.test.js +182 -83
- package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
- package/backend/vsix-scan/detectors/burst-publish.js +14 -7
- package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
- package/backend/vsix-scan/detectors/known-ioc.js +23 -8
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
- package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
- package/backend/vsix-scan/index.js +97 -41
- package/backend/vsix-scan/marketplace-client.js +29 -13
- package/cli/cli.js +154 -64
- package/package.json +12 -3
|
@@ -35,7 +35,9 @@ const ANTI_ANALYSIS_PATTERNS = [
|
|
|
35
35
|
];
|
|
36
36
|
|
|
37
37
|
function truncateSnippet(str, maxLen = 200) {
|
|
38
|
-
if (!str || str.length <= maxLen)
|
|
38
|
+
if (!str || str.length <= maxLen) {
|
|
39
|
+
return str || '';
|
|
40
|
+
}
|
|
39
41
|
return str.slice(0, maxLen) + '...';
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -46,14 +48,16 @@ export async function checkExfilPattern(extensionFiles = []) {
|
|
|
46
48
|
|
|
47
49
|
for (const file of extensionFiles) {
|
|
48
50
|
const content = typeof file.content === 'string' ? file.content : '';
|
|
49
|
-
if (!content)
|
|
51
|
+
if (!content) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
50
54
|
const path = file.path || '';
|
|
51
55
|
|
|
52
56
|
for (const cp of CREDENTIAL_FILE_PATTERNS) {
|
|
53
57
|
const match = content.match(cp);
|
|
54
58
|
if (match) {
|
|
55
59
|
const snippet = truncateSnippet(match[0]);
|
|
56
|
-
if (!exfilPatterns.some(e => e.includes(snippet))) {
|
|
60
|
+
if (!exfilPatterns.some((e) => e.includes(snippet))) {
|
|
57
61
|
exfilPatterns.push(`${path}: ${snippet}`);
|
|
58
62
|
signals.push({ type: 'CREDENTIAL_FILE_TARGET', pattern: cp.source, file: path });
|
|
59
63
|
}
|
|
@@ -11,7 +11,9 @@ const __dirname = dirname(__filename);
|
|
|
11
11
|
const IOC_PATH = join(__dirname, '..', 'vsix-iocs.json');
|
|
12
12
|
|
|
13
13
|
function loadIOCData() {
|
|
14
|
-
if (iocsLoaded)
|
|
14
|
+
if (iocsLoaded) {
|
|
15
|
+
return iocsData;
|
|
16
|
+
}
|
|
15
17
|
iocsLoaded = true;
|
|
16
18
|
try {
|
|
17
19
|
iocsData = JSON.parse(readFileSync(IOC_PATH, 'utf8'));
|
|
@@ -32,9 +34,17 @@ export function reloadIOCData() {
|
|
|
32
34
|
return loadIOCData();
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
export async function checkKnownIOC(
|
|
37
|
+
export async function checkKnownIOC(
|
|
38
|
+
extensionId,
|
|
39
|
+
version,
|
|
40
|
+
publisherAccount,
|
|
41
|
+
orphanCommits = [],
|
|
42
|
+
versionHistory = []
|
|
43
|
+
) {
|
|
36
44
|
const data = loadIOCData();
|
|
37
|
-
if (!data)
|
|
45
|
+
if (!data) {
|
|
46
|
+
return { triggered: false, matches: [] };
|
|
47
|
+
}
|
|
38
48
|
|
|
39
49
|
const matches = [];
|
|
40
50
|
const iocs = data.iocs || [];
|
|
@@ -43,7 +53,11 @@ export async function checkKnownIOC(extensionId, version, publisherAccount, orph
|
|
|
43
53
|
switch (ioc.type) {
|
|
44
54
|
case 'extensionId': {
|
|
45
55
|
if (ioc.value === extensionId) {
|
|
46
|
-
if (
|
|
56
|
+
if (
|
|
57
|
+
!ioc.maliciousVersions ||
|
|
58
|
+
ioc.maliciousVersions.length === 0 ||
|
|
59
|
+
ioc.maliciousVersions.includes(version)
|
|
60
|
+
) {
|
|
47
61
|
matches.push({
|
|
48
62
|
type: 'extensionId',
|
|
49
63
|
value: extensionId,
|
|
@@ -60,9 +74,10 @@ export async function checkKnownIOC(extensionId, version, publisherAccount, orph
|
|
|
60
74
|
|
|
61
75
|
case 'publisherAccount': {
|
|
62
76
|
if (ioc.value === publisherAccount) {
|
|
63
|
-
const pubTime =
|
|
64
|
-
|
|
65
|
-
|
|
77
|
+
const pubTime =
|
|
78
|
+
versionHistory.length > 0
|
|
79
|
+
? new Date(versionHistory[versionHistory.length - 1]?.publishedAt).getTime()
|
|
80
|
+
: null;
|
|
66
81
|
|
|
67
82
|
const windowStart = new Date(ioc.compromiseWindowStart).getTime();
|
|
68
83
|
const windowEnd = ioc.compromiseWindowEnd
|
|
@@ -84,7 +99,7 @@ export async function checkKnownIOC(extensionId, version, publisherAccount, orph
|
|
|
84
99
|
|
|
85
100
|
case 'orphanCommitHash': {
|
|
86
101
|
for (const commit of orphanCommits) {
|
|
87
|
-
if (ioc.value === commit ||
|
|
102
|
+
if (ioc.value === commit || ioc.value === 'PLACEHOLDER_UPDATE_FROM_THREAT_INTEL') {
|
|
88
103
|
continue;
|
|
89
104
|
}
|
|
90
105
|
if (ioc.value && commit && ioc.value.toLowerCase() === commit.toLowerCase()) {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
const GITHUB_COMMIT_SHA_PATTERN =
|
|
1
|
+
const GITHUB_COMMIT_SHA_PATTERN =
|
|
2
|
+
/api\.github\.com\/repos\/[^/]+\/[^/]+\/git\/commits\/[a-f0-9]{40}/;
|
|
2
3
|
const NPX_GIT_URL_PATTERN = /npx\s+.*github\.com.*#[a-f0-9]{8,}/;
|
|
3
4
|
const MCP_KEYWORDS = ['mcp', 'model-context-protocol', 'claude', 'setup', 'init'];
|
|
4
|
-
const
|
|
5
|
-
|
|
5
|
+
const _EXTERNAL_FETCH_PATTERN =
|
|
6
|
+
/(?:https?:\/\/)[^\s"')\]]+(?:\.com|\.io|\.org|\.dev|\.app|\.net)[^\s"')\]]*/;
|
|
7
|
+
const NON_NPMJS_FETCH =
|
|
8
|
+
/(?:fetch|curl|wget)\s*\(?\s*["']https?:\/\/(?!(?:.*npmjs\.org|.*npm\.js\.org|.*github\.com))[^"']+/;
|
|
6
9
|
const BUN_PATTERNS = [/bun\s+install/, /install\s+.*bun/, /\bbunx\b/, /\.bun\/bin\//];
|
|
7
|
-
const
|
|
10
|
+
const _NPX_GIT_SHORT = /npx\s+.*github\.com.*#[a-f0-9]{8,}/;
|
|
8
11
|
|
|
9
12
|
export async function checkOrphanCommitFetch(extensionFiles = []) {
|
|
10
13
|
const signals = [];
|
|
@@ -12,7 +15,9 @@ export async function checkOrphanCommitFetch(extensionFiles = []) {
|
|
|
12
15
|
|
|
13
16
|
for (const file of extensionFiles) {
|
|
14
17
|
const content = typeof file.content === 'string' ? file.content : '';
|
|
15
|
-
if (!content)
|
|
18
|
+
if (!content) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
16
21
|
const path = file.path || '';
|
|
17
22
|
|
|
18
23
|
if (GITHUB_COMMIT_SHA_PATTERN.test(content)) {
|
|
@@ -39,8 +44,7 @@ export async function checkOrphanCommitFetch(extensionFiles = []) {
|
|
|
39
44
|
}
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
const hasMCPKeywords = MCP_KEYWORDS.some(kw =>
|
|
43
|
-
new RegExp(`\\b${kw}\\b`, 'i').test(content));
|
|
47
|
+
const hasMCPKeywords = MCP_KEYWORDS.some((kw) => new RegExp(`\\b${kw}\\b`, 'i').test(content));
|
|
44
48
|
const hasExternalFetch = NON_NPMJS_FETCH.test(content);
|
|
45
49
|
|
|
46
50
|
if (hasMCPKeywords && hasExternalFetch) {
|
|
@@ -1,24 +1,33 @@
|
|
|
1
|
-
export async function checkPublisherAnomaly(
|
|
1
|
+
export async function checkPublisherAnomaly(
|
|
2
|
+
extensionMetadata,
|
|
3
|
+
publisherProfile,
|
|
4
|
+
versionHistory,
|
|
5
|
+
config = {}
|
|
6
|
+
) {
|
|
2
7
|
const signals = [];
|
|
3
8
|
|
|
4
|
-
const
|
|
5
|
-
const
|
|
9
|
+
const _crossNamespaceThreshold = config.crossNamespaceThreshold ?? 3;
|
|
10
|
+
const _crossNamespaceDays = config.crossNamespaceDays ?? 14;
|
|
6
11
|
const newAccountAgeDays = config.newAccountAgeDays ?? 30;
|
|
7
12
|
const highInstallThreshold = config.highInstallThreshold ?? 100000;
|
|
8
13
|
const addPublishWindowMinutes = config.addPublishWindowMinutes ?? 15;
|
|
9
14
|
|
|
10
15
|
const versions = versionHistory || [];
|
|
11
|
-
if (versions.length === 0)
|
|
16
|
+
if (versions.length === 0) {
|
|
17
|
+
return { triggered: false, signals: [] };
|
|
18
|
+
}
|
|
12
19
|
|
|
13
|
-
const publishers = [...new Set(versions.map(v => v.publishedBy).filter(Boolean))];
|
|
14
|
-
if (publishers.length === 0)
|
|
20
|
+
const publishers = [...new Set(versions.map((v) => v.publishedBy).filter(Boolean))];
|
|
21
|
+
if (publishers.length === 0) {
|
|
22
|
+
return { triggered: false, signals: [] };
|
|
23
|
+
}
|
|
15
24
|
|
|
16
25
|
const sortedVersions = [...versions]
|
|
17
|
-
.filter(v => v.publishedAt)
|
|
26
|
+
.filter((v) => v.publishedAt)
|
|
18
27
|
.sort((a, b) => new Date(a.publishedAt) - new Date(b.publishedAt));
|
|
19
28
|
|
|
20
29
|
const extPublisher = publishers[0];
|
|
21
|
-
const allSame = publishers.every(p => p === extPublisher);
|
|
30
|
+
const allSame = publishers.every((p) => p === extPublisher);
|
|
22
31
|
|
|
23
32
|
if (!allSame) {
|
|
24
33
|
for (const pub of publishers) {
|
|
@@ -32,13 +41,18 @@ export async function checkPublisherAnomaly(extensionMetadata, publisherProfile,
|
|
|
32
41
|
}
|
|
33
42
|
}
|
|
34
43
|
|
|
35
|
-
const extInstallCount =
|
|
44
|
+
const extInstallCount =
|
|
45
|
+
extensionMetadata?.statistics?.find((s) => s.statisticName === 'install')?.value || 0;
|
|
36
46
|
|
|
37
47
|
const extAgeDays = publisherProfile?.dateCreated
|
|
38
48
|
? (Date.now() - new Date(publisherProfile.dateCreated).getTime()) / (1000 * 60 * 60 * 24)
|
|
39
49
|
: null;
|
|
40
50
|
|
|
41
|
-
if (
|
|
51
|
+
if (
|
|
52
|
+
extAgeDays !== null &&
|
|
53
|
+
extAgeDays < newAccountAgeDays &&
|
|
54
|
+
extInstallCount >= highInstallThreshold
|
|
55
|
+
) {
|
|
42
56
|
signals.push({
|
|
43
57
|
type: 'NEW_ACCOUNT_HIGH_INSTALL',
|
|
44
58
|
accountAgeDays: Math.round(extAgeDays),
|
|
@@ -4,18 +4,32 @@ import { checkActivationEventRisk } from './detectors/activation-event-risk.js';
|
|
|
4
4
|
import { checkOrphanCommitFetch } from './detectors/orphan-commit-fetch.js';
|
|
5
5
|
import { checkKnownIOC } from './detectors/known-ioc.js';
|
|
6
6
|
import { checkExfilPattern } from './detectors/exfil-pattern.js';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import {
|
|
8
|
+
getExtensionMetadata,
|
|
9
|
+
getVersionHistory,
|
|
10
|
+
getPublisherProfile,
|
|
11
|
+
getOpenVsxMetadata as _getOpenVsxMetadata,
|
|
12
|
+
getOpenVsxVersionHistory,
|
|
13
|
+
} from './marketplace-client.js';
|
|
14
|
+
|
|
15
|
+
const _SEVERITY_SCORE = { none: 0, low: 1, medium: 2, high: 3, critical: 4 };
|
|
10
16
|
const SEVERITY_LABELS = ['none', 'low', 'medium', 'high', 'critical'];
|
|
11
17
|
|
|
12
18
|
export async function vsixScan(extensionId, options = {}) {
|
|
13
19
|
const { publisherId, extensionName } = parseExtensionId(extensionId);
|
|
14
20
|
|
|
15
|
-
const marketplaceMeta =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
21
|
+
const marketplaceMeta =
|
|
22
|
+
options.marketplaceMeta ||
|
|
23
|
+
(options.skipNetwork ? null : await getExtensionMetadata(publisherId, extensionName));
|
|
24
|
+
const marketplaceVersions =
|
|
25
|
+
options.marketplaceVersions ||
|
|
26
|
+
(marketplaceMeta ? await getVersionHistory(publisherId, extensionName) : []);
|
|
27
|
+
const openVsxVersions =
|
|
28
|
+
options.openVsxVersions ||
|
|
29
|
+
(options.skipNetwork ? [] : await getOpenVsxVersionHistory(publisherId, extensionName));
|
|
30
|
+
const publisherProfile =
|
|
31
|
+
options.publisherProfile ||
|
|
32
|
+
(options.skipNetwork ? null : await getPublisherProfile(publisherId));
|
|
19
33
|
|
|
20
34
|
const allVersions = mergeVersionHistories(marketplaceVersions, openVsxVersions);
|
|
21
35
|
const manifest = options.manifest || extractManifest(marketplaceMeta, extensionId);
|
|
@@ -25,7 +39,7 @@ export async function vsixScan(extensionId, options = {}) {
|
|
|
25
39
|
const activationResult = await checkActivationEventRisk(
|
|
26
40
|
manifest,
|
|
27
41
|
allVersions,
|
|
28
|
-
options.priorVersions || []
|
|
42
|
+
options.priorVersions || []
|
|
29
43
|
);
|
|
30
44
|
|
|
31
45
|
const burstResult = await checkBurstPublish(allVersions, config);
|
|
@@ -34,50 +48,82 @@ export async function vsixScan(extensionId, options = {}) {
|
|
|
34
48
|
manifest || {},
|
|
35
49
|
publisherProfile || {},
|
|
36
50
|
allVersions,
|
|
37
|
-
config
|
|
51
|
+
config
|
|
38
52
|
);
|
|
39
53
|
|
|
40
54
|
const orphanResult = await checkOrphanCommitFetch(options.extensionFiles || []);
|
|
41
55
|
|
|
42
56
|
const iocResult = await checkKnownIOC(
|
|
43
57
|
extensionId,
|
|
44
|
-
options.version ||
|
|
58
|
+
options.version ||
|
|
59
|
+
(allVersions.length > 0 ? allVersions[allVersions.length - 1].version : 'unknown'),
|
|
45
60
|
publisherId,
|
|
46
61
|
orphanResult.signals
|
|
47
|
-
.filter(s => s.type === 'ORPHAN_COMMIT_GITHUB_API')
|
|
48
|
-
.map(s => s.indicator),
|
|
49
|
-
allVersions
|
|
62
|
+
.filter((s) => s.type === 'ORPHAN_COMMIT_GITHUB_API')
|
|
63
|
+
.map((s) => s.indicator),
|
|
64
|
+
allVersions
|
|
50
65
|
);
|
|
51
66
|
|
|
52
67
|
const exfilResult = await checkExfilPattern(options.extensionFiles || []);
|
|
53
68
|
|
|
54
69
|
const triggeredSignals = [];
|
|
55
|
-
if (burstResult.triggered)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
70
|
+
if (burstResult.triggered) {
|
|
71
|
+
triggeredSignals.push('VSIX_BURST_PUBLISH');
|
|
72
|
+
}
|
|
73
|
+
if (publisherResult.triggered) {
|
|
74
|
+
triggeredSignals.push('VSIX_PUBLISHER_ANOMALY');
|
|
75
|
+
}
|
|
76
|
+
if (activationResult.triggered) {
|
|
77
|
+
triggeredSignals.push('VSIX_ACTIVATION_EVENT_RISK');
|
|
78
|
+
}
|
|
79
|
+
if (orphanResult.triggered) {
|
|
80
|
+
triggeredSignals.push('VSIX_ORPHAN_COMMIT_FETCH');
|
|
81
|
+
}
|
|
82
|
+
if (iocResult.triggered) {
|
|
83
|
+
triggeredSignals.push('VSIX_KNOWN_IOC');
|
|
84
|
+
}
|
|
85
|
+
if (exfilResult.triggered) {
|
|
86
|
+
triggeredSignals.push('VSIX_EXFIL_PATTERN');
|
|
87
|
+
}
|
|
61
88
|
|
|
62
|
-
if (triggeredSignals.length === 0)
|
|
89
|
+
if (triggeredSignals.length === 0) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
63
92
|
|
|
64
93
|
const registryLabels = [];
|
|
65
|
-
if (marketplaceVersions.length > 0)
|
|
66
|
-
|
|
94
|
+
if (marketplaceVersions.length > 0) {
|
|
95
|
+
registryLabels.push('marketplace');
|
|
96
|
+
}
|
|
97
|
+
if (openVsxVersions.length > 0) {
|
|
98
|
+
registryLabels.push('open-vsx');
|
|
99
|
+
}
|
|
67
100
|
|
|
68
101
|
const maxSeverity = triggeredSignals.reduce((max, s) => {
|
|
69
|
-
if (s === 'VSIX_KNOWN_IOC' || s === 'VSIX_ORPHAN_COMMIT_FETCH')
|
|
70
|
-
|
|
71
|
-
|
|
102
|
+
if (s === 'VSIX_KNOWN_IOC' || s === 'VSIX_ORPHAN_COMMIT_FETCH') {
|
|
103
|
+
return Math.max(max, 4);
|
|
104
|
+
}
|
|
105
|
+
if (
|
|
106
|
+
s === 'VSIX_BURST_PUBLISH' ||
|
|
107
|
+
s === 'VSIX_PUBLISHER_ANOMALY' ||
|
|
108
|
+
s === 'VSIX_EXFIL_PATTERN'
|
|
109
|
+
) {
|
|
110
|
+
return Math.max(max, 3);
|
|
111
|
+
}
|
|
112
|
+
if (s === 'VSIX_ACTIVATION_EVENT_RISK') {
|
|
113
|
+
return Math.max(max, 3);
|
|
114
|
+
}
|
|
72
115
|
return max;
|
|
73
116
|
}, 0);
|
|
74
117
|
|
|
75
118
|
const finalSeverity = SEVERITY_LABELS[maxSeverity] || 'high';
|
|
76
119
|
|
|
77
|
-
const latestVersion =
|
|
120
|
+
const latestVersion =
|
|
121
|
+
allVersions.length > 0 ? allVersions[allVersions.length - 1].version : 'unknown';
|
|
78
122
|
let exposureWindowMinutes = null;
|
|
79
123
|
if (burstResult.hotPullDetected && allVersions.length >= 2) {
|
|
80
|
-
const sorted = [...allVersions].sort(
|
|
124
|
+
const sorted = [...allVersions].sort(
|
|
125
|
+
(a, b) => new Date(b.publishedAt) - new Date(a.publishedAt)
|
|
126
|
+
);
|
|
81
127
|
const gap = (new Date(sorted[0].publishedAt) - new Date(sorted[1].publishedAt)) / (1000 * 60);
|
|
82
128
|
exposureWindowMinutes = Math.round(gap);
|
|
83
129
|
}
|
|
@@ -92,7 +138,9 @@ export async function vsixScan(extensionId, options = {}) {
|
|
|
92
138
|
hotPullDetected: burstResult.hotPullDetected,
|
|
93
139
|
publisherSignals: publisherResult.triggered ? publisherResult.signals : null,
|
|
94
140
|
activationEvents: manifest?.activationEvents || null,
|
|
95
|
-
activationRisk: activationResult.triggered
|
|
141
|
+
activationRisk: activationResult.triggered
|
|
142
|
+
? { riskLevel: activationResult.riskLevel, why: activationResult.why }
|
|
143
|
+
: null,
|
|
96
144
|
orphanCommitIndicators: orphanResult.triggered ? orphanResult.indicators : null,
|
|
97
145
|
iocMatches: iocResult.triggered ? iocResult.matches : null,
|
|
98
146
|
exfilPatterns: exfilResult.triggered ? exfilResult.exfilPatterns : null,
|
|
@@ -101,14 +149,16 @@ export async function vsixScan(extensionId, options = {}) {
|
|
|
101
149
|
|
|
102
150
|
const remediationGuidance = buildRemediation(triggeredSignals, extensionId);
|
|
103
151
|
|
|
104
|
-
return [
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
152
|
+
return [
|
|
153
|
+
{
|
|
154
|
+
id: 'VSIX_SCAN',
|
|
155
|
+
severity: finalSeverity,
|
|
156
|
+
title: `VS Code extension risk: ${extensionId}`,
|
|
157
|
+
description: `${triggeredSignals.length} signal(s): ${triggeredSignals.join(', ')}`,
|
|
158
|
+
evidence: JSON.stringify(evidence),
|
|
159
|
+
mitigation: remediationGuidance,
|
|
160
|
+
},
|
|
161
|
+
];
|
|
112
162
|
}
|
|
113
163
|
|
|
114
164
|
function parseExtensionId(id) {
|
|
@@ -135,7 +185,7 @@ function mergeVersionHistories(marketplace, openVsx) {
|
|
|
135
185
|
seen.add(v.version);
|
|
136
186
|
merged.push({ ...v, registries: ['open-vsx'] });
|
|
137
187
|
} else {
|
|
138
|
-
const existing = merged.find(m => m.version === v.version);
|
|
188
|
+
const existing = merged.find((m) => m.version === v.version);
|
|
139
189
|
if (existing) {
|
|
140
190
|
existing.registries.push('open-vsx');
|
|
141
191
|
}
|
|
@@ -145,14 +195,20 @@ function mergeVersionHistories(marketplace, openVsx) {
|
|
|
145
195
|
return merged.sort((a, b) => new Date(a.publishedAt) - new Date(b.publishedAt));
|
|
146
196
|
}
|
|
147
197
|
|
|
148
|
-
function extractManifest(marketplaceMeta,
|
|
149
|
-
if (!marketplaceMeta?.results?.[0]?.extensions?.[0])
|
|
198
|
+
function extractManifest(marketplaceMeta, _extensionId) {
|
|
199
|
+
if (!marketplaceMeta?.results?.[0]?.extensions?.[0]) {
|
|
200
|
+
return {};
|
|
201
|
+
}
|
|
150
202
|
const ext = marketplaceMeta.results[0].extensions[0];
|
|
151
203
|
const manifestStr = ext.galleryApiUrl || ext.manifest;
|
|
152
|
-
if (!manifestStr)
|
|
204
|
+
if (!manifestStr) {
|
|
205
|
+
return {};
|
|
206
|
+
}
|
|
153
207
|
|
|
154
208
|
try {
|
|
155
|
-
if (typeof manifestStr === 'object')
|
|
209
|
+
if (typeof manifestStr === 'object') {
|
|
210
|
+
return manifestStr;
|
|
211
|
+
}
|
|
156
212
|
return JSON.parse(manifestStr);
|
|
157
213
|
} catch {
|
|
158
214
|
return {};
|
|
@@ -7,7 +7,7 @@ const RATE_LIMIT_MS = 6000;
|
|
|
7
7
|
let _lastFetchTime = 0;
|
|
8
8
|
|
|
9
9
|
function sleep(ms) {
|
|
10
|
-
return new Promise(r => setTimeout(r, ms));
|
|
10
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
async function rateLimitedFetch(url) {
|
|
@@ -44,20 +44,22 @@ async function rateLimitedFetch(url) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function
|
|
47
|
+
function _parseExtensionId(id) {
|
|
48
48
|
const parts = id.split('.');
|
|
49
|
-
if (parts.length < 2)
|
|
49
|
+
if (parts.length < 2) {
|
|
50
|
+
throw new Error(`Invalid extension ID: ${id}`);
|
|
51
|
+
}
|
|
50
52
|
return { publisherId: parts[0], extensionName: parts.slice(1).join('.') };
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
export async function getExtensionMetadata(publisherId, extensionName) {
|
|
54
56
|
const url = `${MARKETPLACE_API}/extensionquery`;
|
|
55
57
|
const body = {
|
|
56
|
-
filters: [
|
|
57
|
-
|
|
58
|
-
{ filterType: 8, value: `${publisherId}.${extensionName}` },
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
filters: [
|
|
59
|
+
{
|
|
60
|
+
criteria: [{ filterType: 8, value: `${publisherId}.${extensionName}` }],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
61
63
|
flags: 914,
|
|
62
64
|
};
|
|
63
65
|
|
|
@@ -77,13 +79,23 @@ export async function getExtensionMetadata(publisherId, extensionName) {
|
|
|
77
79
|
try {
|
|
78
80
|
res = await fetch(url, {
|
|
79
81
|
method: 'POST',
|
|
80
|
-
headers: {
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'application/json',
|
|
84
|
+
Accept: 'application/json;api-version=3.0-preview.1',
|
|
85
|
+
},
|
|
81
86
|
body: JSON.stringify(body),
|
|
82
87
|
});
|
|
83
88
|
if (res.status === 429) {
|
|
84
89
|
const retryAfter = parseInt(res.headers.get('Retry-After') || '10', 10);
|
|
85
90
|
await sleep(retryAfter * 1000);
|
|
86
|
-
res = await fetch(url, {
|
|
91
|
+
res = await fetch(url, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: {
|
|
94
|
+
'Content-Type': 'application/json',
|
|
95
|
+
Accept: 'application/json;api-version=3.0-preview.1',
|
|
96
|
+
},
|
|
97
|
+
body: JSON.stringify(body),
|
|
98
|
+
});
|
|
87
99
|
}
|
|
88
100
|
if (!res.ok) {
|
|
89
101
|
console.debug(`Marketplace API warning: ${url} returned ${res.status}`);
|
|
@@ -100,12 +112,14 @@ export async function getExtensionMetadata(publisherId, extensionName) {
|
|
|
100
112
|
|
|
101
113
|
export async function getVersionHistory(publisherId, extensionName) {
|
|
102
114
|
const data = await getExtensionMetadata(publisherId, extensionName);
|
|
103
|
-
if (!data?.results?.[0]?.extensions?.[0])
|
|
115
|
+
if (!data?.results?.[0]?.extensions?.[0]) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
104
118
|
|
|
105
119
|
const extension = data.results[0].extensions[0];
|
|
106
120
|
const versions = extension.versions || [];
|
|
107
121
|
|
|
108
|
-
return versions.map(v => ({
|
|
122
|
+
return versions.map((v) => ({
|
|
109
123
|
version: v.version,
|
|
110
124
|
publishedAt: v.lastUpdated || v.publishedDate,
|
|
111
125
|
publishedBy: extension.publisher?.publisherName || publisherId,
|
|
@@ -126,7 +140,9 @@ export async function getOpenVsxMetadata(namespace, name) {
|
|
|
126
140
|
|
|
127
141
|
export async function getOpenVsxVersionHistory(namespace, name) {
|
|
128
142
|
const data = await getOpenVsxMetadata(namespace, name);
|
|
129
|
-
if (!data)
|
|
143
|
+
if (!data) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
130
146
|
const versions = data.allVersions || {};
|
|
131
147
|
const files = data.files || {};
|
|
132
148
|
|