herozion 1.0.96 → 1.0.102
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/install.js +94 -2
- package/package.json +1 -1
package/install.js
CHANGED
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
* alongside this package. Running `npx herozion scan .` or adding herozion
|
|
10
10
|
* as a dev-dependency keeps the scope limited to the project.
|
|
11
11
|
*
|
|
12
|
+
* Security model (supply-chain integrity)
|
|
13
|
+
* ----------------------------------------
|
|
14
|
+
* 1. PRIMARY trust anchor: bin/checksums.json bundled inside this npm package.
|
|
15
|
+
* These hashes are embedded at `npm publish` time and cannot be swapped by
|
|
16
|
+
* a GitHub Releases compromise alone.
|
|
17
|
+
* 2. SECONDARY verification: <binary>.sha256 downloaded from GitHub Releases.
|
|
18
|
+
* Both must agree; if either fails the binary is rejected.
|
|
19
|
+
*
|
|
12
20
|
* Supported platforms:
|
|
13
21
|
* Windows x64 → herozion-windows-amd64.exe
|
|
14
22
|
* Linux x64 → herozion-linux-amd64
|
|
@@ -21,6 +29,7 @@
|
|
|
21
29
|
const https = require("https");
|
|
22
30
|
const fs = require("fs");
|
|
23
31
|
const path = require("path");
|
|
32
|
+
const crypto = require("crypto");
|
|
24
33
|
const { execSync } = require("child_process");
|
|
25
34
|
|
|
26
35
|
const VERSION = require("./package.json").version;
|
|
@@ -71,9 +80,38 @@ function download(url, destPath, redirectCount = 0) {
|
|
|
71
80
|
|
|
72
81
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
73
82
|
|
|
83
|
+
/** Download text content (for checksum files). Returns full body as a string. */
|
|
84
|
+
function downloadText(url, redirectCount = 0) {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
if (redirectCount > 5) return reject(new Error("Too many redirects"));
|
|
87
|
+
https.get(url, { headers: { "User-Agent": "herozion-npm-installer" } }, (res) => {
|
|
88
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
89
|
+
return resolve(downloadText(res.headers.location, redirectCount + 1));
|
|
90
|
+
}
|
|
91
|
+
if (res.statusCode !== 200) {
|
|
92
|
+
return reject(new Error(`HTTP ${res.statusCode} — failed to download ${url}`));
|
|
93
|
+
}
|
|
94
|
+
let data = "";
|
|
95
|
+
res.on("data", (chunk) => { data += chunk; });
|
|
96
|
+
res.on("end", () => resolve(data));
|
|
97
|
+
res.on("error", reject);
|
|
98
|
+
}).on("error", reject);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Return the lowercase hex SHA-256 digest of a local file. */
|
|
103
|
+
function sha256File(filePath) {
|
|
104
|
+
const hash = crypto.createHash("sha256");
|
|
105
|
+
hash.update(fs.readFileSync(filePath));
|
|
106
|
+
return hash.digest("hex");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
74
111
|
async function main() {
|
|
75
112
|
const binaryName = getBinaryName();
|
|
76
113
|
const url = `${BASE_URL}/${binaryName}`;
|
|
114
|
+
const checksumUrl = `${BASE_URL}/${binaryName}.sha256`;
|
|
77
115
|
|
|
78
116
|
// Store the binary inside this package directory (node_modules/herozion/)
|
|
79
117
|
const binDir = path.join(__dirname, "bin");
|
|
@@ -82,6 +120,7 @@ async function main() {
|
|
|
82
120
|
const isWindows = process.platform === "win32";
|
|
83
121
|
const destName = isWindows ? "herozion.exe" : "herozion";
|
|
84
122
|
const destPath = path.join(binDir, destName);
|
|
123
|
+
const tmpPath = destPath + ".tmp";
|
|
85
124
|
|
|
86
125
|
// Skip download if binary already exists and matches version
|
|
87
126
|
const markerPath = path.join(binDir, `.version-${VERSION}`);
|
|
@@ -90,16 +129,69 @@ async function main() {
|
|
|
90
129
|
return;
|
|
91
130
|
}
|
|
92
131
|
|
|
132
|
+
// ── Load bundled checksums (primary trust anchor) ─────────────────────────
|
|
133
|
+
const bundledChecksumsPath = path.join(__dirname, "bin", "checksums.json");
|
|
134
|
+
let bundledHash = "";
|
|
135
|
+
try {
|
|
136
|
+
const bundled = JSON.parse(fs.readFileSync(bundledChecksumsPath, "utf8"));
|
|
137
|
+
bundledHash = (bundled[binaryName] || "").toLowerCase().trim();
|
|
138
|
+
} catch (_) {}
|
|
139
|
+
|
|
93
140
|
console.log(`herozion: downloading ${binaryName} v${VERSION}...`);
|
|
94
141
|
try {
|
|
95
|
-
|
|
142
|
+
// Download binary to a temporary path first, then verify checksum before placing
|
|
143
|
+
await download(url, tmpPath);
|
|
144
|
+
|
|
145
|
+
const actualHash = sha256File(tmpPath);
|
|
146
|
+
|
|
147
|
+
// ── Primary check: bundled checksum (trust anchor inside the npm package) ─
|
|
148
|
+
if (bundledHash) {
|
|
149
|
+
if (actualHash !== bundledHash) {
|
|
150
|
+
fs.unlinkSync(tmpPath);
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Bundled checksum mismatch for ${binaryName}.\n` +
|
|
153
|
+
` Expected (bundled) : ${bundledHash}\n` +
|
|
154
|
+
` Got : ${actualHash}\n` +
|
|
155
|
+
"The downloaded binary does not match the hash embedded in this npm package.\n" +
|
|
156
|
+
"This may indicate a supply-chain attack. Aborting installation."
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Secondary check: .sha256 from GitHub Releases ─────────────────────────
|
|
162
|
+
try {
|
|
163
|
+
const checksumData = await downloadText(checksumUrl);
|
|
164
|
+
const remoteHash = checksumData.trim().split(/\s+/)[0].toLowerCase();
|
|
165
|
+
if (remoteHash && actualHash !== remoteHash) {
|
|
166
|
+
fs.unlinkSync(tmpPath);
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Remote checksum mismatch for ${binaryName}.\n` +
|
|
169
|
+
` Expected (remote) : ${remoteHash}\n` +
|
|
170
|
+
` Got : ${actualHash}\n` +
|
|
171
|
+
"The downloaded binary does not match the GitHub Releases .sha256 file."
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
} catch (checksumErr) {
|
|
175
|
+
// If we have a valid bundled hash that already passed, remote failure is non-fatal
|
|
176
|
+
if (!bundledHash) {
|
|
177
|
+
throw checksumErr;
|
|
178
|
+
}
|
|
179
|
+
console.warn(`herozion: WARNING — could not verify remote checksum: ${checksumErr.message}`);
|
|
180
|
+
console.warn("Proceeding because bundled checksum verification passed.");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Both checks passed — move binary into place
|
|
184
|
+
fs.renameSync(tmpPath, destPath);
|
|
96
185
|
if (!isWindows) {
|
|
97
186
|
fs.chmodSync(destPath, 0o755);
|
|
98
187
|
}
|
|
99
188
|
// Write version marker so we don't re-download on repeated installs
|
|
100
189
|
fs.writeFileSync(markerPath, VERSION, "utf8");
|
|
101
|
-
|
|
190
|
+
const verifiedBy = bundledHash ? "bundled + remote SHA-256" : "remote SHA-256";
|
|
191
|
+
console.log(`herozion: binary installed successfully (verified: ${verifiedBy}).`);
|
|
102
192
|
} catch (err) {
|
|
193
|
+
// Clean up temp file if it exists
|
|
194
|
+
try { fs.unlinkSync(tmpPath); } catch (_) {}
|
|
103
195
|
// Non-fatal: warn and continue — the bin shim will give a clear error at runtime
|
|
104
196
|
console.warn(`herozion: WARNING — could not download binary: ${err.message}`);
|
|
105
197
|
console.warn("You can download it manually from:");
|