@rightkit/release 0.2.135 → 0.2.136
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/README.md +62 -0
- package/addon-command.mjs +53 -34
- package/apple-profile-renew.mjs +30 -12
- package/artifact-storage.mjs +47 -10
- package/background-process.mjs +18 -0
- package/build-release.mjs +1 -1
- package/cache-command.mjs +4 -3
- package/cargo-contract.mjs +1 -1
- package/cargo-guard.mjs +4 -3
- package/cargo-registry-readback.mjs +51 -0
- package/cargo-target.mjs +3 -1
- package/cli/right-release.mjs +26 -0
- package/codesign-environment.mjs +26 -2
- package/codesign-shim.mjs +2 -1
- package/committed-source.mjs +3 -3
- package/development.mjs +7 -7
- package/direct-bootstrap.mjs +19 -9
- package/dmg-build.mjs +117 -0
- package/dmg-layout.mjs +123 -0
- package/github-release.mjs +74 -37
- package/heavy-command.mjs +3 -2
- package/install-app.mjs +358 -0
- package/inventory-check.mjs +980 -0
- package/macos-signing-session.mjs +3 -0
- package/managed-cargo.mjs +23 -19
- package/managed-host-secrets.mjs +30 -0
- package/managed-roots.mjs +39 -0
- package/model-promote.mjs +31 -37
- package/notary-auth.mjs +4 -0
- package/osascript-guard.mjs +24 -0
- package/ownership-approval-command.mjs +15 -4
- package/ownership-approval.mjs +114 -49
- package/ownership-availability.mjs +25 -0
- package/ownership-catalog-config.json +18 -0
- package/ownership-catalog.mjs +94 -0
- package/ownership-command.mjs +17 -10
- package/ownership-declaration.json +15 -0
- package/ownership-default-policy.json +1 -11
- package/ownership-evidence.mjs +10 -8
- package/ownership-git-inventory.mjs +29 -1
- package/ownership-helper-paths.json +2 -2
- package/ownership-hooks.mjs +107 -0
- package/ownership-internal-owners.json +44 -0
- package/ownership-key-authority.mjs +112 -7
- package/ownership-key-command.mjs +76 -19
- package/ownership-key-trust.mjs +97 -9
- package/ownership-lockfiles.mjs +311 -0
- package/ownership-report-command.mjs +12 -3
- package/ownership-report.mjs +20 -17
- package/ownership-repositories.json +19 -0
- package/ownership-scan-worker.mjs +96 -11
- package/ownership-source.mjs +39 -0
- package/ownership-staged.mjs +193 -0
- package/ownership-verdict.mjs +74 -0
- package/ownership.mjs +215 -107
- package/package.json +1 -1
- package/protected-credentials.mjs +111 -0
- package/prune-r2.mjs +4 -3
- package/pub-upload.mjs +3 -85
- package/publication-artifacts.mjs +108 -0
- package/publication-gate.mjs +28 -7
- package/publish-cargo.mjs +47 -16
- package/publish-swift.mjs +46 -17
- package/publish-update.mjs +61 -39
- package/r2-verified-upload.mjs +41 -0
- package/registry-parity.mjs +5 -3
- package/release-state.mjs +1 -0
- package/release-token.mjs +2 -0
- package/rightkit-drift.mjs +3 -2
- package/rightkit-ownership.json +2028 -139
- package/rightkit-signing-agent.mjs +5 -3
- package/rightkit-sync.mjs +6 -2
- package/rightkit-versions.json +213 -103
- package/runtime-artifact-manifest.mjs +20 -6
- package/sign-updater.mjs +5 -0
- package/signing-agent-client.mjs +2 -1
- package/signing-agent-install.mjs +3 -2
- package/standalone-clone-verify.mjs +3 -2
- package/tauri-bundle-marker.mjs +3 -2
- package/upload-large.mjs +36 -82
- package/verifier.mjs +5 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"repositories": [
|
|
4
|
+
{ "repositoryIdentity": "https://github.com/bogusyogi/coderight", "roots": ["."], "visibility": "private" },
|
|
5
|
+
{ "repositoryIdentity": "https://github.com/bogusyogi/cutright", "roots": ["."], "visibility": "private" },
|
|
6
|
+
{ "repositoryIdentity": "https://github.com/bogusyogi/genright", "roots": ["."], "visibility": "private" },
|
|
7
|
+
{ "repositoryIdentity": "https://github.com/bogusyogi/heardright", "roots": ["."], "visibility": "private" },
|
|
8
|
+
{ "repositoryIdentity": "https://github.com/bogusyogi/mailright", "roots": ["."], "visibility": "private" },
|
|
9
|
+
{ "repositoryIdentity": "https://github.com/bogusyogi/rightsites", "roots": ["."], "visibility": "private" },
|
|
10
|
+
{ "repositoryIdentity": "https://github.com/bogusyogi/scraperight", "roots": ["."], "visibility": "private" },
|
|
11
|
+
{ "repositoryIdentity": "https://github.com/bogusyogi/viewright", "roots": ["."], "visibility": "private" },
|
|
12
|
+
{ "repositoryIdentity": "https://github.com/bogusyogi/voiceright", "roots": ["."], "visibility": "private" },
|
|
13
|
+
{ "repositoryIdentity": "https://github.com/bogusyogi/postright", "roots": ["."], "visibility": "private" },
|
|
14
|
+
{ "repositoryIdentity": "https://github.com/orthic-labs/cockpit", "roots": ["."], "visibility": "public" },
|
|
15
|
+
{ "repositoryIdentity": "https://github.com/orthic-labs/legion", "roots": ["."], "visibility": "public" },
|
|
16
|
+
{ "repositoryIdentity": "https://github.com/bogusyogi/workright", "roots": ["."], "visibility": "private" },
|
|
17
|
+
{ "repositoryIdentity": "https://github.com/orthic-labs/sellright", "roots": ["."], "visibility": "public" }
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -1,15 +1,100 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
import { spawnSync } from "./background-process.mjs";
|
|
7
|
+
import { catalogDigest, contentInventory, MANIFEST_FILE, SCANNER_VERSION } from "./ownership.mjs";
|
|
8
|
+
import { createOwnershipEvidence, createOwnershipEvidenceForTest } from "./ownership-evidence.mjs";
|
|
9
|
+
import { canonicalRepositoryIdentity, loadTrustedExternal, sha256 } from "./ownership-approval.mjs";
|
|
10
|
+
import { readCommittedKeyAuthority } from "./ownership-key-authority.mjs";
|
|
11
|
+
import { ownershipCacheKey, ownershipVerdict } from "./ownership-verdict.mjs";
|
|
12
|
+
|
|
13
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const catalogPath = path.join(here, MANIFEST_FILE);
|
|
15
|
+
const MODULE_SPECIFIERS = /^\s*(?:import|export)\s[^'";]*?from\s*['"]([^'"]+)['"]|^\s*import\s*['"]([^'"]+)['"]/gm;
|
|
16
|
+
function scannerPolicyDigest() {
|
|
17
|
+
const files = new Map(), queue = [fileURLToPath(import.meta.url)];
|
|
18
|
+
while (queue.length) {
|
|
19
|
+
const file = queue.shift(); if (files.has(file)) continue;
|
|
20
|
+
const bytes = readFileSync(file); files.set(file, sha256(bytes));
|
|
21
|
+
for (const match of bytes.toString("utf8").matchAll(MODULE_SPECIFIERS)) {
|
|
22
|
+
const specifier = match[1] ?? match[2];
|
|
23
|
+
if (specifier.startsWith("node:")) continue;
|
|
24
|
+
queue.push(specifier.startsWith(".") ? createRequire(file).resolve(specifier) : fileURLToPath(import.meta.resolve(specifier)));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return sha256({ version: SCANNER_VERSION, files: [...files].map(([file, digest]) => ({ path: path.relative(here, file).replaceAll("\\", "/"), digest })).sort((a, b) => a.path.localeCompare(b.path)) });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function scanInputs(appRoot, { manifestPath = catalogPath, trustLoader = loadTrustedExternal, inventory = contentInventory, gitRunner = spawnSync, policyDigest = scannerPolicyDigest } = {}) {
|
|
31
|
+
const root = path.resolve(appRoot), manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
32
|
+
const state = trustLoader();
|
|
33
|
+
if (state.protection?.ok !== true && !(state.pending === true && state.enrolled === false && state.trust === null && state.protection?.reason === "external trust file is absent"))
|
|
34
|
+
throw Object.assign(new Error("protected ownership trust location was not verified"), { code: "RK_OWNERSHIP_INVALID_INPUTS" });
|
|
35
|
+
let trust = state.trust, keyLogDigest = null;
|
|
36
|
+
if (trust?.keyAuthority) {
|
|
37
|
+
const resolved = readCommittedKeyAuthority(trust);
|
|
38
|
+
if (!resolved.ok) throw Object.assign(new Error(resolved.problems.join("; ")), { code: "RK_OWNERSHIP_INVALID_INPUTS" });
|
|
39
|
+
trust = resolved.resolvedTrust; keyLogDigest = resolved.digest;
|
|
40
|
+
}
|
|
41
|
+
const origin = gitRunner("git", ["-C", root, "remote", "get-url", "origin"], { encoding: "utf8", windowsHide: true, timeout: 2500 });
|
|
42
|
+
let repositoryIdentity = null;
|
|
43
|
+
try { if (origin?.status === 0) repositoryIdentity = canonicalRepositoryIdentity(String(origin.stdout).trim()); } catch {}
|
|
44
|
+
const source = inventory(root);
|
|
45
|
+
if (typeof source.complete !== "boolean" || !Array.isArray(source.coverage) || source.complete !== (source.coverage.length === 0))
|
|
46
|
+
throw Object.assign(new Error("ownership content inventory completeness is missing or malformed"), { code: "RK_OWNERSHIP_INVALID_INPUTS" });
|
|
47
|
+
const roots = trust?.repositories?.find((entry) => entry.repositoryIdentity === repositoryIdentity)?.roots ?? ["."];
|
|
48
|
+
const approvals = roots.map((registered) => {
|
|
49
|
+
const file = path.join(root, ...(registered === "." ? [] : registered.split("/")), ".rightkit", "ownership-exceptions.json");
|
|
50
|
+
try { return { root: registered, digest: sha256(readFileSync(file)) }; }
|
|
51
|
+
catch (error) { if (error.code !== "ENOENT") throw error; return { root: registered, digest: sha256({ absent: true }) }; }
|
|
52
|
+
});
|
|
53
|
+
const inputs = {
|
|
54
|
+
contentDigest: source.digest, catalogDigest: catalogDigest(manifest),
|
|
55
|
+
trustDigest: sha256({ trust, repositoryIdentity, enrolled: state.enrolled, pending: state.pending, keyLogDigest }),
|
|
56
|
+
approvalsDigest: sha256(approvals), scannerPolicyDigest: policyDigest(),
|
|
57
|
+
protectedFloors: { minimumScannerVersion: trust?.minimumScannerVersion ?? null, minimumCatalogVersion: trust?.minimumCatalogVersion ?? null, minimumCatalogDigest: trust?.minimumCatalogDigest ?? null },
|
|
58
|
+
};
|
|
59
|
+
ownershipCacheKey(inputs);
|
|
60
|
+
return { inputs, trust, complete: source.complete, enrollmentPending: trust?.enrollmentComplete === false || state.enrolled !== true || state.pending === true,
|
|
61
|
+
metadata: { repositoryIdentity, scannerVersion: SCANNER_VERSION, catalogVersion: manifest.version ?? String(manifest.schema ?? "1"), catalogDigest: inputs.catalogDigest, coverage: source.coverage, roots: roots.map((registered) => ({ root: registered })) } };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function ownershipScanInputs(appRoot) { return scanInputs(appRoot); }
|
|
65
|
+
export function ownershipScanInputsForTest(appRoot, options) { return scanInputs(appRoot, options); }
|
|
66
|
+
|
|
67
|
+
function scanProtected(appRoot, options, createEvidence) {
|
|
68
|
+
const before = scanInputs(appRoot, options);
|
|
69
|
+
const preliminary = { ok: true, enrollmentPending: before.enrollmentPending, violations: [], problems: [], coverage: before.metadata.coverage, metadata: before.metadata };
|
|
70
|
+
const preflight = ownershipVerdict(preliminary, before.trust);
|
|
71
|
+
if (!preflight.admit && preflight.code !== "RK_OWNERSHIP_PENDING_ENROLLMENT") return { ...before, report: { ...preliminary, ok: false }, verdict: preflight };
|
|
72
|
+
let checked;
|
|
73
|
+
try { checked = createEvidence(); }
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (preflight.code !== "RK_OWNERSHIP_PENDING_ENROLLMENT") throw error;
|
|
76
|
+
const report = { ...preliminary, ok: false, problems: [error.message] };
|
|
77
|
+
return { ...before, report, verdict: ownershipVerdict(report, before.trust) };
|
|
78
|
+
}
|
|
79
|
+
const report = JSON.parse(JSON.stringify({ ...checked.result, ok: checked.ok, enrollmentPending: checked.enrollmentPending === true,
|
|
80
|
+
metadata: { ...checked.result.metadata, scannerVersion: checked.evidence.scannerVersion, catalogVersion: checked.evidence.catalogVersion } }));
|
|
81
|
+
const verdict = ownershipVerdict(report, before.trust);
|
|
82
|
+
const after = scanInputs(appRoot, options);
|
|
83
|
+
if (ownershipCacheKey(before.inputs) !== ownershipCacheKey(after.inputs) || before.complete !== after.complete || sha256(before.metadata.coverage) !== sha256(after.metadata.coverage))
|
|
84
|
+
return { error: "ownership inputs changed during protected evidence scan", code: "RK_OWNERSHIP_INPUTS_CHANGED" };
|
|
85
|
+
return { inputs: after.inputs, trust: after.trust, report, verdict, evidence: checked.evidence };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function scanOwnership(appRoot) { return scanProtected(appRoot, {}, () => createOwnershipEvidence(appRoot)); }
|
|
89
|
+
export function scanOwnershipForTest(appRoot, options) { return scanProtected(appRoot, options, () => createOwnershipEvidenceForTest(appRoot, options)); }
|
|
90
|
+
|
|
91
|
+
const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
|
|
92
|
+
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
|
|
93
|
+
try {
|
|
94
|
+
const [appRoot, manifestPath, mode] = process.argv.slice(2);
|
|
95
|
+
if (!appRoot || !manifestPath || path.resolve(manifestPath) !== catalogPath || mode && mode !== "--inputs") throw new TypeError("app root & installed protected catalog path are required");
|
|
96
|
+
process.stdout.write(JSON.stringify(mode === "--inputs" ? ownershipScanInputs(path.resolve(appRoot)) : scanOwnership(path.resolve(appRoot))) + "\n");
|
|
97
|
+
} catch (error) {
|
|
98
|
+
process.stdout.write(JSON.stringify({ error: error.message, code: error.code?.startsWith("RK_OWNERSHIP_") ? error.code : "RK_OWNERSHIP_UNVERIFIED" }) + "\n");
|
|
99
|
+
}
|
|
15
100
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Code-only patterns may consume string arguments, but must start outside
|
|
2
|
+
// literals/comments. Keep offsets/newlines intact for multiline call reporting.
|
|
3
|
+
export function sourceNonCode(text, rust) {
|
|
4
|
+
const nonCode = new Uint8Array(text.length);
|
|
5
|
+
for (let i = 0; i < text.length;) {
|
|
6
|
+
const start = i;
|
|
7
|
+
if (text.startsWith("//", i) || text.startsWith("/*", i)) {
|
|
8
|
+
if (text[i + 1] === "/") {
|
|
9
|
+
i = text.indexOf("\n", i + 2);
|
|
10
|
+
if (i === -1) i = text.length;
|
|
11
|
+
} else {
|
|
12
|
+
i += 2;
|
|
13
|
+
let depth = 1;
|
|
14
|
+
while (i < text.length && depth) {
|
|
15
|
+
if (rust && text.startsWith("/*", i)) { depth++; i += 2; }
|
|
16
|
+
else if (text.startsWith("*/", i)) { depth--; i += 2; }
|
|
17
|
+
else i++;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
nonCode.fill(1, start, i);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const raw = rust && (i === 0 || !/[\w]/.test(text[i - 1])) && /^(?:br|cr|r)(#*)"/.exec(text.slice(i));
|
|
24
|
+
if (raw) {
|
|
25
|
+
const closing = `"${raw[1]}`, end = text.indexOf(closing, i + raw[0].length);
|
|
26
|
+
i = end === -1 ? text.length : end + closing.length;
|
|
27
|
+
} else if (text[i] === '"' || (!rust && (text[i] === "'" || text[i] === "`")) ||
|
|
28
|
+
(rust && text[i] === "'" && /^'(?:\\(?:u\{[\da-fA-F]+\}|x[\da-fA-F]{2}|[^\r\n])|[^'\\\r\n])'/.test(text.slice(i)))) {
|
|
29
|
+
const quote = text[i++];
|
|
30
|
+
while (i < text.length) {
|
|
31
|
+
if (text[i] === "\\") i = Math.min(i + 2, text.length);
|
|
32
|
+
else if (text[i++] === quote) break;
|
|
33
|
+
}
|
|
34
|
+
} else { i++; continue; }
|
|
35
|
+
nonCode.fill(1, start, i);
|
|
36
|
+
}
|
|
37
|
+
return nonCode;
|
|
38
|
+
}
|
|
39
|
+
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Advisory pre-commit ownership gate: scans the STAGED tree (index content), never the working tree.
|
|
2
|
+
// Authoritative enforcement stays in broker admission and the release gate.
|
|
3
|
+
//
|
|
4
|
+
// right-release ownership --staged [--app <dir>] [--json]
|
|
5
|
+
//
|
|
6
|
+
// Block-tier findings without a valid exception fail the gate. Advise-tier findings print a
|
|
7
|
+
// summary and pass. A new Cargo.toml/package.json whose name or description matches a catalog
|
|
8
|
+
// capability keyword is advised unless it declares that capability's owner package.
|
|
9
|
+
import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { parse as parseToml } from "smol-toml";
|
|
14
|
+
import { spawnSync } from "./background-process.mjs";
|
|
15
|
+
import { checkApp, MANIFEST_FILE } from "./ownership.mjs";
|
|
16
|
+
import { isBlockingFinding } from "./ownership-verdict.mjs";
|
|
17
|
+
|
|
18
|
+
const releaseRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
export const DEFAULT_MANIFEST = path.join(releaseRoot, MANIFEST_FILE);
|
|
20
|
+
const NEW_MANIFEST_NAMES = new Set(["Cargo.toml", "package.json"]);
|
|
21
|
+
|
|
22
|
+
const cleanEnv = () => Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^GIT_/i.test(key)));
|
|
23
|
+
|
|
24
|
+
function git(cwd, args, { env = cleanEnv(), encoding = "buffer" } = {}) {
|
|
25
|
+
const result = spawnSync("git", ["-C", cwd, ...args], { encoding, env, maxBuffer: 512 * 1024 * 1024, windowsHide: true });
|
|
26
|
+
if (result?.error || result?.status !== 0) {
|
|
27
|
+
throw new Error(`git ${args[0]} failed${result?.stderr ? `: ${String(result.stderr).trim().slice(0, 200)}` : ""}`);
|
|
28
|
+
}
|
|
29
|
+
return result.stdout;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Repository top-level for a directory, or a thrown error when it is not a Git worktree. */
|
|
33
|
+
export function repositoryRootFor(dir) {
|
|
34
|
+
return realpathSync(String(git(path.resolve(dir), ["rev-parse", "--show-toplevel"], { encoding: "utf8" })).trim());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Staged index path for this repository; honors GIT_INDEX_FILE so pre-commit hooks see the real index. */
|
|
38
|
+
function stagedIndex(repoRoot) {
|
|
39
|
+
if (process.env.GIT_INDEX_FILE) return path.resolve(repoRoot, process.env.GIT_INDEX_FILE);
|
|
40
|
+
return path.resolve(repoRoot, String(git(repoRoot, ["rev-parse", "--git-path", "index"], { encoding: "utf8" })).trim());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stagedEnv(repoRoot) {
|
|
44
|
+
return { ...cleanEnv(), GIT_INDEX_FILE: stagedIndex(repoRoot) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Materialize the staged index into a throwaway worktree. The temporary repository borrows the
|
|
49
|
+
* real object store through alternates, so blobs are the exact staged bytes. The real HEAD,
|
|
50
|
+
* index, and working tree are never written.
|
|
51
|
+
*/
|
|
52
|
+
export function materializeStagedTree(repoRoot) {
|
|
53
|
+
const root = realpathSync(repoRoot);
|
|
54
|
+
const workdir = mkdtempSync(path.join(os.tmpdir(), "rightkit-staged-"));
|
|
55
|
+
const tree = path.join(workdir, "tree");
|
|
56
|
+
try {
|
|
57
|
+
mkdirSync(tree);
|
|
58
|
+
const init = spawnSync("git", ["init", "-q", tree], { env: cleanEnv(), windowsHide: true, encoding: "utf8" });
|
|
59
|
+
if (init.status !== 0) throw new Error("git init failed for staged scan");
|
|
60
|
+
const objects = path.resolve(root, String(git(root, ["rev-parse", "--git-path", "objects"], { encoding: "utf8" })).trim());
|
|
61
|
+
writeFileSync(path.join(tree, ".git", "objects", "info", "alternates"), `${objects}\n`);
|
|
62
|
+
copyFileSync(stagedIndex(root), path.join(tree, ".git", "index"));
|
|
63
|
+
const origin = spawnSync("git", ["-C", root, "config", "--get", "remote.origin.url"], { env: cleanEnv(), windowsHide: true, encoding: "utf8" });
|
|
64
|
+
if (origin.status === 0 && origin.stdout.trim()) git(tree, ["config", "remote.origin.url", origin.stdout.trim()], { encoding: "utf8" });
|
|
65
|
+
git(tree, ["-c", "core.autocrlf=false", "-c", "core.eol=lf", "checkout-index", "-a", "-f"], { encoding: "utf8" });
|
|
66
|
+
return { tree, cleanup: () => rmSync(workdir, { recursive: true, force: true }) };
|
|
67
|
+
} catch (error) {
|
|
68
|
+
rmSync(workdir, { recursive: true, force: true });
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
74
|
+
|
|
75
|
+
/** Keyword index from the catalog: each capability id matches its hyphen, space, or underscore spellings. */
|
|
76
|
+
export function capabilityKeywords(manifest) {
|
|
77
|
+
const byCapability = new Map();
|
|
78
|
+
for (const entry of manifest.capabilities ?? []) {
|
|
79
|
+
if (!byCapability.has(entry.capability)) byCapability.set(entry.capability, new Set());
|
|
80
|
+
byCapability.get(entry.capability).add(entry.package);
|
|
81
|
+
}
|
|
82
|
+
return [...byCapability].map(([capability, owners]) => ({
|
|
83
|
+
capability,
|
|
84
|
+
owners: [...owners],
|
|
85
|
+
pattern: new RegExp(`(?<![a-z0-9])${escapeRegExp(capability).replace(/-/g, "[-_ ]")}(?![a-z0-9])`, "i"),
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function manifestIdentity(file, bytes) {
|
|
90
|
+
const text = bytes.toString("utf8");
|
|
91
|
+
if (path.posix.basename(file) === "Cargo.toml") {
|
|
92
|
+
const parsed = parseToml(text);
|
|
93
|
+
const dependencyTables = [parsed.dependencies, parsed["dev-dependencies"], parsed["build-dependencies"], parsed.workspace?.dependencies];
|
|
94
|
+
for (const target of Object.values(parsed.target ?? {})) dependencyTables.push(target.dependencies, target["dev-dependencies"], target["build-dependencies"]);
|
|
95
|
+
const declared = new Set();
|
|
96
|
+
for (const table of dependencyTables) for (const [key, value] of Object.entries(table ?? {})) {
|
|
97
|
+
declared.add(key);
|
|
98
|
+
if (value && typeof value === "object" && typeof value.package === "string") declared.add(value.package);
|
|
99
|
+
}
|
|
100
|
+
return { name: parsed.package?.name ?? "", description: parsed.package?.description ?? "", declared };
|
|
101
|
+
}
|
|
102
|
+
const parsed = JSON.parse(text);
|
|
103
|
+
const declared = new Set();
|
|
104
|
+
for (const field of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) for (const key of Object.keys(parsed[field] ?? {})) declared.add(key);
|
|
105
|
+
return { name: parsed.name ?? "", description: parsed.description ?? "", declared };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Advisory: new staged manifests whose name/description mention a capability without declaring its owner. */
|
|
109
|
+
export function newManifestAdvisories(repoRoot, manifest, { env = stagedEnv(repoRoot) } = {}) {
|
|
110
|
+
const added = git(repoRoot, ["diff", "--cached", "--name-only", "--diff-filter=A", "-z"], { env })
|
|
111
|
+
.toString("utf8").split("\0").filter(Boolean)
|
|
112
|
+
.filter((file) => NEW_MANIFEST_NAMES.has(path.posix.basename(file)));
|
|
113
|
+
const keywords = capabilityKeywords(manifest);
|
|
114
|
+
const advisories = [];
|
|
115
|
+
for (const file of added) {
|
|
116
|
+
let identity;
|
|
117
|
+
try { identity = manifestIdentity(file, git(repoRoot, ["show", `:${file}`], { env })); }
|
|
118
|
+
catch (error) { advisories.push({ file, capability: null, message: `could not parse staged manifest: ${error.message}` }); continue; }
|
|
119
|
+
const text = `${identity.name}\n${identity.description}`;
|
|
120
|
+
for (const { capability, owners, pattern } of keywords) {
|
|
121
|
+
if (!pattern.test(text)) continue;
|
|
122
|
+
if (owners.includes(identity.name)) continue;
|
|
123
|
+
if (owners.some((owner) => identity.declared.has(owner))) continue;
|
|
124
|
+
advisories.push({ file, capability, owners, message: `new manifest ${file} matches capability "${capability}" without declaring its owner (${owners.join(", ")}); use the RightKit owner or declare it` });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return advisories;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Scan the staged tree. `scan` and `trust` are injectable for fixtures; production loads the
|
|
132
|
+
* protected trust registry through the scanner itself.
|
|
133
|
+
*/
|
|
134
|
+
export function scanStaged(repoRoot, { manifest, scan = checkApp, trust, materialize = materializeStagedTree } = {}) {
|
|
135
|
+
const root = realpathSync(repoRoot);
|
|
136
|
+
const resolvedManifest = manifest ?? JSON.parse(readFileSync(DEFAULT_MANIFEST, "utf8"));
|
|
137
|
+
const staged = materialize(root);
|
|
138
|
+
let result;
|
|
139
|
+
// Hooks export GIT_DIR/GIT_INDEX_FILE; the scanner's own Git calls must address the staged copy only.
|
|
140
|
+
const saved = Object.fromEntries(Object.entries(process.env).filter(([key]) => /^GIT_/i.test(key)));
|
|
141
|
+
for (const key of Object.keys(saved)) delete process.env[key];
|
|
142
|
+
try {
|
|
143
|
+
result = scan(staged.tree, resolvedManifest, trust === undefined ? {} : { trust });
|
|
144
|
+
} finally {
|
|
145
|
+
Object.assign(process.env, saved);
|
|
146
|
+
staged.cleanup();
|
|
147
|
+
}
|
|
148
|
+
const blocking = result.violations.filter(isBlockingFinding);
|
|
149
|
+
const advisory = result.violations.filter((finding) => !isBlockingFinding(finding));
|
|
150
|
+
const keywords = newManifestAdvisories(root, resolvedManifest);
|
|
151
|
+
return {
|
|
152
|
+
ok: blocking.length === 0,
|
|
153
|
+
blocking,
|
|
154
|
+
advisory,
|
|
155
|
+
keywords,
|
|
156
|
+
problems: result.problems ?? [],
|
|
157
|
+
warnings: result.warnings ?? [],
|
|
158
|
+
coverage: result.coverage ?? [],
|
|
159
|
+
enrollmentPending: result.enrollmentPending === true,
|
|
160
|
+
covered: result.covered?.length ?? 0,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function formatStagedReport(result) {
|
|
165
|
+
const lines = [];
|
|
166
|
+
for (const finding of result.blocking) lines.push(` BLOCK ${finding.fingerprint} (${finding.package}) ${finding.file}${finding.line ? `:${finding.line}` : ""} ${finding.text ?? ""}`);
|
|
167
|
+
for (const finding of result.advisory) lines.push(` advise ${finding.fingerprint} (${finding.package}) ${finding.file}${finding.line ? `:${finding.line}` : ""}`);
|
|
168
|
+
for (const item of result.keywords) lines.push(` advise ${item.message}`);
|
|
169
|
+
for (const problem of result.problems) lines.push(` note ${problem}`);
|
|
170
|
+
for (const warning of result.warnings) lines.push(` note ${warning}`);
|
|
171
|
+
if (result.coverage.length) lines.push(` note ${result.coverage.length} staged path(s) unscanned by the gate; broker admission remains authoritative`);
|
|
172
|
+
const summary = `[right-release] staged ownership: ${result.blocking.length} block, ${result.advisory.length} advise, ${result.keywords.length} new-manifest advisory, ${result.covered} excepted`;
|
|
173
|
+
lines.push(result.ok ? `${summary}; passed` : `${summary}; FAILED: block-tier findings need an owner integration or a valid exception`);
|
|
174
|
+
return lines.join("\n");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** CLI entry used by `right-release ownership --staged`. Returns the process exit code. */
|
|
178
|
+
export function main(args, { print = (line) => console.log(line), scanner = scanStaged } = {}) {
|
|
179
|
+
const flag = (name) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; };
|
|
180
|
+
let repoRoot;
|
|
181
|
+
try { repoRoot = repositoryRootFor(flag("--app") ?? process.cwd()); }
|
|
182
|
+
catch { print("[right-release] ownership --staged FAILED: not inside a Git repository"); return 1; }
|
|
183
|
+
let result;
|
|
184
|
+
try { result = scanner(repoRoot); }
|
|
185
|
+
catch (error) {
|
|
186
|
+
if (args.includes("--json")) print(JSON.stringify({ ok: false, error: { type: "staged-scan", message: error.message } }, null, 2));
|
|
187
|
+
else print(`[right-release] ownership --staged FAILED: ${error.message}`);
|
|
188
|
+
return 1;
|
|
189
|
+
}
|
|
190
|
+
if (args.includes("--json")) print(JSON.stringify({ ok: result.ok, blocking: result.blocking, advisory: result.advisory, keywords: result.keywords, problems: result.problems, coverage: result.coverage, enrollmentPending: result.enrollmentPending }, null, 2));
|
|
191
|
+
else print(formatStagedReport(result));
|
|
192
|
+
return result.ok ? 0 : 1;
|
|
193
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { sha256, validateOwnershipFloor, verifyRepositoryRegistration } from "./ownership-approval.mjs";
|
|
2
|
+
|
|
3
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
4
|
+
const text = (value) => typeof value === "string" && value.length > 0;
|
|
5
|
+
const digest = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
6
|
+
const fields = (value, required, optional = {}) => object(value) && Object.keys(value).every((key) => Object.hasOwn(required, key) || Object.hasOwn(optional, key))
|
|
7
|
+
&& Object.entries(required).every(([key, check]) => Object.hasOwn(value, key) && check(value[key]))
|
|
8
|
+
&& Object.entries(optional).every(([key, check]) => !Object.hasOwn(value, key) || check(value[key]));
|
|
9
|
+
const strings = (value) => Array.isArray(value) && value.every(text);
|
|
10
|
+
const nullableText = (value) => value === null || text(value);
|
|
11
|
+
const string = (value) => typeof value === "string";
|
|
12
|
+
const boolean = (value) => typeof value === "boolean";
|
|
13
|
+
const ordinal = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
14
|
+
const version = (value) => text(value) && /^\d+(?:\.\d+)*$/.test(value);
|
|
15
|
+
const coverage = (value) => Array.isArray(value) && value.every((entry) => fields(entry, { path: text, status: text, reason: text }, { detail: text }));
|
|
16
|
+
const finding = (value, extra = {}) => fields(value, { fingerprint: text, capability: text, package: text, file: text, line: ordinal }, {
|
|
17
|
+
kind: (v) => ["source", "dependency", "transitive-dependency"].includes(v), lineText: string, ordinal, text: string, occurrenceHash: digest,
|
|
18
|
+
lockfile: text, dependencyPath: (v) => strings(v) && v.length >= 3,
|
|
19
|
+
ecosystem: text, name: text, alias: nullableText, section: text, requirement: nullableText, features: strings,
|
|
20
|
+
defaultFeatures: (v) => v === null || boolean(v), source: nullableText, resolvedVersion: nullableText, local: boolean, provenance: nullableText, tier: (v) => ["block", "advise"].includes(v), policy: text, ...extra,
|
|
21
|
+
}) && (value.kind !== "transitive-dependency" || text(value.name) && text(value.resolvedVersion) && text(value.source) && strings(value.dependencyPath) && value.dependencyPath.length >= 3 && value.dependencyPath.at(-1) === value.name && value.lockfile === value.file);
|
|
22
|
+
const findings = (value) => Array.isArray(value) && value.every((entry) => finding(entry));
|
|
23
|
+
const provenance = (value, extra = {}) => fields(value, { kind: (v) => ["third-party", "generated"].includes(v), requestId: text, contentDigest: digest }, extra);
|
|
24
|
+
const contentDigests = (value) => Array.isArray(value) && value.every((entry) => fields(entry, { path: text, digest: (v) => digest(v) || typeof v === "string" && /^git:[a-f0-9]{40,64}$/.test(v) }, { provenance }));
|
|
25
|
+
const rows = (value) => Array.isArray(value) && value.every((entry) => fields(entry, { capability: text, owner: text, uses: boolean, status: text, count: ordinal }, { root: text, owners: strings }));
|
|
26
|
+
const metadata = (value) => fields(value, { repositoryIdentity: nullableText, scannerVersion: text, catalogVersion: text, catalogDigest: digest, coverage }, {
|
|
27
|
+
commit: nullableText, tree: text, host: text, scannedAt: text, contentDigests, pathBase: text, pathPrefix: string,
|
|
28
|
+
provenance: (v) => Array.isArray(v) && v.every((entry) => provenance(entry, { paths: strings })),
|
|
29
|
+
exceptionDigest: digest, source: text, keyLogDigest: digest, keyCheckpoint: (v) => fields(v, { sequence: ordinal, head: digest }), rootFingerprint: digest,
|
|
30
|
+
keyAuthority: (v) => fields(v, { repositoryIdentity: text, commit: text, tree: text }),
|
|
31
|
+
roots: (v) => Array.isArray(v) && v.every((entry) => fields(entry, { root: text }, { metadata })),
|
|
32
|
+
});
|
|
33
|
+
const validReport = (value) => fields(value, { ok: (v) => typeof v === "boolean", enrollmentPending: (v) => typeof v === "boolean", violations: findings, problems: strings, coverage, metadata }, {
|
|
34
|
+
findings, covered: (v) => Array.isArray(v) && v.every((entry) => finding(entry, { exception: object })), rows, warnings: strings, warning: text,
|
|
35
|
+
});
|
|
36
|
+
/** Only `block`-tier or banned findings fail a scan; `advise` findings are reported, never refused. */
|
|
37
|
+
export const isBlockingFinding = (f) => f?.policy === "banned" || f?.tier !== "advise";
|
|
38
|
+
const refuse = (code, problems) => ({ admit: false, code, problems });
|
|
39
|
+
|
|
40
|
+
/** One fail-closed contract for protected scan workers & broker admission. */
|
|
41
|
+
export function ownershipVerdict(report, trust) {
|
|
42
|
+
if (!validReport(report)) return refuse("RK_OWNERSHIP_MALFORMED_REPORT", ["ownership report has missing, unknown, or wrong-typed fields"]);
|
|
43
|
+
if (report.enrollmentPending) return refuse("RK_OWNERSHIP_PENDING_ENROLLMENT", ["protected ownership approval enrollment is pending", ...report.problems]);
|
|
44
|
+
let registration;
|
|
45
|
+
try {
|
|
46
|
+
registration = verifyRepositoryRegistration(trust, report.metadata.repositoryIdentity, { files: [
|
|
47
|
+
...report.violations.map((entry) => entry.file), ...(report.findings ?? []).map((entry) => entry.file),
|
|
48
|
+
...(report.metadata.contentDigests ?? []).map((entry) => entry.path),
|
|
49
|
+
] });
|
|
50
|
+
} catch { return refuse("RK_OWNERSHIP_INVALID_REGISTRATION", ["protected repository registry is missing or malformed"]); }
|
|
51
|
+
if (!registration.ok) return refuse("RK_OWNERSHIP_INVALID_REGISTRATION", registration.problems);
|
|
52
|
+
const scannedRoots = report.metadata.roots?.map((entry) => entry.root) ?? [report.metadata.pathPrefix?.replace(/\/$/, "") || "."];
|
|
53
|
+
if (new Set(scannedRoots).size !== scannedRoots.length || sha256([...scannedRoots].sort()) !== sha256([...registration.registration.roots].sort()))
|
|
54
|
+
return refuse("RK_OWNERSHIP_INVALID_REGISTRATION", ["scan roots do not exactly cover protected repository registration"]);
|
|
55
|
+
const floor = validateOwnershipFloor(trust, report.metadata);
|
|
56
|
+
if (!floor.ok) return refuse("RK_OWNERSHIP_POLICY_DOWNGRADE", floor.problems);
|
|
57
|
+
const incomplete = [...report.coverage, ...report.metadata.coverage].filter((entry) => entry.status !== "complete");
|
|
58
|
+
if (incomplete.length) return refuse("RK_OWNERSHIP_INCOMPLETE_COVERAGE", [...new Set(incomplete.map((entry) => entry.path + ": " + entry.reason))]);
|
|
59
|
+
const blocking = report.violations.filter(isBlockingFinding);
|
|
60
|
+
if (blocking.length) return refuse("RK_OWNERSHIP_VIOLATION", blocking.map((entry) => entry.capability + " (" + entry.fingerprint + " in " + entry.file + ") is owned by " + entry.package));
|
|
61
|
+
if (report.problems.length) return refuse("RK_OWNERSHIP_UNVERIFIED", report.problems);
|
|
62
|
+
if (!report.ok) return refuse("RK_OWNERSHIP_REPORT_FAILED", ["protected ownership scanner reported ok:false"]);
|
|
63
|
+
const advisory = report.violations.filter((entry) => !isBlockingFinding(entry));
|
|
64
|
+
if (advisory.length) return { admit: true, code: "RK_OWNERSHIP_ADVISORY", problems: advisory.map((entry) => entry.capability + " (" + entry.fingerprint + " in " + entry.file + ") is owned by " + entry.package + " [advise]") };
|
|
65
|
+
return { admit: true, code: "RK_OWNERSHIP_CLEAN", problems: [] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** SHA-256 over every required authenticated cache binding; never directory or age. */
|
|
69
|
+
export function ownershipCacheKey(inputs) {
|
|
70
|
+
if (!fields(inputs, { contentDigest: digest, catalogDigest: digest, trustDigest: digest, approvalsDigest: digest, scannerPolicyDigest: digest,
|
|
71
|
+
protectedFloors: (value) => fields(value, { minimumScannerVersion: (v) => v === null || version(v), minimumCatalogVersion: (v) => v === null || version(v), minimumCatalogDigest: (v) => v === null || digest(v) }) }))
|
|
72
|
+
throw Object.assign(new TypeError("ownership cache inputs have missing, unknown, or malformed digest/floor fields"), { code: "RK_OWNERSHIP_INVALID_INPUTS" });
|
|
73
|
+
return sha256(inputs);
|
|
74
|
+
}
|