@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
package/ownership.mjs
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// RightKit ownership scan. Public exports retain the original findings-array API.
|
|
2
|
+
import { isBlockingFinding } from "./ownership-verdict.mjs";
|
|
2
3
|
import { spawnSync } from "./background-process.mjs";
|
|
3
|
-
import { canonicalRepositoryIdentity, loadTrustedExternal, occurrenceHash, resolveKeyLogTrust, sha256, verifyException, verifyProtectedProvenance } from "./ownership-approval.mjs";
|
|
4
|
+
import { canonicalRepositoryIdentity, loadTrustedExternal, occurrenceHash, policyEntryDigest, resolveKeyLogTrust, sha256, verifyException, verifyProtectedProvenance } from "./ownership-approval.mjs";
|
|
5
|
+
import installedManifest from "./rightkit-ownership.json" with { type: "json" };
|
|
4
6
|
import { parse as parseToml } from "smol-toml";
|
|
7
|
+
import { sourceNonCode } from "./ownership-source.mjs";
|
|
8
|
+
import { OWNERSHIP_LOCKFILES, transitiveDependencies } from "./ownership-lockfiles.mjs";
|
|
5
9
|
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
6
10
|
import { createHash } from "node:crypto";
|
|
7
11
|
import { acquireCommittedSource, committedSourceDataForInternalUse } from "./committed-source.mjs";
|
|
@@ -15,7 +19,7 @@ export const EXCEPTIONS_FILE = path.join(".rightkit", "ownership-exceptions.json
|
|
|
15
19
|
const CODE_EXT = new Set([".rs", ".ts", ".tsx", ".js", ".mjs", ".cjs", ".jsx", ".swift", ".m", ".mm", ".c", ".cc", ".cpp", ".h", ".py", ".go", ".kt", ".java", ".cs"]);
|
|
16
20
|
// Listed installed/build inputs retain explicit coverage diagnostics.
|
|
17
21
|
const OPAQUE_DIRS = new Set(["target", ".build", "DerivedData", "Pods"]);
|
|
18
|
-
export const SCANNER_VERSION = "2.
|
|
22
|
+
export const SCANNER_VERSION = "2.6.0";
|
|
19
23
|
export const catalogDigest = (manifest) => sha256(manifest);
|
|
20
24
|
const byteDigest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
21
25
|
const inputDigest = (file) => file.digest ?? byteDigest(file.bytes);
|
|
@@ -40,57 +44,24 @@ function sourceMatchers(manifest) {
|
|
|
40
44
|
return matchers;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
// Code-only patterns may consume string arguments, but must start outside
|
|
44
|
-
// literals/comments. Keep offsets/newlines intact for multiline call reporting.
|
|
45
|
-
function sourceNonCode(text, rust) {
|
|
46
|
-
const nonCode = new Uint8Array(text.length);
|
|
47
|
-
for (let i = 0; i < text.length;) {
|
|
48
|
-
const start = i;
|
|
49
|
-
if (text.startsWith("//", i) || text.startsWith("/*", i)) {
|
|
50
|
-
if (text[i + 1] === "/") {
|
|
51
|
-
i = text.indexOf("\n", i + 2);
|
|
52
|
-
if (i === -1) i = text.length;
|
|
53
|
-
} else {
|
|
54
|
-
i += 2;
|
|
55
|
-
let depth = 1;
|
|
56
|
-
while (i < text.length && depth) {
|
|
57
|
-
if (rust && text.startsWith("/*", i)) { depth++; i += 2; }
|
|
58
|
-
else if (text.startsWith("*/", i)) { depth--; i += 2; }
|
|
59
|
-
else i++;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
nonCode.fill(1, start, i);
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
const raw = rust && (i === 0 || !/[\w]/.test(text[i - 1])) && /^(?:br|cr|r)(#*)"/.exec(text.slice(i));
|
|
66
|
-
if (raw) {
|
|
67
|
-
const closing = `"${raw[1]}`, end = text.indexOf(closing, i + raw[0].length);
|
|
68
|
-
i = end === -1 ? text.length : end + closing.length;
|
|
69
|
-
} else if (text[i] === '"' || (!rust && (text[i] === "'" || text[i] === "`")) ||
|
|
70
|
-
(rust && text[i] === "'" && /^'(?:\\(?:u\{[\da-fA-F]+\}|x[\da-fA-F]{2}|[^\r\n])|[^'\\\r\n])'/.test(text.slice(i)))) {
|
|
71
|
-
const quote = text[i++];
|
|
72
|
-
while (i < text.length) {
|
|
73
|
-
if (text[i] === "\\") i = Math.min(i + 2, text.length);
|
|
74
|
-
else if (text[i++] === quote) break;
|
|
75
|
-
}
|
|
76
|
-
} else { i++; continue; }
|
|
77
|
-
nonCode.fill(1, start, i);
|
|
78
|
-
}
|
|
79
|
-
return nonCode;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
47
|
function sourceFindings(file, matchers) {
|
|
83
48
|
const ext = path.posix.extname(file.rel).toLowerCase();
|
|
84
49
|
const key = `${matchers.digest}:${ext}:${file.cacheKey ?? inputDigest(file)}`;
|
|
85
50
|
if (sourceCache.has(key)) return sourceCache.get(key);
|
|
86
51
|
const text = file.bytes.toString("utf8"), findings = [];
|
|
52
|
+
const ordinals = new Map();
|
|
53
|
+
const push = (c, f, line, lineText) => {
|
|
54
|
+
const identity = `${f.id}\0${lineText}`, ordinal = ordinals.get(identity) ?? 0;
|
|
55
|
+
ordinals.set(identity, ordinal + 1);
|
|
56
|
+
findings.push({ fingerprint: f.id, capability: c.capability, package: c.package, ...findingPolicy(c, f), kind: "source", line, lineText, ordinal, text: lineText.trim().slice(0, 160) });
|
|
57
|
+
};
|
|
87
58
|
if (matchers.prefilter.test(text)) {
|
|
88
59
|
const lines = text.split(/\r?\n/);
|
|
89
60
|
const applicable = matchers.sources.filter(({ f }) => !f.extensions || f.extensions.includes(ext));
|
|
90
61
|
const lineMatchers = applicable.filter(({ f }) => !f.codeOnly);
|
|
91
62
|
for (const [i, line] of lines.entries()) if (!commentOnly(line)) for (const { c, f, re } of lineMatchers) {
|
|
92
63
|
re.lastIndex = 0;
|
|
93
|
-
if (re.test(line))
|
|
64
|
+
if (re.test(line)) push(c, f, i + 1, line);
|
|
94
65
|
}
|
|
95
66
|
const codeMatchers = applicable.filter(({ f }) => f.codeOnly);
|
|
96
67
|
if (codeMatchers.length) {
|
|
@@ -104,7 +75,7 @@ function sourceFindings(file, matchers) {
|
|
|
104
75
|
for (; offset < match.index; offset++) if (text[offset] === "\n") line++;
|
|
105
76
|
if (reported.has(line)) continue;
|
|
106
77
|
reported.add(line);
|
|
107
|
-
|
|
78
|
+
push(c, f, line, lines[line - 1]);
|
|
108
79
|
}
|
|
109
80
|
}
|
|
110
81
|
}
|
|
@@ -121,7 +92,8 @@ export function aggregateOwnership(rightkitRoot) {
|
|
|
121
92
|
if (!existsSync(dir)) continue;
|
|
122
93
|
for (const name of readdirSync(dir).sort()) {
|
|
123
94
|
const child = path.join(dir, name);
|
|
124
|
-
let file = path.join(child,
|
|
95
|
+
let file = path.join(child, "ownership-declaration.json");
|
|
96
|
+
if (!existsSync(file)) file = path.join(child, MANIFEST_FILE);
|
|
125
97
|
if (!existsSync(file) && group === "swift") file = path.join(child, "rightkit-ownership.json");
|
|
126
98
|
if (!existsSync(file)) continue;
|
|
127
99
|
const declared = JSON.parse(readFileSync(file, "utf8"));
|
|
@@ -129,7 +101,9 @@ export function aggregateOwnership(rightkitRoot) {
|
|
|
129
101
|
for (const capability of declared.capabilities ?? []) entries.push({ package: declared.package, ...capability });
|
|
130
102
|
}
|
|
131
103
|
}
|
|
132
|
-
const
|
|
104
|
+
const internalFile = path.join(rightkitRoot, "packages/release/ownership-internal-owners.json");
|
|
105
|
+
const internalOwners = existsSync(internalFile) ? JSON.parse(readFileSync(internalFile, "utf8")).owners : [];
|
|
106
|
+
const manifest = { schema: 1, version: "2", capabilities: entries, internalOwners };
|
|
133
107
|
validateManifest(manifest);
|
|
134
108
|
return manifest;
|
|
135
109
|
}
|
|
@@ -137,7 +111,7 @@ export function aggregateOwnership(rightkitRoot) {
|
|
|
137
111
|
export function validateManifest(manifest) {
|
|
138
112
|
const ids = new Set();
|
|
139
113
|
for (const c of manifest.capabilities ?? []) {
|
|
140
|
-
if (!c.package || !c.capability || !Array.isArray(c.fingerprints)
|
|
114
|
+
if (!c.package || !c.capability || !Array.isArray(c.fingerprints)) throw new Error(`ownership: incomplete capability ${JSON.stringify(c).slice(0, 120)}`);
|
|
141
115
|
for (const f of c.fingerprints) {
|
|
142
116
|
if (!f.id || ids.has(f.id)) throw new Error(`ownership: missing or duplicate fingerprint id ${f.id}`);
|
|
143
117
|
ids.add(f.id);
|
|
@@ -177,18 +151,25 @@ function parseCargo(parsed, file, out, workspace, root, coverage, inventory) {
|
|
|
177
151
|
const pathSpec = (pathValue, sourceFile, display) => {
|
|
178
152
|
resolveLocalPath(root, sourceFile, pathValue, coverage, display, inventory);
|
|
179
153
|
};
|
|
180
|
-
const addTable = (table) => {
|
|
154
|
+
const addTable = (table, prefix = "") => {
|
|
181
155
|
if (!table || typeof table !== "object") return;
|
|
182
156
|
for (const section of ["dependencies", "dev-dependencies", "build-dependencies"]) for (const [alias, raw] of Object.entries(table[section] ?? {})) {
|
|
183
157
|
const spec = raw && typeof raw === "object" ? raw : {};
|
|
184
|
-
const
|
|
158
|
+
const inheritedRaw = spec.workspace === true ? workspace.deps?.[alias] : undefined;
|
|
159
|
+
if (spec.workspace === true && inheritedRaw === undefined) throw new Error(`missing workspace dependency: ${alias}`);
|
|
160
|
+
const inherited = typeof inheritedRaw === "string" ? { version: inheritedRaw } : inheritedRaw ?? {};
|
|
185
161
|
const name = spec.package ?? inherited.package ?? alias;
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (
|
|
162
|
+
const pathValue = spec.path ?? inherited.path, git = spec.git ?? inherited.git, selector = spec.rev ?? inherited.rev ?? spec.branch ?? inherited.branch ?? spec.tag ?? inherited.tag;
|
|
163
|
+
const source = pathValue ? `path:${pathValue}` : git ? `git+${git}${selector ? `#${selector}` : ""}` : "registry";
|
|
164
|
+
const requirement = typeof raw === "string" ? raw : spec.version ?? inherited.version ?? null;
|
|
165
|
+
if (spec.features !== undefined && !Array.isArray(spec.features) || inherited.features !== undefined && !Array.isArray(inherited.features)) throw new Error(`malformed dependency features: ${alias}`);
|
|
166
|
+
const features = [...new Set([...(inherited.features ?? []), ...(spec.features ?? [])])].sort();
|
|
167
|
+
if (!raw || typeof raw !== "string" && (typeof raw !== "object" || Array.isArray(raw)) || features.some((x) => typeof x !== "string") || requirement !== null && typeof requirement !== "string") throw new Error(`malformed dependency declaration: ${alias}`);
|
|
168
|
+
addDependency(out, "cargo", name, file, { alias: name === alias ? null : alias, section: `${prefix}${section}`, requirement, features, defaultFeatures: spec["default-features"] ?? inherited["default-features"] ?? null, source, resolvedVersion: null, local: !!pathValue || !!git });
|
|
169
|
+
if (pathValue) pathSpec(pathValue, spec.path ? file : path.posix.join(workspace.dir ?? ".", "Cargo.toml"), file);
|
|
170
|
+
if (git) coverage.push({ path: file, status: "unscanned", reason: `git-dependency-source:${name}` });
|
|
190
171
|
}
|
|
191
|
-
for (const target of Object.
|
|
172
|
+
for (const [target, value] of Object.entries(table.target ?? {})) addTable(value, `${prefix}target.'${target}'.`);
|
|
192
173
|
};
|
|
193
174
|
addTable(parsed);
|
|
194
175
|
}
|
|
@@ -211,7 +192,7 @@ function fileInventory(appRoot, trust, pathPrefix = "", repositoryRoot = appRoot
|
|
|
211
192
|
const inventory = gitListedInventory(appRoot, {
|
|
212
193
|
repositoryRoot,
|
|
213
194
|
protectedPaths: [...protectedPaths],
|
|
214
|
-
relevant: (rel) => ["Cargo.toml", "package.json", "Package.swift"].includes(path.posix.basename(rel)) || CODE_EXT.has(path.posix.extname(rel).toLowerCase()),
|
|
195
|
+
relevant: (rel) => ["Cargo.toml", "package.json", "Package.swift", ...OWNERSHIP_LOCKFILES].includes(path.posix.basename(rel)) || CODE_EXT.has(path.posix.extname(rel).toLowerCase()),
|
|
215
196
|
maxFileBytes: (rel) => CODE_EXT.has(path.posix.extname(rel).toLowerCase()) || protectedPaths.has(rel) ? 2 * 1024 * 1024 : 64 * 1024 * 1024,
|
|
216
197
|
});
|
|
217
198
|
for (const item of inventory.coverage) if (item.reason === "file-over-size-limit") item.reason = protectedPaths.has(item.path) ? "protected-provenance-file-over-2MiB" : CODE_EXT.has(path.posix.extname(item.path).toLowerCase()) ? "source-file-over-2MiB" : "manifest-file-over-64MiB";
|
|
@@ -231,7 +212,7 @@ function committedFileInventory(appRoot, trust, pathPrefix, committed) {
|
|
|
231
212
|
if (entry.mode === "120000") { coverage.push({ path: rel, status: "unscanned", reason: "committed-symlink" }); continue; }
|
|
232
213
|
if (entry.mode === "160000" || entry.type === "commit") { coverage.push({ path: rel, status: "unscanned", reason: "committed-gitlink" }); continue; }
|
|
233
214
|
if (entry.type !== "blob") { coverage.push({ path: rel, status: "unscanned", reason: "committed-unsupported-entry" }); continue; }
|
|
234
|
-
const relevant = protectedPaths.has(entry.rel) || ["Cargo.toml", "package.json", "Package.swift", "ownership-exceptions.json"].includes(path.posix.basename(rel)) || CODE_EXT.has(path.posix.extname(rel).toLowerCase());
|
|
215
|
+
const relevant = protectedPaths.has(entry.rel) || ["Cargo.toml", "package.json", "Package.swift", "ownership-exceptions.json", ...OWNERSHIP_LOCKFILES].includes(path.posix.basename(rel)) || CODE_EXT.has(path.posix.extname(rel).toLowerCase());
|
|
235
216
|
const bytes = relevant ? entry.bytes : undefined;
|
|
236
217
|
if (relevant && !bytes) coverage.push({ path: rel, status: "unscanned", reason: "committed-blob-unavailable" });
|
|
237
218
|
const isExceptionFile = path.posix.basename(rel) === "ownership-exceptions.json";
|
|
@@ -253,6 +234,113 @@ function committedFileInventory(appRoot, trust, pathPrefix, committed) {
|
|
|
253
234
|
return { files, coverage, committed: true, pathPrefix, repositoryFiles, snapshotFiles: new Map(repositoryFiles.map((file) => [file.rel, file])) };
|
|
254
235
|
}
|
|
255
236
|
|
|
237
|
+
function swiftPackageArguments(text) {
|
|
238
|
+
const args = [];
|
|
239
|
+
for (const match of text.matchAll(/\.package\s*\(/g)) {
|
|
240
|
+
let at = match.index + match[0].length, start = at, depth = 1, quote = false;
|
|
241
|
+
for (; at < text.length && depth; at++) {
|
|
242
|
+
if (text[at] === "\\" && quote) { at++; continue; }
|
|
243
|
+
if (text[at] === '"') quote = !quote;
|
|
244
|
+
if (!quote && text[at] === "(") depth++;
|
|
245
|
+
if (!quote && text[at] === ")") depth--;
|
|
246
|
+
}
|
|
247
|
+
if (depth) throw new Error("unterminated Swift package declaration");
|
|
248
|
+
args.push(text.slice(start, at - 1));
|
|
249
|
+
}
|
|
250
|
+
return args;
|
|
251
|
+
}
|
|
252
|
+
function resolveDependencyVersions(appRoot, inventory, deps, coverage, contentDigests) {
|
|
253
|
+
const cache = new Map();
|
|
254
|
+
const adjacent = (file, name) => {
|
|
255
|
+
const rel = path.posix.join(path.posix.dirname(file), name);
|
|
256
|
+
if (cache.has(rel)) return cache.get(rel);
|
|
257
|
+
let bytes;
|
|
258
|
+
if (inventory.committed) {
|
|
259
|
+
const item = inventory.snapshotFiles.get(`${inventory.pathPrefix ?? ""}${rel}`);
|
|
260
|
+
if (item && !item.bytes) throw new Error(`lockfile bytes unavailable: ${rel}`);
|
|
261
|
+
bytes = item?.bytes;
|
|
262
|
+
} else {
|
|
263
|
+
const inventoried = OWNERSHIP_LOCKFILES.includes(name) ? inventory.files.find((item) => item.rel === rel) : undefined;
|
|
264
|
+
if (inventoried && !inventoried.bytes) throw new Error(`lockfile bytes unavailable: ${rel}`);
|
|
265
|
+
const absolute = path.resolve(appRoot, ...rel.split("/"));
|
|
266
|
+
if (inventoried) bytes = inventoried.bytes;
|
|
267
|
+
else if (existsSync(absolute)) {
|
|
268
|
+
const root = realpathSync(appRoot), real = realpathSync(absolute);
|
|
269
|
+
if (!real.startsWith(`${root}${path.sep}`)) throw new Error(`lockfile outside scan root: ${rel}`);
|
|
270
|
+
if (!lstatSync(absolute).isFile() || lstatSync(absolute).size > 64 * 1024 * 1024) throw new Error(`lockfile unsupported or too large: ${rel}`);
|
|
271
|
+
bytes = readFileSync(absolute);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const text = bytes?.toString("utf8");
|
|
275
|
+
cache.set(rel, text);
|
|
276
|
+
if (bytes) contentDigests.push({ path: rel, digest: byteDigest(bytes) });
|
|
277
|
+
return text;
|
|
278
|
+
};
|
|
279
|
+
for (const d of deps) {
|
|
280
|
+
try {
|
|
281
|
+
if (d.ecosystem === "cargo") {
|
|
282
|
+
const text = adjacent(d.file, "Cargo.lock");
|
|
283
|
+
if (text !== undefined) {
|
|
284
|
+
const lock = parseToml(text);
|
|
285
|
+
if (!Array.isArray(lock.package)) throw new Error("Cargo.lock package list missing");
|
|
286
|
+
let candidates = lock.package.filter((p) => p.name === d.name && (d.source === "registry" ? String(p.source ?? "").startsWith("registry+") : d.source.startsWith("git+") ? String(p.source ?? "").split("#")[0].split("?")[0] === d.source.split("#")[0] : !p.source));
|
|
287
|
+
if (candidates.length > 1) {
|
|
288
|
+
const manifestText = inventory.files.find((f) => f.rel === d.file)?.bytes?.toString("utf8");
|
|
289
|
+
const owner = manifestText && parseToml(manifestText).package?.name;
|
|
290
|
+
const refs = lock.package.filter((p) => p.name === owner).flatMap((p) => p.dependencies ?? []);
|
|
291
|
+
candidates = candidates.filter((p) => refs.some((ref) => ref === `${d.name} ${p.version}` || ref.startsWith(`${d.name} ${p.version} (`)));
|
|
292
|
+
}
|
|
293
|
+
if (candidates.length !== 1 || typeof candidates[0].version !== "string") throw new Error(`Cargo.lock resolution missing or ambiguous: ${d.name}`);
|
|
294
|
+
d.resolvedVersion = candidates[0].version;
|
|
295
|
+
}
|
|
296
|
+
} else if (d.ecosystem === "npm") {
|
|
297
|
+
const text = adjacent(d.file, "npm-shrinkwrap.json") ?? adjacent(d.file, "package-lock.json");
|
|
298
|
+
if (text !== undefined) {
|
|
299
|
+
const lock = JSON.parse(text), name = d.alias ?? d.name;
|
|
300
|
+
const item = lock.packages?.[`node_modules/${name}`] ?? lock.dependencies?.[name];
|
|
301
|
+
if (typeof item?.version !== "string") throw new Error(`npm lock resolution missing: ${name}`);
|
|
302
|
+
d.resolvedVersion = item.version;
|
|
303
|
+
} else {
|
|
304
|
+
const pnpm = adjacent(d.file, "pnpm-lock.yaml"), yarn = pnpm === undefined ? adjacent(d.file, "yarn.lock") : undefined;
|
|
305
|
+
if (pnpm !== undefined) {
|
|
306
|
+
let section, name, dependencyIndent, inRoot = !/^importers:/m.test(pnpm), rootIndent;
|
|
307
|
+
for (const line of pnpm.split(/\r?\n/)) {
|
|
308
|
+
const entry = /^(\s*)(?:'([^']+)'|"([^"]+)"|([^:]+)):\s*(.*?)\s*$/.exec(line);
|
|
309
|
+
if (!entry) continue;
|
|
310
|
+
const indent = entry[1].length, key = entry[2] ?? entry[3] ?? entry[4], value = entry[5];
|
|
311
|
+
if (key === "." && indent === 2) { inRoot = true; rootIndent = 2; continue; }
|
|
312
|
+
if (rootIndent !== undefined && indent <= rootIndent) inRoot = false;
|
|
313
|
+
if (!inRoot) continue;
|
|
314
|
+
if (["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"].includes(key)) { section = key; dependencyIndent = indent + 2; name = undefined; continue; }
|
|
315
|
+
if (indent === dependencyIndent) { name = key; if (name === (d.alias ?? d.name) && section === d.section && value) d.resolvedVersion = value.replace(/^['"]|['"]$/g, "").split("(")[0]; }
|
|
316
|
+
if (key === "version" && name === (d.alias ?? d.name) && section === d.section && indent === dependencyIndent + 2) d.resolvedVersion = value.replace(/^['"]|['"]$/g, "").split("(")[0].replace(/^.*@(?=\d)/, "");
|
|
317
|
+
}
|
|
318
|
+
if (!d.resolvedVersion) throw new Error(`pnpm lock resolution missing: ${d.alias ?? d.name}`);
|
|
319
|
+
} else if (yarn !== undefined) {
|
|
320
|
+
let selected = false;
|
|
321
|
+
for (const line of yarn.split(/\r?\n/)) {
|
|
322
|
+
if (/^\S/.test(line) && /:$/.test(line)) selected = line.includes(`${d.alias ?? d.name}@${d.requirement}`) || line.includes(`${d.alias ?? d.name}@npm:${d.requirement}`);
|
|
323
|
+
const version = /^\s+version(?::|\s)\s*["']?([^\s"']+)/.exec(line);
|
|
324
|
+
if (selected && version) d.resolvedVersion = version[1];
|
|
325
|
+
}
|
|
326
|
+
if (!d.resolvedVersion) throw new Error(`yarn lock resolution missing: ${d.alias ?? d.name}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} else if (d.ecosystem === "swiftpm") {
|
|
330
|
+
const text = adjacent(d.file, "Package.resolved");
|
|
331
|
+
if (text !== undefined && !d.local) {
|
|
332
|
+
const lock = JSON.parse(text), pins = lock.pins ?? lock.object?.pins;
|
|
333
|
+
if (!Array.isArray(pins)) throw new Error("Swift lock pins missing");
|
|
334
|
+
const pin = pins.filter((p) => (p.identity ?? p.package)?.toLowerCase() === d.name.toLowerCase() || p.location === d.source.slice(4).split("#")[0] || p.repositoryURL === d.source.slice(4).split("#")[0]);
|
|
335
|
+
if (pin.length !== 1) throw new Error(`Swift lock resolution missing or ambiguous: ${d.name}`);
|
|
336
|
+
d.resolvedVersion = pin[0].state?.version ?? null;
|
|
337
|
+
if (d.resolvedVersion !== null && typeof d.resolvedVersion !== "string") throw new Error(`invalid Swift resolved version: ${d.name}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
} catch (error) { coverage.push({ path: d.file, status: "unscanned", reason: `dependency-lock-resolution:${error.message}` }); }
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
256
344
|
function dependenciesFromInventory(appRoot, inventory) {
|
|
257
345
|
const deps = [], coverage = [], contentDigests = [];
|
|
258
346
|
// Workspace inheritance is loaded before member manifests.
|
|
@@ -304,29 +392,35 @@ function dependenciesFromInventory(appRoot, inventory) {
|
|
|
304
392
|
else if (base === "package.json") {
|
|
305
393
|
let pkg; try { pkg = JSON.parse(text); } catch { coverage.push({ path: rel, status: "unscanned", reason: "invalid-json" }); continue; }
|
|
306
394
|
for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) for (const [name, spec] of Object.entries(pkg[field] ?? {})) {
|
|
307
|
-
if (typeof spec !== "string") continue;
|
|
395
|
+
if (typeof spec !== "string") { coverage.push({ path: rel, status: "unscanned", reason: `invalid-npm-dependency:${name}` }); continue; }
|
|
308
396
|
const alias = spec.match(/^(?:npm:)(@[^/]+\/[^@]+|[^@]+)(?:@|$)/)?.[1];
|
|
309
397
|
const localPath = spec.match(/^(?:file:|link:)(.+)$/)?.[1];
|
|
310
398
|
const isGit = /^(git\+|https?:.*\.git)/i.test(spec);
|
|
311
399
|
const devOnly = field === "devDependencies" && !["dependencies", "optionalDependencies", "peerDependencies"].some((section) => Object.hasOwn(pkg[section] ?? {}, name));
|
|
312
|
-
addDependency(deps, "npm", alias ?? name, rel, { alias: alias ? name :
|
|
400
|
+
addDependency(deps, "npm", alias ?? name, rel, { alias: alias ? name : null, section: field, requirement: spec, features: [], defaultFeatures: null, resolvedVersion: null, local: !!localPath, source: localPath ? `path:${localPath}` : isGit ? spec.startsWith("git+") ? spec : `git+${spec}` : spec.startsWith("workspace:") ? `path:${spec}` : "registry", ...(devOnly ? { devOnly: true } : {}) });
|
|
313
401
|
if (localPath) {
|
|
314
402
|
resolveLocalPath(appRoot, rel, localPath, coverage, rel, inventory, "local-npm-dependency", name);
|
|
315
403
|
} else if (isGit) coverage.push({ path: rel, status: "unscanned", reason: `git-npm-dependency-source:${name}` });
|
|
316
404
|
}
|
|
317
405
|
} else {
|
|
318
|
-
for (const
|
|
319
|
-
const
|
|
320
|
-
const
|
|
321
|
-
|
|
406
|
+
for (const arg of swiftPackageArguments(text)) {
|
|
407
|
+
const url = arg.match(/url:\s*["']([^"']+)["']/)?.[1], pathDep = arg.match(/path:\s*["']([^"']+)["']/)?.[1], registry = arg.match(/id:\s*["']([^"']+)["']/)?.[1];
|
|
408
|
+
const explicitName = arg.match(/name:\s*["']([^"']+)["']/)?.[1];
|
|
409
|
+
const id = explicitName ?? registry ?? url?.replace(/\.git$/i, "").split("/").pop()?.toLowerCase() ?? pathDep?.split(/[\\/]/).pop();
|
|
410
|
+
if (!id || !url && !pathDep && !registry) throw new Error("unsupported Swift package identity or source");
|
|
411
|
+
const selector = arg.match(/(?:branch|revision):\s*["']([^"']+)["']/)?.[1];
|
|
412
|
+
const requirement = arg.replace(/(?:name|url|path|id):\s*"(?:\\.|[^"\\])*"\s*,?\s*/g, "").trim() || null;
|
|
413
|
+
addDependency(deps, "swiftpm", id, rel, { alias: null, section: "dependencies", requirement, features: [], defaultFeatures: null, resolvedVersion: null, local: !!pathDep, source: pathDep ? `path:${pathDep}` : registry ? "registry" : `git+${url}${selector ? `#${selector}` : ""}` });
|
|
322
414
|
if (pathDep) {
|
|
323
415
|
resolveLocalPath(appRoot, rel, pathDep, coverage, rel, inventory, "local-swift-dependency", id);
|
|
324
416
|
}
|
|
325
417
|
}
|
|
326
418
|
}
|
|
327
419
|
}
|
|
420
|
+
resolveDependencyVersions(appRoot, inventory, deps, coverage, contentDigests);
|
|
328
421
|
Object.defineProperty(deps, "coverage", { value: coverage, enumerable: false });
|
|
329
422
|
Object.defineProperty(deps, "contentDigests", { value: contentDigests, enumerable: false });
|
|
423
|
+
Object.defineProperty(deps, "manifests", { value: files, enumerable: false });
|
|
330
424
|
return deps;
|
|
331
425
|
}
|
|
332
426
|
|
|
@@ -419,31 +513,23 @@ function trustedProvenance(appRoot, inventory, trust, repositoryIdentity, covera
|
|
|
419
513
|
return { excluded, classifications };
|
|
420
514
|
}
|
|
421
515
|
|
|
422
|
-
function npmImportUsage(inventory) {
|
|
423
|
-
const usage = new Map(), qaSegments = new Set(["test", "tests", "__tests__", "e2e", "qa", "fixtures"]);
|
|
424
|
-
for (const { rel, bytes } of inventory.files) {
|
|
425
|
-
if (!CODE_EXT.has(path.posix.extname(rel).toLowerCase()) || !bytes) continue;
|
|
426
|
-
const qaPath = rel.split("/").some((segment) => qaSegments.has(segment)) || /(?:\.(?:test|spec)\.|^qa-.*\.)/.test(path.posix.basename(rel));
|
|
427
|
-
for (const match of bytes.toString("utf8").matchAll(/\bimport\b\s*(?:[\w$*{},\s]+?\bfrom\s*)?["']([^"']+)["']|\b(?:import|require)\s*\(\s*["']([^"']+)["']/g)) {
|
|
428
|
-
const specifier = match[1] ?? match[2];
|
|
429
|
-
const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
|
|
430
|
-
usage.set(name, (usage.get(name) ?? true) && qaPath);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
return usage;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
516
|
function scanInternal(appRoot, manifest, { inventory = fileInventory(appRoot), trust, repositoryIdentity, repositoryRoot = appRoot, pathPrefix = "" } = {}) {
|
|
437
517
|
const findings = [], coverage = [...inventory.coverage], deps = dependenciesFromInventory(appRoot, inventory), contentDigests = [...(deps.contentDigests ?? [])];
|
|
518
|
+
const catalogOwners = new Set(manifest.capabilities.map((entry) => entry.package));
|
|
438
519
|
coverage.push(...(deps.coverage ?? []));
|
|
439
520
|
const provenance = trustedProvenance(appRoot, inventory, trust, repositoryIdentity, coverage, repositoryRoot, pathPrefix);
|
|
440
|
-
const
|
|
521
|
+
const pushDependency = (c, f, d, kind = "dependency") => findings.push({ fingerprint: f.id, capability: c.capability, package: c.package, ...findingPolicy(c, f), file: d.file, line: 0, kind, ecosystem: d.ecosystem, name: d.name,
|
|
522
|
+
...(kind === "dependency" ? { alias: d.alias, section: d.section, requirement: d.requirement, features: d.features, defaultFeatures: d.defaultFeatures, local: d.local } : { lockfile: d.lockfile, dependencyPath: d.dependencyPath }),
|
|
523
|
+
source: d.source, resolvedVersion: d.resolvedVersion, text: `${d.ecosystem} dependency ${d.alias ? `${d.alias} aliases ` : ""}${d.name}`, provenance: d.source });
|
|
441
524
|
for (const c of manifest.capabilities) for (const f of c.fingerprints.filter((x) => x.type === "dependency")) for (const d of deps) {
|
|
442
|
-
if (d.ecosystem === f.ecosystem && (d.name === f.name || d.alias === f.name)) {
|
|
443
|
-
|
|
444
|
-
findings.push({ fingerprint: f.id, capability: c.capability, package: c.package, file: d.file, line: 0, text: `${d.ecosystem} dependency ${d.alias ? `${d.alias} aliases ` : ""}${d.name}`, local: d.local, provenance: d.source, ...(qaOnly ? { qaOnly: true } : {}) });
|
|
525
|
+
if (d.ecosystem === f.ecosystem && (d.name === f.name || d.alias === f.name) && !catalogOwners.has(d.name)) {
|
|
526
|
+
pushDependency(c, f, d);
|
|
445
527
|
}
|
|
446
528
|
}
|
|
529
|
+
const blockRules = manifest.capabilities.flatMap((c) => c.fingerprints.filter((f) => f.type === "dependency" && findingPolicy(c, f).tier === "block").map((f) => ({ c, f })));
|
|
530
|
+
const transitiveOwners = new Set([...catalogOwners, ...(manifest.internalOwners ?? []).map((owner) => typeof owner === "string" ? owner : owner.package)]);
|
|
531
|
+
const transitive = transitiveDependencies(appRoot, inventory, deps.manifests, transitiveOwners, coverage, contentDigests, blockRules.map(({ f }) => `${f.ecosystem}:${f.name}`));
|
|
532
|
+
for (const { c, f } of blockRules) for (const d of transitive) if (d.ecosystem === f.ecosystem && d.name === f.name) pushDependency(c, f, d, "transitive-dependency");
|
|
447
533
|
const matchers = sourceMatchers(manifest);
|
|
448
534
|
for (const file of inventory.files) {
|
|
449
535
|
const { rel, size, bytes } = file;
|
|
@@ -458,7 +544,7 @@ function scanInternal(appRoot, manifest, { inventory = fileInventory(appRoot), t
|
|
|
458
544
|
}
|
|
459
545
|
if (pathPrefix) {
|
|
460
546
|
const prefix = (p) => p === "." ? pathPrefix.slice(0, -1) : `${pathPrefix}${p}`;
|
|
461
|
-
for (const f of findings) f.file = prefix(f.file);
|
|
547
|
+
for (const f of findings) { f.file = prefix(f.file); if (f.lockfile) f.lockfile = prefix(f.lockfile); }
|
|
462
548
|
for (const c of coverage) c.path = prefix(c.path);
|
|
463
549
|
for (const d of deps) d.file = prefix(d.file);
|
|
464
550
|
for (const d of contentDigests) d.path = prefix(d.path);
|
|
@@ -473,26 +559,32 @@ function trustForScan(options = {}) {
|
|
|
473
559
|
try { return loadTrustedExternal().trust; } catch { return null; }
|
|
474
560
|
}
|
|
475
561
|
|
|
476
|
-
function resolveSharedAuthority(trust) {
|
|
562
|
+
function resolveSharedAuthority(trust, readAuthority = readCommittedKeyAuthority) {
|
|
477
563
|
if (!trust?.keyAuthority) return { trust };
|
|
478
|
-
const authority =
|
|
564
|
+
const authority = readAuthority(trust);
|
|
479
565
|
if (!authority.ok) throw new Error(`protected ownership key authority failed: ${authority.problems.join("; ")}`);
|
|
480
566
|
if (!authority.resolvedTrust.ownershipKeys.length) throw new Error("protected ownership key authority has no active approval keys");
|
|
481
567
|
return { trust: authority.resolvedTrust, keyLogDigest: authority.digest, keyCheckpoint: authority.keyCheckpoint, rootFingerprint: authority.rootFingerprint, keyAuthority: authority.authority };
|
|
482
568
|
}
|
|
483
569
|
|
|
484
|
-
function resolveRepositoryKeyLog(trust, repositoryRoot, repositoryIdentity) {
|
|
570
|
+
function resolveRepositoryKeyLog(trust, repositoryRoot, repositoryIdentity, readAuthority = readCommittedKeyAuthority) {
|
|
485
571
|
if (!trust?.rootFingerprint || trust.keyAuthority) return { trust };
|
|
486
|
-
const authority =
|
|
572
|
+
const authority = readAuthority({ ...trust, keyAuthority: { repositoryIdentity, checkoutPath: repositoryRoot } });
|
|
487
573
|
if (!authority.ok) throw new Error(`protected ownership key log failed: ${authority.problems.join("; ")}`);
|
|
488
574
|
if (!authority.resolvedTrust.ownershipKeys.length) throw new Error("protected ownership key log has no active approval keys");
|
|
489
575
|
return { trust: authority.resolvedTrust, keyLogDigest: authority.digest, keyCheckpoint: authority.keyCheckpoint, rootFingerprint: authority.rootFingerprint };
|
|
490
576
|
}
|
|
491
577
|
|
|
492
578
|
export function scanApp(appRoot, manifest, options = {}) {
|
|
579
|
+
return scanAppInternal(appRoot, manifest, options);
|
|
580
|
+
}
|
|
581
|
+
export function scanAppForTest(appRoot, manifest, options = {}) {
|
|
582
|
+
return scanAppInternal(appRoot, manifest, options, options.readAuthorityForTest);
|
|
583
|
+
}
|
|
584
|
+
function scanAppInternal(appRoot, manifest, options, readAuthority) {
|
|
493
585
|
const initialTrust = trustForScan(options), location = scanRepositoryRoot(appRoot);
|
|
494
586
|
const repositoryIdentity = repositoryIdentityFor(appRoot);
|
|
495
|
-
const authority = initialTrust?.keyAuthority ? resolveSharedAuthority(initialTrust) : resolveRepositoryKeyLog(initialTrust, location.root, repositoryIdentity);
|
|
587
|
+
const authority = initialTrust?.keyAuthority ? resolveSharedAuthority(initialTrust, readAuthority) : resolveRepositoryKeyLog(initialTrust, location.root, repositoryIdentity, readAuthority);
|
|
496
588
|
const trust = authority.trust;
|
|
497
589
|
const result = scanInternal(appRoot, manifest, { inventory: fileInventory(appRoot, trust, location.prefix, location.root), trust, repositoryIdentity, repositoryRoot: location.root, pathPrefix: location.prefix });
|
|
498
590
|
Object.defineProperty(result.findings, "coverage", { value: result.coverage, enumerable: false });
|
|
@@ -522,52 +614,68 @@ function enrollmentPending(trust, state) {
|
|
|
522
614
|
return !keys.every((key) => key?.keyId && key.publicKeyPem && !trust.revokedKeyIds?.includes(key.keyId)) || keys[0]?.keyId === keys[1]?.keyId || keys[0]?.publicKeyPem === keys[1]?.publicKeyPem;
|
|
523
615
|
}
|
|
524
616
|
|
|
617
|
+
/** Effective enforcement policy; a fingerprint without a tier is treated as `block` (fail closed). */
|
|
618
|
+
function findingPolicy(c, f) {
|
|
619
|
+
return { tier: f.tier ?? c.tier ?? "block", ...(f.policy ? { policy: f.policy } : {}) };
|
|
620
|
+
}
|
|
525
621
|
export function evaluate(findings, exceptions, options = {}) {
|
|
526
622
|
const today = options.today ?? new Date().toISOString().slice(0, 10), problems = [], covered = [], violations = [], warnings = [];
|
|
527
623
|
const metadata = options.metadata ?? findings.metadata ?? {};
|
|
528
624
|
const catalogDigest = metadata.catalogDigest ?? options.catalogDigest ?? "";
|
|
529
625
|
const identity = options.repositoryIdentity ?? metadata.repositoryIdentity ?? "";
|
|
530
626
|
for (const e of exceptions) {
|
|
531
|
-
const p = e
|
|
532
|
-
const where = `exception #${e
|
|
533
|
-
const missing = ["fingerprint", "
|
|
627
|
+
const p = e?.payload ?? e ?? {};
|
|
628
|
+
const where = `exception #${e?.index} (${p.fingerprint ?? "?"})`;
|
|
629
|
+
const missing = ["fingerprint", "occurrences", "policyDigest", "minimumScannerVersion", "minimumCatalogVersion", "issued", "reason", "owner", "expires", "requestId"].filter((k) => !p[k] || (Array.isArray(p[k]) && !p[k].length));
|
|
534
630
|
if (missing.length) { problems.push(`${where}: missing ${missing.join(", ")}`); continue; }
|
|
535
631
|
const approvedBy = e.approvedBy ?? p.approvedBy;
|
|
536
632
|
if (approvedBy !== undefined && approvedBy !== "Adrian") { problems.push(`${where}: approvedBy must be Adrian`); continue; }
|
|
537
633
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(p.expires) || Number.isNaN(Date.parse(`${p.expires}T00:00:00Z`)) || p.expires < today) { problems.push(`${where}: invalid or expired expires date`); continue; }
|
|
538
634
|
if (!p.trackingRef || typeof p.trackingRef !== "object" || !p.trackingRef.verified || !/^https:\/\//.test(p.trackingRef.url ?? "") || !p.trackingRef.id) { problems.push(`${where}: tracking must be a verified RightKit request reference`); continue; }
|
|
539
|
-
|
|
540
|
-
|
|
635
|
+
let policyDigest;
|
|
636
|
+
try { policyDigest = policyEntryDigest(options.manifest ?? installedManifest, p.fingerprint); } catch (error) { problems.push(`${where}: ${error.message}`); continue; }
|
|
637
|
+
const { index, ...record } = e;
|
|
638
|
+
const verification = verifyException(record, { repositoryIdentity: identity, catalogDigest, policyDigest, findings, trust: options.trust, today, scannerVersion: options.scannerVersion ?? metadata.scannerVersion ?? SCANNER_VERSION, catalogVersion: options.catalogVersion ?? metadata.catalogVersion });
|
|
541
639
|
if (!verification?.ok) { problems.push(`${where}: signature verification failed${verification?.problems?.length ? ` (${verification.problems.join("; ")})` : ""}`); continue; }
|
|
542
|
-
const matching = findings.filter((f) => f.fingerprint === p.fingerprint && p.
|
|
640
|
+
const matching = findings.filter((f) => f.fingerprint === p.fingerprint && p.occurrences.some((item) => item.path === f.file && item.hash === occurrenceHash(f)));
|
|
543
641
|
if (!matching.length) { problems.push(`${where}: stale, matches no signed occurrence`); continue; }
|
|
544
642
|
for (const f of matching) covered.push({ ...f, occurrenceHash: occurrenceHash(f), exception: e });
|
|
545
643
|
}
|
|
546
644
|
const pending = enrollmentPending(options.trust, options.trustEnrollment);
|
|
547
645
|
if (pending) warnings.push("approval enrollment pending; detected findings remain open backlog requests until distinct Windows & macOS keys are enrolled");
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
return { violations, covered, qaOnly, problems, warnings, enrollmentPending: pending };
|
|
646
|
+
for (const f of findings) if (!covered.some((x) => x.occurrenceHash === occurrenceHash(f))) violations.push({ ...f, occurrenceHash: occurrenceHash(f) });
|
|
647
|
+
return { violations, covered, problems, warnings, enrollmentPending: pending };
|
|
551
648
|
}
|
|
552
649
|
|
|
553
650
|
export function capabilityTable(manifest, deps, result) {
|
|
554
|
-
|
|
555
|
-
|
|
651
|
+
const groups = new Map();
|
|
652
|
+
for (const c of manifest.capabilities) {
|
|
653
|
+
if (!groups.has(c.capability)) groups.set(c.capability, []);
|
|
654
|
+
groups.get(c.capability).push(c.package);
|
|
655
|
+
}
|
|
656
|
+
return [...groups].map(([capability, owners]) => {
|
|
657
|
+
const owner = owners[0];
|
|
658
|
+
const declared = deps.some((d) => owners.includes(d.name) || owners.some((name) => d.name === name.replace(/^rightkit-/, "@rightkit/")));
|
|
556
659
|
const own = result.violations.filter((x) => x.capability === capability), covered = result.covered.filter((x) => x.capability === capability);
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
return { capability, owner, uses: declared, status, count: own.length + covered.length + qaOnly.length };
|
|
660
|
+
const status = own.length ? "blocked" : covered.length ? "excepted" : declared ? "declared-dependency" : result.coverage?.length ? "unscanned" : "not-detected";
|
|
661
|
+
return { capability, owner, ...(owners.length > 1 ? { owners } : {}), uses: declared, status, count: own.length + covered.length };
|
|
560
662
|
});
|
|
561
663
|
}
|
|
562
664
|
export function formatTable(rows) { return ["capability owner status", ...rows.map((x) => `${x.capability} ${x.owner} ${x.status}`)].join("\n"); }
|
|
563
665
|
|
|
564
666
|
export function checkApp(appRoot, manifest, options = {}) {
|
|
667
|
+
return checkAppInternal(appRoot, manifest, options);
|
|
668
|
+
}
|
|
669
|
+
export function checkAppForTest(appRoot, manifest, options = {}) {
|
|
670
|
+
return checkAppInternal(appRoot, manifest, options, options.readAuthorityForTest);
|
|
671
|
+
}
|
|
672
|
+
function checkAppInternal(appRoot, manifest, options, readAuthority) {
|
|
565
673
|
let trustState = options.trust;
|
|
566
674
|
if (!trustState) trustState = loadTrustedExternal();
|
|
567
675
|
const initialTrust = trustState && Object.prototype.hasOwnProperty.call(trustState, "trust") ? trustState.trust : trustState;
|
|
568
676
|
const repositoryLocation = scanRepositoryRoot(appRoot);
|
|
569
677
|
const repositoryIdentity = repositoryIdentityFor(appRoot);
|
|
570
|
-
const resolved = initialTrust?.keyAuthority ? resolveSharedAuthority(initialTrust) : resolveRepositoryKeyLog(initialTrust, repositoryLocation.root, repositoryIdentity);
|
|
678
|
+
const resolved = initialTrust?.keyAuthority ? resolveSharedAuthority(initialTrust, readAuthority) : resolveRepositoryKeyLog(initialTrust, repositoryLocation.root, repositoryIdentity, readAuthority);
|
|
571
679
|
const trust = resolved.trust;
|
|
572
680
|
const location = repositoryLocation;
|
|
573
681
|
const scanned = scanInternal(appRoot, manifest, { inventory: fileInventory(appRoot, trust, location.prefix, location.root), trust, repositoryIdentity, repositoryRoot: location.root, pathPrefix: location.prefix });
|
|
@@ -576,13 +684,13 @@ export function checkApp(appRoot, manifest, options = {}) {
|
|
|
576
684
|
const enrollment = trust?.rootFingerprint
|
|
577
685
|
? { enrolled: trust.enrollmentComplete === true, pending: trust.enrollmentComplete !== true }
|
|
578
686
|
: trustState?.pending !== undefined || trustState?.enrolled !== undefined ? trustState : undefined;
|
|
579
|
-
const result = evaluate(scanned.findings, loadExceptions(appRoot), { ...options, trust, trustEnrollment: enrollment, metadata });
|
|
687
|
+
const result = evaluate(scanned.findings, loadExceptions(appRoot), { ...options, manifest, trust, trustEnrollment: enrollment, metadata });
|
|
580
688
|
const coverage = [...new Map(metadata.coverage.map((x) => [`${x.path}\0${x.reason}`, x])).values()];
|
|
581
689
|
const rows = capabilityTable(manifest, scanned.deps, { ...result, coverage });
|
|
582
|
-
return { ok: result.enrollmentPending
|
|
690
|
+
return { ok: !result.enrollmentPending && !result.violations.some(isBlockingFinding) && !result.problems.length && !coverage.length, rows, ...result, findings: scanned.findings, coverage, metadata };
|
|
583
691
|
}
|
|
584
692
|
|
|
585
|
-
function checkCommitted(appRoot, manifest, options, acquire) {
|
|
693
|
+
function checkCommitted(appRoot, manifest, options, acquire, readAuthority = readCommittedKeyAuthority) {
|
|
586
694
|
let trustState = options.trust;
|
|
587
695
|
if (!trustState) trustState = loadTrustedExternal();
|
|
588
696
|
let trust = trustState && Object.prototype.hasOwnProperty.call(trustState, "trust") ? trustState.trust : trustState;
|
|
@@ -601,7 +709,7 @@ function checkCommitted(appRoot, manifest, options, acquire) {
|
|
|
601
709
|
if (!repositoryIdentity) throw new Error("committed ownership: repository origin is missing or invalid");
|
|
602
710
|
let keyLogDigest, keyCheckpoint, keyAuthorityBinding;
|
|
603
711
|
if (trust?.keyAuthority) {
|
|
604
|
-
const authority =
|
|
712
|
+
const authority = readAuthority(trust);
|
|
605
713
|
if (!authority.ok) throw new Error(`committed ownership key authority failed: ${authority.problems.join("; ")}`);
|
|
606
714
|
trust = authority.resolvedTrust;
|
|
607
715
|
if (!trust.ownershipKeys.length) throw new Error("protected ownership key authority has no active approval keys");
|
|
@@ -639,12 +747,12 @@ function checkCommitted(appRoot, manifest, options, acquire) {
|
|
|
639
747
|
pathBase: location.pathBase, pathPrefix: location.prefix, provenance: scanned.provenance, exceptionDigest,
|
|
640
748
|
source: "committed-git-tree", ...(keyLogDigest ? { keyLogDigest, keyCheckpoint, rootFingerprint: trust.rootFingerprint, ...(keyAuthorityBinding ? { keyAuthority: keyAuthorityBinding } : {}) } : {}),
|
|
641
749
|
};
|
|
642
|
-
const result = evaluate(scanned.findings, exceptions, { ...options, trust, trustEnrollment: trust?.rootFingerprint
|
|
750
|
+
const result = evaluate(scanned.findings, exceptions, { ...options, manifest, trust, trustEnrollment: trust?.rootFingerprint
|
|
643
751
|
? { enrolled: trust.enrollmentComplete === true, pending: trust.enrollmentComplete !== true }
|
|
644
752
|
: trustState?.pending !== undefined || trustState?.enrolled !== undefined ? trustState : undefined, metadata });
|
|
645
753
|
const coverage = [...new Map(metadata.coverage.map((x) => [`${x.path}\0${x.reason}`, x])).values()];
|
|
646
754
|
const rows = capabilityTable(manifest, scanned.deps, { ...result, coverage });
|
|
647
|
-
return { ok: !result.enrollmentPending && !result.violations.
|
|
755
|
+
return { ok: !result.enrollmentPending && !result.violations.some(isBlockingFinding) && !result.problems.length && !coverage.length, rows, ...result, findings: scanned.findings, coverage, metadata };
|
|
648
756
|
}
|
|
649
757
|
|
|
650
758
|
/** Protected production scan: acquire and inspect exact HEAD blobs without reading checkout contents. */
|
|
@@ -654,5 +762,5 @@ export function checkCommittedApp(appRoot, manifest, options = {}) {
|
|
|
654
762
|
|
|
655
763
|
/** Explicit test-only seam for bounded Git process failures and fixtures. */
|
|
656
764
|
export function checkCommittedAppForTest(appRoot, manifest, options = {}) {
|
|
657
|
-
return checkCommitted(appRoot, manifest, options, options.acquireCommittedSourceForTest ?? acquireCommittedSource);
|
|
765
|
+
return checkCommitted(appRoot, manifest, options, options.acquireCommittedSourceForTest ?? acquireCommittedSource, options.readAuthorityForTest);
|
|
658
766
|
}
|
package/package.json
CHANGED