herozion 1.0.100 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/install.js +94 -2
  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
- await download(url, destPath);
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
- console.log(`herozion: binary installed successfully.`);
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:");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "herozion",
3
- "version": "1.0.100",
3
+ "version": "1.1.0",
4
4
  "description": "Security audit and performance analysis CLI tool for developers",
5
5
  "keywords": [
6
6
  "security",