@rightkit/release 0.2.143 → 0.2.144
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 +13 -2
- package/cli/right-release.mjs +3 -0
- package/github-release.mjs +9 -0
- package/ownership-approval.mjs +14 -1
- package/ownership-command.mjs +48 -2
- package/ownership-declaration.json +175 -4
- package/ownership-dependency-allowlist-seed.mjs +86 -0
- package/ownership-dependency-allowlist.json +1087 -0
- package/ownership-dependency-allowlist.mjs +124 -0
- package/ownership-evidence.mjs +13 -2
- package/ownership-key-command.mjs +55 -13
- package/ownership-key-elevation.mjs +114 -8
- package/ownership-key-trust.mjs +25 -4
- package/ownership-policy-bundle.mjs +155 -0
- package/ownership-report-command.mjs +1 -1
- package/ownership-trust-summary.mjs +119 -0
- package/ownership-windows-presence.mjs +171 -0
- package/ownership.mjs +42 -6
- package/package.json +1 -1
- package/protected-credentials.mjs +6 -1
- package/publish-update.mjs +8 -1
- package/release-qa-command.mjs +145 -0
- package/release-qa-floor.json +9 -0
- package/release-qa-receipt.mjs +276 -0
- package/release.mjs +1 -1
- package/rightkit-drift.mjs +1 -1
- package/rightkit-ownership.json +2276 -429
- package/rightkit-sync.mjs +3 -0
- package/rightkit-versions.json +4 -3
- package/upload-release.mjs +8 -1
package/build-release.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { spawn, spawnSync } from "./background-process.mjs";
|
|
|
24
24
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
25
25
|
import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
|
|
26
26
|
import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
|
|
27
|
+
import { assertSourceQaAdmission } from "./release-qa-receipt.mjs";
|
|
27
28
|
import { commandOutputPortable, releaseEnvironment, resolveReleaseLayout, runBuildStateMachine, verifySealedRelease, watchProgress } from "./release-state.mjs";
|
|
28
29
|
import { acquireCacheLease, acquireSuiteBuildSlot, applyCachePrune, assertWriteVolumeFloors, inspectCache, inspectWriteVolumes, markCacheEntrySuccessful, planCachePrune, readCachePolicy, resolveCacheLayout, resolveSharedCacheIdentity, resolveSharedCacheRoot } from "./cache-policy.mjs";
|
|
29
30
|
import { createTargetBridge } from "./target-bridge.mjs";
|
|
@@ -253,6 +254,15 @@ try {
|
|
|
253
254
|
for (const check of preflight) {
|
|
254
255
|
if (check.status !== "ok") console.error(`[right-release] preflight ${check.name}: ${check.detail}`);
|
|
255
256
|
}
|
|
257
|
+
// Hardening step 4: source QA admits the candidate build/sign. The receipt digest is sealed into the manifest so
|
|
258
|
+
// upload/publication can require installed QA chained to it. Dev lanes (install-app --dev) never reach this file.
|
|
259
|
+
let qaAdmission;
|
|
260
|
+
try {
|
|
261
|
+
qaAdmission = assertSourceQaAdmission({ repoRoot: layout.repoRoot, releaseId, platform, configName: path.relative(process.cwd(), configPath) || path.basename(configPath), releaseConfigSha256: hashFile(configPath) });
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (error?.code !== "QA_ADMISSION_REFUSED") throw error;
|
|
264
|
+
fail(error.message);
|
|
265
|
+
}
|
|
256
266
|
|
|
257
267
|
// A tool being INSTALLED is not the same as it being the one PATH picks. The
|
|
258
268
|
// vendored-OpenSSL build shells out to `perl`, and under Git Bash/MSYS that
|
|
@@ -396,7 +406,7 @@ try {
|
|
|
396
406
|
});
|
|
397
407
|
},
|
|
398
408
|
seal: async ({ sealedDir }) => {
|
|
399
|
-
sealRelease({ configRoot: appRoot, managedCargoTarget, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence, managedRoot });
|
|
409
|
+
sealRelease({ configRoot: appRoot, managedCargoTarget, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence, qaAdmission, managedRoot });
|
|
400
410
|
verifySealedRelease(sealedDir);
|
|
401
411
|
if (cacheMode === "shared") markCacheEntrySuccessful({ layout: sharedLayout });
|
|
402
412
|
checkpoint(stateRoot, "sealed");
|
|
@@ -436,7 +446,7 @@ function sqlCipherFeatures(text) {
|
|
|
436
446
|
.filter((value) => /sqlcipher|openssl/i.test(value));
|
|
437
447
|
}
|
|
438
448
|
|
|
439
|
-
function sealRelease({ configRoot, managedCargoTarget, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence, managedRoot }) {
|
|
449
|
+
function sealRelease({ configRoot, managedCargoTarget, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence, qaAdmission, managedRoot }) {
|
|
440
450
|
// Every app's right-release.config.mjs names installer/updater artifacts
|
|
441
451
|
// relative to its resolved native target. On a broker-managed host that
|
|
442
452
|
// directory is never populated (the broker owns CARGO_TARGET_DIR and
|
|
@@ -507,6 +517,7 @@ function sealRelease({ configRoot, managedCargoTarget, nativeLayout, sealedDir,
|
|
|
507
517
|
version: config.version,
|
|
508
518
|
commit,
|
|
509
519
|
ownershipEvidence,
|
|
520
|
+
qa: { sourceReceiptDigest: qaAdmission.digest, floorDigest: qaAdmission.floorDigest },
|
|
510
521
|
platform,
|
|
511
522
|
cacheKey,
|
|
512
523
|
// Portable contracts sign only raw executables — installer/embedding
|
package/cli/right-release.mjs
CHANGED
|
@@ -23,6 +23,7 @@ const commands = new Map([
|
|
|
23
23
|
["mirror-root-artifact", "mirror-root-artifact.mjs"],
|
|
24
24
|
["github", "github-release.mjs"],
|
|
25
25
|
["cargo-target", "cargo-target.mjs"],
|
|
26
|
+
["qa", "release-qa-command.mjs"],
|
|
26
27
|
["ownership", "ownership-command.mjs"],
|
|
27
28
|
["ownership-approval", "ownership-approval-command.mjs"],
|
|
28
29
|
["ownership-report", "ownership-report-command.mjs"],
|
|
@@ -183,6 +184,7 @@ Commands:
|
|
|
183
184
|
Stage SwiftPM app; build+sign MSIX (Windows) or assemble+sign+notarize .app (macOS); never uploads
|
|
184
185
|
upload --release <id> --platform mac|win --tier patch|update
|
|
185
186
|
Upload one sealed release; never builds
|
|
187
|
+
qa --stage source|installed --platform mac|win [--release <id>] Run rightkit-qa and write the sealed QA receipt (required to build/upload)
|
|
186
188
|
publish-update --release <id> --platform mac|win --tier patch|update
|
|
187
189
|
Upload sealed bytes to R2 & register RightApps
|
|
188
190
|
release Compatibility alias for build
|
|
@@ -212,6 +214,7 @@ Commands:
|
|
|
212
214
|
ownership approve-key <fingerprint|request.json> Approve one pending device key from an active host
|
|
213
215
|
ownership revoke-key <fingerprint> Revoke one active device key
|
|
214
216
|
ownership trust-sync Advance this host's protected trust to the committed key log
|
|
217
|
+
ownership trust-compare [--json] Print this host's protected-trust digest (Mac is the reference)
|
|
215
218
|
ownership-approval --draft <input.json> [--out <draft.json>]
|
|
216
219
|
Draft an exception for native owner approval
|
|
217
220
|
ownership-approval --verify <signed.json> --context <scan.json>
|
package/github-release.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { readVerifiedArtifact, assertArtifactUnchanged, digestBytes, verifyArtif
|
|
|
7
7
|
import { spawnSync } from "./background-process.mjs";
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
9
|
import { verifySealedRelease } from "./release-state.mjs";
|
|
10
|
+
import { assertInstalledQaAdmission } from "./release-qa-receipt.mjs";
|
|
10
11
|
import { addonManifestSha256, validateAddonConfig, validateAddonManifest } from "./addon-contract.mjs";
|
|
11
12
|
import { RIGHTRELEASE_MANIFEST_SIGNER, verifyManifestCatalogSignature } from "./sign-release-manifest.mjs";
|
|
12
13
|
|
|
@@ -394,6 +395,14 @@ if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(
|
|
|
394
395
|
plan = prepareGitHubAddonRelease({ repoRoot, config, configRoot: path.dirname(configPath), ...options });
|
|
395
396
|
} else {
|
|
396
397
|
options.repo ||= repositoryFromRemote(repoRoot);
|
|
398
|
+
// Hardening step 4: the direct GitHub lane is a publication path too; it needs installed QA like `upload` does.
|
|
399
|
+
if (!options.dryRun) {
|
|
400
|
+
const sealed = verifySealedRelease(path.join(repoRoot, ".right-release", "sealed", options.releaseId, options.platform === "win" ? "windows" : "mac"));
|
|
401
|
+
const configPath = path.resolve("right-release.config.mjs");
|
|
402
|
+
try {
|
|
403
|
+
assertInstalledQaAdmission({ repoRoot, manifest: sealed.manifest, platform: options.platform, configName: "right-release.config.mjs", releaseConfigSha256: existsSync(configPath) ? createHash("sha256").update(readFileSync(configPath)).digest("hex") : undefined });
|
|
404
|
+
} catch (error) { if (error?.code !== "QA_ADMISSION_REFUSED") throw error; console.error(`right-release github: ${error.message}`); process.exit(1); }
|
|
405
|
+
}
|
|
397
406
|
plan = prepareGitHubRelease({ repoRoot, ...options });
|
|
398
407
|
}
|
|
399
408
|
const result = publishGitHubRelease(plan, options);
|
package/ownership-approval.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawnSync, windowsPowerShellEnvironment } from "./background-process.mjs";
|
|
6
6
|
import { verifyKeyLog, verifyOwnershipSignature } from "./ownership-key-log.mjs";
|
|
7
|
+
import { validateAllowlist } from "./ownership-dependency-allowlist.mjs";
|
|
7
8
|
|
|
8
9
|
export const APPROVAL_SCHEMA = 2;
|
|
9
10
|
export const REQUIRED_HOSTS = Object.freeze(["windows", "macos"]);
|
|
@@ -171,6 +172,16 @@ export function validateTrustRegistry(trust) {
|
|
|
171
172
|
if (!Array.isArray(trust?.revokedKeyIds) || !Array.isArray(trust?.verifiedTrackingEvidence) || trust.verifiedTrackingEvidence.some((x) => !HEX.test(x))) problems.push("trust revocation or tracking evidence lists are malformed");
|
|
172
173
|
if (!/^\d+(?:\.\d+)*$/.test(trust?.minimumScannerVersion ?? "") || !/^\d+(?:\.\d+)*$/.test(trust?.minimumCatalogVersion ?? "")) problems.push("scanner/catalog floors are missing or malformed");
|
|
173
174
|
if (trust?.minimumCatalogDigest !== undefined && !HEX.test(trust.minimumCatalogDigest)) problems.push("minimum catalog digest is malformed");
|
|
175
|
+
// Policy bundle + allowlist are optional so trust written before they existed stays valid (treated as unpinned).
|
|
176
|
+
if (trust?.policyBundle !== undefined) {
|
|
177
|
+
const bundle = trust.policyBundle;
|
|
178
|
+
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle) || bundle.schema !== 1 || !HEX.test(bundle.digest ?? "") || !HEX.test(bundle.catalogDigest ?? "")
|
|
179
|
+
|| !/^\d+(?:\.\d+)*$/.test(bundle.scannerVersion ?? "") || !Array.isArray(bundle.files) || bundle.files.some((file) => typeof file?.path !== "string" || !HEX.test(file.sha256 ?? ""))) problems.push("protected policy bundle is malformed");
|
|
180
|
+
}
|
|
181
|
+
if (trust?.dependencyAllowlist !== undefined) {
|
|
182
|
+
const allowlist = validateAllowlist(trust.dependencyAllowlist);
|
|
183
|
+
if (!allowlist.ok) problems.push(`protected dependency allowlist is malformed: ${allowlist.problems[0]}`);
|
|
184
|
+
}
|
|
174
185
|
if (trust?.rootFingerprint !== undefined || trust?.keyCheckpoint !== undefined) {
|
|
175
186
|
if (!HEX.test(trust.rootFingerprint ?? "") || !trust.keyCheckpoint || !Number.isSafeInteger(trust.keyCheckpoint.sequence) || trust.keyCheckpoint.sequence < 0 || !HEX.test(trust.keyCheckpoint.head ?? "")) problems.push("protected key-log root pin or checkpoint is malformed");
|
|
176
187
|
}
|
|
@@ -274,12 +285,14 @@ function versionAtLeast(actual, minimum) {
|
|
|
274
285
|
}
|
|
275
286
|
return true;
|
|
276
287
|
}
|
|
277
|
-
export function validateOwnershipFloor(trust, { scannerVersion, catalogVersion, catalogDigest } = {}) {
|
|
288
|
+
export function validateOwnershipFloor(trust, { scannerVersion, catalogVersion, catalogDigest, policyBundleDigest } = {}) {
|
|
278
289
|
const problems = [];
|
|
279
290
|
if (!trust) return { ok: false, problems: ["external ownership trust unavailable"] };
|
|
280
291
|
if (!versionAtLeast(scannerVersion, trust.minimumScannerVersion)) problems.push("scanner version missing, malformed, or below protected minimum");
|
|
281
292
|
if (!versionAtLeast(catalogVersion, trust.minimumCatalogVersion)) problems.push("catalog version missing, malformed, or below protected minimum");
|
|
282
293
|
if (trust.minimumCatalogDigest && trust.minimumCatalogDigest !== catalogDigest) problems.push("catalog digest below protected pinned digest");
|
|
294
|
+
// Trust written before the bundle existed has no policyBundle and stays unpinned until the next trust-sync.
|
|
295
|
+
if (trust.policyBundle?.digest && policyBundleDigest !== undefined && trust.policyBundle.digest !== policyBundleDigest) problems.push("ownership policy bundle (scanner source, catalog, registry or dependency allowlist) differs from the protected pinned bundle; run trust-sync");
|
|
283
296
|
return { ok: problems.length === 0, problems };
|
|
284
297
|
}
|
|
285
298
|
function protectedLocation(file, platform, { runner = spawnSync, inspect = lstatSync, stat = statSync, requireNonElevated = true } = {}) {
|
package/ownership-command.mjs
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
// the shipped manifest from each package's declaration.
|
|
4
4
|
//
|
|
5
5
|
// right-release ownership [--app <dir>] [--json]
|
|
6
|
-
// right-release ownership
|
|
6
|
+
// right-release ownership [--app <dir>] --audit-json (Legion governance.capability-ownership provider)
|
|
7
|
+
// right-release ownership trust-compare [--json] (print this host's protected-trust digest)
|
|
8
|
+
// right-release ownership --write-manifest [--rightkit <rightkit>] [--approve-new-capabilities]
|
|
7
9
|
// --approve-new-capabilities permits new ids during local generation only.
|
|
8
10
|
// Existing owner changes are always refused; installation needs protected authorization.
|
|
9
11
|
import { realpathSync } from "node:fs";
|
|
@@ -14,6 +16,8 @@ import { writeOwnershipCatalog } from "./ownership-catalog.mjs";
|
|
|
14
16
|
import { createOwnershipEvidence } from "./ownership-evidence.mjs";
|
|
15
17
|
import { spawnSync } from "./background-process.mjs";
|
|
16
18
|
import { main as stagedMain } from "./ownership-staged.mjs";
|
|
19
|
+
import { loadTrustedExternal } from "./ownership-approval.mjs";
|
|
20
|
+
import { trustComparison } from "./ownership-policy-bundle.mjs";
|
|
17
21
|
|
|
18
22
|
const packageRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
19
23
|
|
|
@@ -22,11 +26,23 @@ function flag(args, name) {
|
|
|
22
26
|
return i >= 0 ? args[i + 1] : undefined;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
/** Print this host's protected-trust digest. The Mac's digest is the reference; see docs/OWNERSHIP-ENFORCEMENT.md. */
|
|
30
|
+
export function trustCompare(args, { print, trustLoader = loadTrustedExternal } = {}) {
|
|
31
|
+
let state;
|
|
32
|
+
try { state = trustLoader(); } catch (error) { print(`[right-release] trust-compare: protected trust unreadable (${error.message})`); return 1; }
|
|
33
|
+
if (!state?.trust) { print("[right-release] trust-compare: no protected trust on this host (not enrolled)"); return 1; }
|
|
34
|
+
const { digest, components } = trustComparison(state.trust);
|
|
35
|
+
if (args.includes("--json")) print(JSON.stringify({ platform: process.platform, digest, components }, null, 2));
|
|
36
|
+
else { print(`trust digest (${process.platform}): ${digest}`); for (const [name, value] of Object.entries(components)) print(` ${name}: ${typeof value === "object" && value !== null ? JSON.stringify(value) : value ?? "none"}`); }
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function main(args, { evidenceFactory = createOwnershipEvidence, gitRunner = spawnSync, print = (value) => console.log(value), trustLoader = loadTrustedExternal } = {}) {
|
|
26
41
|
if (args.includes("--help")) {
|
|
27
42
|
print("right-release ownership [--app <dir>] [--json]\nright-release ownership --staged [--app <dir>] [--json] (pre-commit gate: scans the index, not the working tree)\nright-release ownership --write-manifest [--rightkit <dir>] [--approve-new-capabilities]\n--approve-new-capabilities permits new capability ids during local generation only. Existing owner conflicts are always refused. Protected installation authorization remains separate.");
|
|
28
43
|
return 0;
|
|
29
44
|
}
|
|
45
|
+
if (args[0] === "trust-compare") return trustCompare(args, { print, trustLoader });
|
|
30
46
|
if (args.includes("--staged")) return stagedMain(args, { print });
|
|
31
47
|
if (["enroll", "approve-key", "revoke-key", "trust-sync"].includes(args[0])) {
|
|
32
48
|
const { main: keyMain } = await import("./ownership-key-command.mjs");
|
|
@@ -48,6 +64,7 @@ export async function main(args, { evidenceFactory = createOwnershipEvidence, gi
|
|
|
48
64
|
try { checked = evidenceFactory(repoRoot, app); }
|
|
49
65
|
catch (error) { return failClosed(args, "protected-evidence", error.message, print); }
|
|
50
66
|
const result = checked.result;
|
|
67
|
+
if (args.includes("--audit-json")) return printAuditJson(result, print);
|
|
51
68
|
if (args.includes("--json")) {
|
|
52
69
|
const error = checked.enrollmentPending ? undefined : !checked.floor.ok
|
|
53
70
|
? { type: "ownership-floor", message: checked.floor.problems.join("; ") }
|
|
@@ -70,7 +87,36 @@ export async function main(args, { evidenceFactory = createOwnershipEvidence, gi
|
|
|
70
87
|
return checked.ok ? 0 : 1;
|
|
71
88
|
}
|
|
72
89
|
|
|
90
|
+
// Audit provider output: every scan violation (block and advise tiers) as a repo-relative finding.
|
|
91
|
+
// Exit 0 whenever the scan completes; findings never change the exit code.
|
|
92
|
+
export function auditFindings(result) {
|
|
93
|
+
return {
|
|
94
|
+
schemaVersion: 1,
|
|
95
|
+
findings: result.violations.map((v) => {
|
|
96
|
+
const tier = v.tier ?? "block";
|
|
97
|
+
const file = String(v.file ?? "").replaceAll("\\", "/");
|
|
98
|
+
if (!file || file.startsWith("/") || /^[A-Za-z]:/.test(file) || file === ".." || file.startsWith("../") || file.split("/").includes("..")) throw new Error(`audit finding path is outside the audited root: ${v.file}`);
|
|
99
|
+
const body = v.text ?? `${v.ecosystem ?? "dependency"} ${v.name ?? ""}`.trim();
|
|
100
|
+
const finding = { rule: String(v.fingerprint), path: file };
|
|
101
|
+
if (Number.isSafeInteger(v.line) && v.line > 0) finding.line = v.line;
|
|
102
|
+
finding.message = `${tier === "advise" ? "advise: " : ""}${v.capability} duplicates RightKit owner ${v.package} (${tier}): ${body}`;
|
|
103
|
+
return finding;
|
|
104
|
+
}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function printAuditJson(result, print) {
|
|
109
|
+
try {
|
|
110
|
+
print(JSON.stringify(auditFindings(result)));
|
|
111
|
+
return 0;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(`[right-release] ownership audit failed: ${error.message}`);
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
73
118
|
function failClosed(args, type, message, print = (value) => console.log(value)) {
|
|
119
|
+
if (args.includes("--audit-json")) { console.error(`[right-release] ownership audit failed: ${message}`); return 1; }
|
|
74
120
|
if (args.includes("--json")) print(JSON.stringify({ ok: false, error: { type, message }, rows: [], violations: [], covered: [], problems: [message], coverage: [], metadata: null }, null, 2));
|
|
75
121
|
else print(`[right-release] ownership FAILED: ${message}`);
|
|
76
122
|
return 1;
|
|
@@ -3,12 +3,183 @@
|
|
|
3
3
|
"capabilities": [
|
|
4
4
|
{
|
|
5
5
|
"capability": "release-tooling",
|
|
6
|
-
"summary": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution
|
|
7
|
-
"fingerprints": [
|
|
8
|
-
|
|
6
|
+
"summary": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.",
|
|
7
|
+
"fingerprints": [
|
|
8
|
+
{
|
|
9
|
+
"id": "release.sign.codesign-signtool",
|
|
10
|
+
"type": "source",
|
|
11
|
+
"pattern": "\\b(?:Command\\s*::\\s*new|spawn\\w*|execFile\\w*|exec\\w*)\\s*\\(\\s*(?:[\"`]\\s*(?:codesign\\b[^\"`';]{0,160}--sign|signtool(?:\\.exe)?\\s+sign\\b)|[\"'](?:codesign|signtool(?:\\.exe)?)[\"'][^;]{0,300}?[\"'](?:--sign|sign)[\"'])",
|
|
12
|
+
"codeOnly": true,
|
|
13
|
+
"extensions": [
|
|
14
|
+
".ts",
|
|
15
|
+
".tsx",
|
|
16
|
+
".js",
|
|
17
|
+
".mjs",
|
|
18
|
+
".cjs",
|
|
19
|
+
".jsx",
|
|
20
|
+
".rs",
|
|
21
|
+
".py"
|
|
22
|
+
],
|
|
23
|
+
"tier": "advise",
|
|
24
|
+
"usageContext": {
|
|
25
|
+
"hosts": [
|
|
26
|
+
"macos",
|
|
27
|
+
"windows"
|
|
28
|
+
],
|
|
29
|
+
"scope": "Executable code only; authenticated owner provenance required for exclusions"
|
|
30
|
+
},
|
|
31
|
+
"tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "release.notarize.notarytool",
|
|
35
|
+
"type": "source",
|
|
36
|
+
"pattern": "\\b(?:Command\\s*::\\s*new|spawn\\w*|execFile\\w*|exec\\w*)\\s*\\([^;]{0,200}?[\"'`\\s]notarytool\\b",
|
|
37
|
+
"codeOnly": true,
|
|
38
|
+
"extensions": [
|
|
39
|
+
".ts",
|
|
40
|
+
".tsx",
|
|
41
|
+
".js",
|
|
42
|
+
".mjs",
|
|
43
|
+
".cjs",
|
|
44
|
+
".jsx",
|
|
45
|
+
".rs",
|
|
46
|
+
".py"
|
|
47
|
+
],
|
|
48
|
+
"tier": "advise",
|
|
49
|
+
"usageContext": {
|
|
50
|
+
"hosts": [
|
|
51
|
+
"macos",
|
|
52
|
+
"windows"
|
|
53
|
+
],
|
|
54
|
+
"scope": "Executable code only; authenticated owner provenance required for exclusions"
|
|
55
|
+
},
|
|
56
|
+
"tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "release.npm.electron-builder",
|
|
60
|
+
"type": "dependency",
|
|
61
|
+
"ecosystem": "npm",
|
|
62
|
+
"name": "electron-builder",
|
|
63
|
+
"tier": "advise",
|
|
64
|
+
"usageContext": {
|
|
65
|
+
"hosts": [
|
|
66
|
+
"macos",
|
|
67
|
+
"windows"
|
|
68
|
+
],
|
|
69
|
+
"scope": "npm direct competing dependency; transitive findings require authenticated reachability"
|
|
70
|
+
},
|
|
71
|
+
"tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"id": "release.npm.electron-notarize",
|
|
75
|
+
"type": "dependency",
|
|
76
|
+
"ecosystem": "npm",
|
|
77
|
+
"name": "electron-notarize",
|
|
78
|
+
"tier": "advise",
|
|
79
|
+
"usageContext": {
|
|
80
|
+
"hosts": [
|
|
81
|
+
"macos",
|
|
82
|
+
"windows"
|
|
83
|
+
],
|
|
84
|
+
"scope": "npm direct competing dependency; transitive findings require authenticated reachability"
|
|
85
|
+
},
|
|
86
|
+
"tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "release.npm.electron-notarize-scoped",
|
|
90
|
+
"type": "dependency",
|
|
91
|
+
"ecosystem": "npm",
|
|
92
|
+
"name": "@electron/notarize",
|
|
93
|
+
"tier": "advise",
|
|
94
|
+
"usageContext": {
|
|
95
|
+
"hosts": [
|
|
96
|
+
"macos",
|
|
97
|
+
"windows"
|
|
98
|
+
],
|
|
99
|
+
"scope": "npm direct competing dependency; transitive findings require authenticated reachability"
|
|
100
|
+
},
|
|
101
|
+
"tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"id": "release.npm.electron-osx-sign",
|
|
105
|
+
"type": "dependency",
|
|
106
|
+
"ecosystem": "npm",
|
|
107
|
+
"name": "electron-osx-sign",
|
|
108
|
+
"tier": "advise",
|
|
109
|
+
"usageContext": {
|
|
110
|
+
"hosts": [
|
|
111
|
+
"macos",
|
|
112
|
+
"windows"
|
|
113
|
+
],
|
|
114
|
+
"scope": "npm direct competing dependency; transitive findings require authenticated reachability"
|
|
115
|
+
},
|
|
116
|
+
"tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"id": "release.npm.electron-osx-sign-scoped",
|
|
120
|
+
"type": "dependency",
|
|
121
|
+
"ecosystem": "npm",
|
|
122
|
+
"name": "@electron/osx-sign",
|
|
123
|
+
"tier": "advise",
|
|
124
|
+
"usageContext": {
|
|
125
|
+
"hosts": [
|
|
126
|
+
"macos",
|
|
127
|
+
"windows"
|
|
128
|
+
],
|
|
129
|
+
"scope": "npm direct competing dependency; transitive findings require authenticated reachability"
|
|
130
|
+
},
|
|
131
|
+
"tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"id": "release.npm.create-dmg",
|
|
135
|
+
"type": "dependency",
|
|
136
|
+
"ecosystem": "npm",
|
|
137
|
+
"name": "create-dmg",
|
|
138
|
+
"tier": "advise",
|
|
139
|
+
"usageContext": {
|
|
140
|
+
"hosts": [
|
|
141
|
+
"macos",
|
|
142
|
+
"windows"
|
|
143
|
+
],
|
|
144
|
+
"scope": "npm direct competing dependency; transitive findings require authenticated reachability"
|
|
145
|
+
},
|
|
146
|
+
"tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"id": "release.npm.appdmg",
|
|
150
|
+
"type": "dependency",
|
|
151
|
+
"ecosystem": "npm",
|
|
152
|
+
"name": "appdmg",
|
|
153
|
+
"tier": "advise",
|
|
154
|
+
"usageContext": {
|
|
155
|
+
"hosts": [
|
|
156
|
+
"macos",
|
|
157
|
+
"windows"
|
|
158
|
+
],
|
|
159
|
+
"scope": "npm direct competing dependency; transitive findings require authenticated reachability"
|
|
160
|
+
},
|
|
161
|
+
"tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"id": "release.cargo.apple-codesign",
|
|
165
|
+
"type": "dependency",
|
|
166
|
+
"ecosystem": "cargo",
|
|
167
|
+
"name": "apple-codesign",
|
|
168
|
+
"tier": "advise",
|
|
169
|
+
"usageContext": {
|
|
170
|
+
"hosts": [
|
|
171
|
+
"macos",
|
|
172
|
+
"windows"
|
|
173
|
+
],
|
|
174
|
+
"scope": "cargo direct competing dependency; transitive findings require authenticated reachability"
|
|
175
|
+
},
|
|
176
|
+
"tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
|
|
177
|
+
}
|
|
178
|
+
],
|
|
179
|
+
"detection": "fingerprinted",
|
|
9
180
|
"policyNotes": [
|
|
10
181
|
"Package contract retained; dependency declarations do not verify integration.",
|
|
11
|
-
"
|
|
182
|
+
"Fingerprints identify review candidates (competing dependencies and distinctive implementation code); published two-host replacement qualification is not recorded."
|
|
12
183
|
]
|
|
13
184
|
}
|
|
14
185
|
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Propose or regenerate dependency-allowlist baselines from each registered repository's CURRENT direct
|
|
3
|
+
// dependencies. This only edits the committed proposal file; it takes effect solely through Adrian's trust-sync.
|
|
4
|
+
// node ownership-dependency-allowlist-seed.mjs --workspace <dir> [--write]
|
|
5
|
+
// Existing approvals are never removed (union with the current file); block-tier catalog dependencies
|
|
6
|
+
// and RightKit packages are never recorded as approvals.
|
|
7
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { spawnSync } from "./background-process.mjs";
|
|
11
|
+
import { canonicalRepositoryIdentity } from "./ownership-approval.mjs";
|
|
12
|
+
import { directDependencies } from "./ownership.mjs";
|
|
13
|
+
import { ALLOWLIST_FILE, entryKey, isRightKitPackage, sourceKind, validateAllowlist } from "./ownership-dependency-allowlist.mjs";
|
|
14
|
+
|
|
15
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const readJson = (name) => JSON.parse(readFileSync(path.join(here, name), "utf8"));
|
|
17
|
+
|
|
18
|
+
// Universally approved tooling: tiny on purpose. Everything else is per-repository.
|
|
19
|
+
export const SEED_GLOBAL = Object.freeze(["npm:@types/node", "npm:typescript", "npm:vitest"]);
|
|
20
|
+
// A renamed repository that has no checkout of its own reuses the baseline of its current name.
|
|
21
|
+
export const SEED_ALIASES = Object.freeze({ "https://github.com/orthic-labs/cockpit": "https://github.com/orthic-labs/pulse" });
|
|
22
|
+
|
|
23
|
+
function localCheckouts(workspace) {
|
|
24
|
+
const found = new Map();
|
|
25
|
+
for (const name of readdirSync(workspace)) {
|
|
26
|
+
const dir = path.join(workspace, name);
|
|
27
|
+
if (!existsSync(path.join(dir, ".git"))) continue;
|
|
28
|
+
const origin = spawnSync("git", ["-C", dir, "config", "--get", "remote.origin.url"], { encoding: "utf8", windowsHide: true });
|
|
29
|
+
if (origin.status !== 0) continue;
|
|
30
|
+
try { found.set(canonicalRepositoryIdentity(origin.stdout.trim()), dir); } catch { /* not a canonical remote */ }
|
|
31
|
+
}
|
|
32
|
+
return found;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function seedRepository(root, manifest) {
|
|
36
|
+
const blocked = new Set(manifest.capabilities.flatMap((c) => c.fingerprints.filter((f) => f.type === "dependency" && (f.tier ?? c.tier ?? "block") === "block").map((f) => `${f.ecosystem}:${f.name}`)));
|
|
37
|
+
const owners = manifest.capabilities.map((c) => c.package);
|
|
38
|
+
const entries = new Set(); let skippedBlock = 0, skippedFirstParty = 0, skippedRightKit = 0;
|
|
39
|
+
const deps = directDependencies(root);
|
|
40
|
+
for (const d of deps) {
|
|
41
|
+
const kind = sourceKind(d.source);
|
|
42
|
+
if (kind === "path") { skippedFirstParty += 1; continue; }
|
|
43
|
+
if (isRightKitPackage(d.name, owners)) { skippedRightKit += 1; continue; }
|
|
44
|
+
if (blocked.has(`${d.ecosystem}:${d.name}`)) { skippedBlock += 1; continue; }
|
|
45
|
+
entries.add(entryKey(d.ecosystem, d.name, kind));
|
|
46
|
+
}
|
|
47
|
+
return { entries: [...entries].sort(), direct: deps.length, skippedBlock, skippedFirstParty, skippedRightKit, unscanned: deps.coverage?.length ?? 0 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildSeed({ workspace, registry = readJson("ownership-repositories.json"), manifest = readJson("rightkit-ownership.json"), existing = null, log = () => {} } = {}) {
|
|
51
|
+
const checkouts = localCheckouts(workspace);
|
|
52
|
+
const previous = new Map((existing?.repositories ?? []).map((entry) => [entry.repositoryIdentity, entry]));
|
|
53
|
+
const seeded = new Map(), stats = [];
|
|
54
|
+
for (const { repositoryIdentity } of registry.repositories) {
|
|
55
|
+
const root = checkouts.get(repositoryIdentity);
|
|
56
|
+
if (!root) continue;
|
|
57
|
+
const result = seedRepository(root, manifest);
|
|
58
|
+
seeded.set(repositoryIdentity, result.entries);
|
|
59
|
+
stats.push({ repositoryIdentity, root, ...result });
|
|
60
|
+
}
|
|
61
|
+
const repositories = registry.repositories.map(({ repositoryIdentity }) => {
|
|
62
|
+
const prior = previous.get(repositoryIdentity)?.dependencies ?? [];
|
|
63
|
+
let note;
|
|
64
|
+
let fresh = seeded.get(repositoryIdentity);
|
|
65
|
+
if (!fresh && SEED_ALIASES[repositoryIdentity] && seeded.has(SEED_ALIASES[repositoryIdentity])) { fresh = seeded.get(SEED_ALIASES[repositoryIdentity]); note = `seeded from ${SEED_ALIASES[repositoryIdentity]} (same repository, renamed)`; stats.push({ repositoryIdentity, alias: SEED_ALIASES[repositoryIdentity], entries: fresh }); }
|
|
66
|
+
if (!fresh) { fresh = []; note = "no local checkout when seeded; baseline is empty until proposed"; stats.push({ repositoryIdentity, unseeded: true, entries: [] }); }
|
|
67
|
+
return { repositoryIdentity, ...(note ? { note } : {}), dependencies: [...new Set([...prior, ...fresh])].sort() };
|
|
68
|
+
});
|
|
69
|
+
const allowlist = { schema: 1, global: [...new Set([...(existing?.global ?? []), ...SEED_GLOBAL])].sort(), repositories };
|
|
70
|
+
const check = validateAllowlist(allowlist);
|
|
71
|
+
if (!check.ok) throw new Error(`generated allowlist is invalid: ${check.problems.join("; ")}`);
|
|
72
|
+
log(stats);
|
|
73
|
+
return { allowlist, stats };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
|
|
77
|
+
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
|
|
78
|
+
const args = process.argv.slice(2);
|
|
79
|
+
const flag = (name) => { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; };
|
|
80
|
+
const workspace = path.resolve(flag("--workspace") ?? path.resolve(here, "../../.."));
|
|
81
|
+
const file = path.join(here, ALLOWLIST_FILE);
|
|
82
|
+
const existing = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : null;
|
|
83
|
+
const { allowlist, stats } = buildSeed({ workspace, existing });
|
|
84
|
+
for (const s of stats) process.stdout.write(`${s.repositoryIdentity}: ${s.entries.length} entries${s.direct !== undefined ? ` (direct ${s.direct}, first-party ${s.skippedFirstParty}, rightkit ${s.skippedRightKit}, block-tier ${s.skippedBlock}, unscanned ${s.unscanned})` : s.alias ? ` (alias of ${s.alias})` : " (UNSEEDED)"}\n`);
|
|
85
|
+
if (args.includes("--write")) { writeFileSync(file, `${JSON.stringify(allowlist, null, 2)}\n`); process.stdout.write(`wrote ${file}\n`); }
|
|
86
|
+
}
|