@rightkit/release 0.2.80 → 0.2.82
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/build-release.mjs +18 -2
- package/hardeningscan.mjs +144 -139
- package/package.json +1 -1
- package/release.mjs +7 -1
- package/rightkit-versions.json +1 -1
package/build-release.mjs
CHANGED
|
@@ -168,7 +168,15 @@ try {
|
|
|
168
168
|
// outlived the change that was supposed to retire them.
|
|
169
169
|
pipeline: PIPELINE_FINGERPRINT,
|
|
170
170
|
configSha256: hashFile(configPath),
|
|
171
|
-
|
|
171
|
+
// Resolve each phase receipt the same way signing does: the config's
|
|
172
|
+
// sign.receipt / sign.installerReceipt override wins, the default
|
|
173
|
+
// windows-<phase>.json name is only the fallback. Hardcoding the default
|
|
174
|
+
// here made the seal read a path the signer never wrote whenever a config
|
|
175
|
+
// (e.g. an architecture-qualified receipt) overrode it.
|
|
176
|
+
receiptInputs: ["raw-exe", "installer", "embedding"].map((phase) => {
|
|
177
|
+
const configured = phase === "raw-exe" ? target.sign?.receipt : phase === "installer" ? target.sign?.installerReceipt : undefined;
|
|
178
|
+
return configured ? path.resolve(appRoot, configured) : path.join(receiptRoot, `windows-${phase}.json`);
|
|
179
|
+
}),
|
|
172
180
|
} : null;
|
|
173
181
|
if (signingIdentity) inputHashes[".right-release/signing-identity.json"] = hashFileText(JSON.stringify(signingIdentity));
|
|
174
182
|
const cargoLock = nativeLayout.lockPath;
|
|
@@ -435,7 +443,15 @@ function sealRelease({ configRoot, managedCargoTarget, nativeLayout, sealedDir,
|
|
|
435
443
|
commit,
|
|
436
444
|
platform,
|
|
437
445
|
cacheKey,
|
|
438
|
-
|
|
446
|
+
// Portable contracts sign only raw executables — installer/embedding
|
|
447
|
+
// receipts legitimately never exist there. Seal every receipt that was
|
|
448
|
+
// produced, but never zero: a signed platform with no receipt at all is
|
|
449
|
+
// an unsound seal, not a portable release.
|
|
450
|
+
signing: signingIdentity ? { ...signingIdentity, receipts: (() => {
|
|
451
|
+
const present = signingIdentity.receiptInputs.filter((file) => existsSync(file));
|
|
452
|
+
if (present.length === 0) throw new Error(`no signing receipts found to seal; expected at least ${signingIdentity.receiptInputs[0]}`);
|
|
453
|
+
return present.map((file) => ({ file, sha256: hashFile(file) }));
|
|
454
|
+
})() } : null,
|
|
439
455
|
files,
|
|
440
456
|
routes,
|
|
441
457
|
inputs: inputHashes,
|
package/hardeningscan.mjs
CHANGED
|
@@ -1,139 +1,144 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { createReadStream, readFileSync, realpathSync } from "node:fs";
|
|
4
|
-
import { lstat, readdir } from "node:fs/promises";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { HARDENING_RULES } from "./hardening-evidence.mjs";
|
|
7
|
-
|
|
8
|
-
const rawArgs = process.argv.slice(2);
|
|
9
|
-
if (rawArgs.length === 0 || rawArgs.includes("-h") || rawArgs.includes("--help")) {
|
|
10
|
-
console.error("usage: node hardeningscan.mjs [--allow-evidence <json>] <artifact-path>...");
|
|
11
|
-
process.exit(rawArgs.length === 0 ? 2 : 0);
|
|
12
|
-
}
|
|
13
|
-
const allowIndex = rawArgs.indexOf("--allow-evidence");
|
|
14
|
-
const allowEvidencePath = allowIndex >= 0 ? rawArgs[allowIndex + 1] : null;
|
|
15
|
-
if (allowIndex >= 0 && !allowEvidencePath) throw new Error("--allow-evidence requires a JSON path");
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
35
|
-
throw new Error("hardening allowance source
|
|
36
|
-
}
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
[
|
|
50
|
-
[
|
|
51
|
-
[
|
|
52
|
-
[
|
|
53
|
-
[/[/\\]
|
|
54
|
-
]
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
[
|
|
58
|
-
[
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
["
|
|
63
|
-
["
|
|
64
|
-
["
|
|
65
|
-
["
|
|
66
|
-
["
|
|
67
|
-
]
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (st.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
console.
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { createReadStream, readFileSync, realpathSync } from "node:fs";
|
|
4
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { HARDENING_RULES } from "./hardening-evidence.mjs";
|
|
7
|
+
|
|
8
|
+
const rawArgs = process.argv.slice(2);
|
|
9
|
+
if (rawArgs.length === 0 || rawArgs.includes("-h") || rawArgs.includes("--help")) {
|
|
10
|
+
console.error("usage: node hardeningscan.mjs [--allow-evidence <json>] <artifact-path>...");
|
|
11
|
+
process.exit(rawArgs.length === 0 ? 2 : 0);
|
|
12
|
+
}
|
|
13
|
+
const allowIndex = rawArgs.indexOf("--allow-evidence");
|
|
14
|
+
const allowEvidencePath = allowIndex >= 0 ? rawArgs[allowIndex + 1] : null;
|
|
15
|
+
if (allowIndex >= 0 && !allowEvidencePath) throw new Error("--allow-evidence requires a JSON path");
|
|
16
|
+
// Only strip the flag pair when the flag is present: with allowIndex = -1,
|
|
17
|
+
// filtering out index `allowIndex + 1` would silently drop the first
|
|
18
|
+
// artifact path (a lone artifact arg then failed as "no artifact path").
|
|
19
|
+
const args = allowIndex >= 0
|
|
20
|
+
? rawArgs.filter((_, index) => index !== allowIndex && index !== allowIndex + 1)
|
|
21
|
+
: rawArgs;
|
|
22
|
+
if (!args.length) throw new Error("at least one artifact path is required");
|
|
23
|
+
|
|
24
|
+
const root = realpathSync(process.cwd());
|
|
25
|
+
const findings = [];
|
|
26
|
+
const evidence = allowEvidencePath ? JSON.parse(readFileSync(allowEvidencePath, "utf8")) : { allowances: [] };
|
|
27
|
+
if (allowEvidencePath && (evidence.schemaVersion !== 1 || evidence.kind !== "rightkit-hardening-evidence")) {
|
|
28
|
+
throw new Error("invalid hardening evidence");
|
|
29
|
+
}
|
|
30
|
+
const allowances = evidence.allowances ?? [];
|
|
31
|
+
for (const allowance of allowances) {
|
|
32
|
+
const sourceMatch = typeof allowance.sourceEvidence === "string" ? /^(.*):([1-9]\d*)$/.exec(allowance.sourceEvidence) : null;
|
|
33
|
+
const knownRule = HARDENING_RULES[allowance.rule];
|
|
34
|
+
if (!knownRule || allowance.exact !== knownRule.exact || !sourceMatch || !/^[a-f0-9]{64}$/.test(allowance.artifactSha256 ?? "") || !/^[a-f0-9]{64}$/.test(allowance.sourceSha256 ?? "") || allowance.sourceLine !== Number(sourceMatch[2]) || typeof allowance.rationale !== "string" || allowance.rationale.trim().length < 12) {
|
|
35
|
+
throw new Error("invalid hardening allowance source binding");
|
|
36
|
+
}
|
|
37
|
+
const sourcePath = realpathSync(path.resolve(root, sourceMatch[1]));
|
|
38
|
+
const sourceRelative = path.relative(root, sourcePath);
|
|
39
|
+
if (sourceRelative === ".." || sourceRelative.startsWith("../") || sourceRelative.startsWith("..\\") || path.isAbsolute(sourceRelative)) {
|
|
40
|
+
throw new Error("hardening allowance source escapes release root");
|
|
41
|
+
}
|
|
42
|
+
const sourceBytes = readFileSync(sourcePath);
|
|
43
|
+
if (createHash("sha256").update(sourceBytes).digest("hex") !== allowance.sourceSha256) throw new Error("hardening allowance source digest mismatch");
|
|
44
|
+
const line = sourceBytes.toString("utf8").split(/\r?\n/)[allowance.sourceLine - 1];
|
|
45
|
+
if (typeof line !== "string" || !line.includes(allowance.exact)) throw new Error("hardening allowance source token mismatch");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const fileNameRules = [
|
|
49
|
+
[/\.map$/i, "source map shipped"],
|
|
50
|
+
[/\.pdb$/i, "Windows debug symbols shipped"],
|
|
51
|
+
[/\.dSYM(?:$|[/\\])/i, "macOS debug symbols shipped"],
|
|
52
|
+
[/\.mlpackage(?:$|[/\\])/i, "raw CoreML package shipped"],
|
|
53
|
+
[/[/\\]docs[/\\]experiments(?:$|[/\\])/i, "experiment docs shipped"],
|
|
54
|
+
[/[/\\]\.scratch(?:$|[/\\])/i, "scratch directory shipped"],
|
|
55
|
+
[/[/\\]\.git(?:$|[/\\])/i, "git metadata shipped"],
|
|
56
|
+
[/[/\\]node_modules(?:$|[/\\])/i, "node_modules shipped"],
|
|
57
|
+
[/[/\\](?:src|tests?|benches|benchmarks)(?:$|[/\\])/i, "source/test tree shipped"],
|
|
58
|
+
[/[/\\]tdt_w15_p6(?:$|[/\\])/i, "stale TDT p6 model shipped"],
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const contentRules = [
|
|
62
|
+
["local-source-path", /\/Users\/adrdsouza\/claude/gi, "local source path leaked"],
|
|
63
|
+
["local-cargo-path", /\/Users\/adrdsouza\/\.cargo/gi, "local cargo path leaked"],
|
|
64
|
+
["local-windows-path", /C:\\Users\\adrdsouza\\(?:claude|\.cargo)/gi, "local Windows source path leaked"],
|
|
65
|
+
["experiment-path", /docs\/experiments/gi, "experiment path leaked"],
|
|
66
|
+
["cleanup-prompt-marker", /l3_cleanup_prompt/gi, "L3 cleanup prompt marker leaked"],
|
|
67
|
+
["system-prompt-marker", /SYSTEM_PROMPT/gi, "system prompt marker leaked"],
|
|
68
|
+
["secret-env-name", /OPENAI_API_KEY|ANTHROPIC_API_KEY|APPLE_PASSWORD|NOTARY_PROFILE/gi, "secret/env key name leaked"],
|
|
69
|
+
["gpl-flag", /--enable-gpl|--enable-nonfree/gi, "GPL/nonfree ffmpeg flag leaked"],
|
|
70
|
+
["stale-model", /tdt_w15_p6/gi, "stale TDT p6 string leaked"],
|
|
71
|
+
["experiment-model", /Canary-Qwen|Nemotron|dqlstm|bias-experiments/gi, "experiment/model-history marker leaked"],
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const maxPatternLen = Math.max(...contentRules.map(([, rule]) => String(rule).length), 256);
|
|
75
|
+
|
|
76
|
+
function add(kind, file, message, detail = "") {
|
|
77
|
+
findings.push({ kind, file: path.relative(root, file) || file, message, detail });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function walk(target) {
|
|
81
|
+
const st = await lstat(target);
|
|
82
|
+
const normalized = target.split(path.sep).join("/");
|
|
83
|
+
for (const [rule, message] of fileNameRules) if (rule.test(normalized)) add("path", target, message);
|
|
84
|
+
if (st.isDirectory()) {
|
|
85
|
+
const entries = await readdir(target);
|
|
86
|
+
await Promise.all(entries.map((name) => walk(path.join(target, name))));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (st.isFile()) await scanFile(target);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function scanFile(file) {
|
|
93
|
+
return new Promise((resolveScan) => {
|
|
94
|
+
const artifactSha256 = createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
95
|
+
const stream = createReadStream(file, { highWaterMark: 256 * 1024 });
|
|
96
|
+
let tail = "";
|
|
97
|
+
stream.on("data", (chunk) => {
|
|
98
|
+
const text = tail + chunk.toString("latin1");
|
|
99
|
+
for (const [ruleId, rule, message] of contentRules) {
|
|
100
|
+
if (/ffmpeg-LICENSE\.txt$/i.test(file) && ruleId === "gpl-flag") continue;
|
|
101
|
+
rule.lastIndex = 0;
|
|
102
|
+
for (const match of text.matchAll(rule)) {
|
|
103
|
+
const allowed = allowances.some((allowance) =>
|
|
104
|
+
allowance.rule === ruleId &&
|
|
105
|
+
allowance.artifactSha256 === artifactSha256 &&
|
|
106
|
+
allowance.exact === match[0] &&
|
|
107
|
+
typeof allowance.sourceEvidence === "string" &&
|
|
108
|
+
typeof allowance.rationale === "string"
|
|
109
|
+
);
|
|
110
|
+
if (!allowed) add("content", file, message, rule.source);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
tail = text.slice(-maxPatternLen);
|
|
114
|
+
});
|
|
115
|
+
stream.on("error", (error) => {
|
|
116
|
+
add("read", file, "could not scan file", error.message);
|
|
117
|
+
resolveScan();
|
|
118
|
+
});
|
|
119
|
+
stream.on("end", resolveScan);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const arg of args) {
|
|
124
|
+
const target = path.resolve(arg);
|
|
125
|
+
try {
|
|
126
|
+
await walk(target);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
add("target", target, "could not scan target", error.message);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const unique = new Map();
|
|
133
|
+
for (const finding of findings) unique.set([finding.kind, finding.file, finding.message].join("\0"), finding);
|
|
134
|
+
const deduped = [...unique.values()].sort((left, right) =>
|
|
135
|
+
(left.file + " " + left.message).localeCompare(right.file + " " + right.message),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (deduped.length) {
|
|
139
|
+
console.error("hardeningscan: " + deduped.length + " finding(s)");
|
|
140
|
+
for (const finding of deduped) console.error("- [" + finding.kind + "] " + finding.file + ": " + finding.message);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log("hardeningscan: clean");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.82",
|
|
4
4
|
"description": "Portable Right Suite release CLI/SDK: signed native archives, direct bootstrap transactions, immutable GitHub Release upload, R2 bootstrap publication, and add-on adoption.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/release.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { access, readFile, readdir } from "node:fs/promises";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -378,6 +378,12 @@ async function runNativeFinalizer({ finalizer, nativeAssembly, target, root, sig
|
|
|
378
378
|
...finalizer,
|
|
379
379
|
args: finalizer.args.map((arg) => expandFinalizerToken(arg, { provenance, outputPath, packageIdentity })),
|
|
380
380
|
};
|
|
381
|
+
// A finalization receipt at outputPath from a PREVIOUS run must not
|
|
382
|
+
// survive into this one: the file is preferred over the finalizer's
|
|
383
|
+
// stdout below, and a stale receipt carries the prior run's provenance,
|
|
384
|
+
// which fails the identity check against this run's mint. Any file
|
|
385
|
+
// present after the finalizer returns was written by this run.
|
|
386
|
+
if (!opts.dryRun) rmSync(outputPath, { force: true });
|
|
381
387
|
const stdout = await runCommand(finalizerCommand, root, {
|
|
382
388
|
captureStdout: true,
|
|
383
389
|
env: {
|
package/rightkit-versions.json
CHANGED