magenta-canon 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/bin/magenta-canon.mjs +8 -1
- package/dist/MANIFEST.json +9 -0
- package/dist/scripts/magenta-sentinel.js +192 -0
- package/dist/server/sentinel/artifact-sentinel.js +286 -0
- package/dist/server/sentinel/promotion.js +58 -0
- package/dist/server/sentinel/repository-sentinel.js +204 -0
- package/dist/server/sentinel/rules.js +114 -0
- package/dist/server/sentinel/tar-reader.js +116 -0
- package/dist/server/sentinel/violation.js +73 -0
- package/dist/server/sentinel/witness-sentinel.js +408 -0
- package/dist/shared/invariants.js +755 -0
- package/docs/NPM_PACKAGING.md +24 -17
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ npx magenta-canon demo # full local proof loop: allow · block
|
|
|
22
22
|
npx magenta-canon verify --self-test # verifier self-check: all verdict levels incl. a tamper FAIL
|
|
23
23
|
npx magenta-canon gateway <config.json> # the stdio MCP capability gateway
|
|
24
24
|
npx magenta-canon mirror --self-test # external STH mirror: catch operator history rewrites
|
|
25
|
+
npx magenta-canon sentinel pack # Sentinel Mesh: invariant-check the packed artifact
|
|
25
26
|
```
|
|
26
27
|
|
|
27
28
|
Magenta Canon is a proven **reference implementation**, not production-hosted infra
|
|
@@ -221,6 +222,40 @@ still regenerates on restart; old evidence keeps verifying). Keystore format,
|
|
|
221
222
|
rotation protocol, boot-reconciliation table, and operator procedures:
|
|
222
223
|
[`docs/WITNESS_IDENTITY.md`](docs/WITNESS_IDENTITY.md) (ships in the package).
|
|
223
224
|
|
|
225
|
+
### Sentinel Mesh foundation — new in 0.6.0
|
|
226
|
+
|
|
227
|
+
Magenta Canon 0.6.0 introduces an **executable Sentinel Mesh** that protects
|
|
228
|
+
repository state, npm artifacts, dependency boundaries, release promotion,
|
|
229
|
+
witness identity, and cryptographically authorized witness-key rotation:
|
|
230
|
+
versioned invariants, deterministic repository / artifact / witness
|
|
231
|
+
evaluation, evidence-preserving violations, and scoped release-promotion
|
|
232
|
+
blocking.
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
magenta-canon sentinel repository # invariant-check a git repository
|
|
236
|
+
magenta-canon sentinel artifact <tgz|dir> # invariant-check a packed artifact
|
|
237
|
+
magenta-canon sentinel pack # npm pack, then check the result
|
|
238
|
+
magenta-canon sentinel witness --ledger ledger.jsonl # witness rotation-authority check
|
|
239
|
+
magenta-canon sentinel invariants # list the invariant registry
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The **Witness & Rotation Sentinel** (MC-WIT-001…011) watches the 0.5.0
|
|
243
|
+
rotation-authority model itself: pinned-anchor continuity, dual-signed
|
|
244
|
+
rotation validity, exact epoch boundaries, epoch-correct STH signing,
|
|
245
|
+
retired-key resurrection / pre-activation refusal, and keystore↔ledger
|
|
246
|
+
agreement — re-derived independently from the published wire formats, so it
|
|
247
|
+
can disagree with a wrong (or compromised) server.
|
|
248
|
+
|
|
249
|
+
Sentinels are read-only, redacting (paths + fingerprints, never secret
|
|
250
|
+
bodies), deterministic, network-free, and emit `magenta-sentinel-violation/1`
|
|
251
|
+
evidence records; exit codes map to a scoped promotion decision (`eligible` /
|
|
252
|
+
`blocked` / `requires-independent-review`). Records at this layer are
|
|
253
|
+
**unsigned local diagnostics**, and the freeze is a release/build promotion
|
|
254
|
+
gate — **not** a production kill switch, a hosted Sentinel service, or
|
|
255
|
+
complete Mesh coverage (ledger/execution/configuration sentinels are future
|
|
256
|
+
lanes). Honest scope and the full model:
|
|
257
|
+
[`docs/SENTINEL_MESH.md`](docs/SENTINEL_MESH.md).
|
|
258
|
+
|
|
224
259
|
### From the repo vs. as a CLI
|
|
225
260
|
|
|
226
261
|
- **From a repo checkout** (above): `npm install` then `npm run demo`.
|
package/bin/magenta-canon.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* magenta-canon — CLI entry point for the open-source reference implementation.
|
|
4
4
|
*
|
|
@@ -48,6 +48,8 @@ Usage:
|
|
|
48
48
|
verify <mirror>, check <mirror> <bundle>, --self-test.
|
|
49
49
|
magenta-canon mirror-feed ... One feed cycle for a mirror operator:
|
|
50
50
|
(--bundle <file> | --url <evidence-url>) --mirror <file>.
|
|
51
|
+
magenta-canon sentinel <cmd> Sentinel Mesh: repository | artifact <tgz|dir> |
|
|
52
|
+
pack | invariants (deterministic invariant checks).
|
|
51
53
|
magenta-canon --help Show this help.
|
|
52
54
|
magenta-canon --version Print the version.
|
|
53
55
|
|
|
@@ -110,6 +112,11 @@ switch (sub) {
|
|
|
110
112
|
// Plain-Node operator helper; delegates all mirror semantics to `mirror`.
|
|
111
113
|
run(process.execPath, [path.join(ROOT, "scripts", "mirror-feed.mjs"), ...rest], { cwd: process.cwd() });
|
|
112
114
|
break;
|
|
115
|
+
case "sentinel":
|
|
116
|
+
// Sentinel Mesh foundation: deterministic invariant evaluation over a
|
|
117
|
+
// repository or a packed artifact (read-only; unsigned local diagnostics).
|
|
118
|
+
runTs("scripts/magenta-sentinel.ts", rest, { cwd: process.cwd() });
|
|
119
|
+
break;
|
|
113
120
|
case "-v":
|
|
114
121
|
case "--version":
|
|
115
122
|
case "version":
|
package/dist/MANIFEST.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"scripts/demo-control-plane.js": "3a559f39d17c1a403988e731e053d0d071cbd41ab595fd6246910f852337ab2f",
|
|
4
4
|
"scripts/intake-cli.js": "189014db22d6fccb034ed1a93ec11efe8905be820bc468366c68bb2cabaf8a97",
|
|
5
5
|
"scripts/magenta-mirror.js": "cf701065f7a6e20f7f44a9b815396aeb5fe031c3f003bcd793b8014ea8801b85",
|
|
6
|
+
"scripts/magenta-sentinel.js": "0280d7584524201551c61a93beb1f2ee4964e76b0e44eb7c9d15a0f5a95cafdc",
|
|
6
7
|
"scripts/magenta-verify.js": "0bb65ef6ff1350789eecc4f0434ca94dcf3a89930f8ef1d62cc38100e18094ba",
|
|
7
8
|
"scripts/mcp-gateway.js": "335923004b73511fe42ea0fc6f2e567afe8018dc95464a79027e4c2537e30de1",
|
|
8
9
|
"server/agent-policy.js": "20dd9150f8244c33aaf14ecdf3a09f471210960d246cda9ab8d5abe0e8433518",
|
|
@@ -24,6 +25,13 @@
|
|
|
24
25
|
"server/ledger-store.js": "fc8a46f925908764bac7a076cffa08053af4c1f93237bee408eb0ed1e61eeca2",
|
|
25
26
|
"server/ledger.js": "9bf8f132e08d636070d2ede568b2eaf75810be245c90ecd5979d2d4de69af934",
|
|
26
27
|
"server/persistence.js": "2e6b1d0b1c74babbd581780b028c2593256c5ec96c2d60f4c25159e3f54e4e3f",
|
|
28
|
+
"server/sentinel/artifact-sentinel.js": "93bae917af313c9247f8062af148e7c25ccfd16d0507fa85e755c64056715f8d",
|
|
29
|
+
"server/sentinel/promotion.js": "68ac5e54eb8c07d5c75ba0d2325d595b1583557840cc46322b2053dd9924a0be",
|
|
30
|
+
"server/sentinel/repository-sentinel.js": "722e973c8860cf281b29db8c4a22ec3cf473afcb088247916e8278d5e11638cf",
|
|
31
|
+
"server/sentinel/rules.js": "ab24435df4f6f5b10f8b721fde37fe7fa894453648f17f04923cab56020c459f",
|
|
32
|
+
"server/sentinel/tar-reader.js": "ded811eefebf3e4afa332d1bc0a48318cc22ad824222eb15b2dc08eb51048272",
|
|
33
|
+
"server/sentinel/violation.js": "70d44ae5eb43a560fada399b31eac1a2beccb66916340e1f3e9b549731776b38",
|
|
34
|
+
"server/sentinel/witness-sentinel.js": "f024dbe9fb505c7520ec466ed31bf9b3284c3c3066dbd287121d2e4397d3fb0a",
|
|
27
35
|
"server/storage.js": "7a7556a6d15d736f028698b6094072daa71eed2230427594edac9d29b531081a",
|
|
28
36
|
"server/transparency-log.js": "7f238edc5cad4edc2c70a7b3e1977197cf2087e77789d181ffae0035f948391b",
|
|
29
37
|
"server/trust-bootstrap.js": "57a53a3ee9cd630ada2867e8b087a34b069ba26f86a696d3ead74888dd7e8d5f",
|
|
@@ -31,6 +39,7 @@
|
|
|
31
39
|
"server/witness.js": "b1a8209d4dae4ffafc3ca4a158474b369250472ba80cef03bd92048282be91a4",
|
|
32
40
|
"shared/canonical.js": "476cee4b5c5702e8a2a198e79c2639cf8ff84000ffb92f68a04433ed2a2f5484",
|
|
33
41
|
"shared/certificates.js": "7844e71a38e1b1324586c7d054b2f5c15222b86446318d1e6dea9bab504a4a73",
|
|
42
|
+
"shared/invariants.js": "414a2aac034c9b28d0e32fc4afefb9b9ab723ed465b4aba1f8da98dab08363d4",
|
|
34
43
|
"shared/schema.js": "f89190990e3826e44c722d5e8f5b39fd59b4d3fb9ec047229930a17604e4646c",
|
|
35
44
|
"shared/terminating-kernel.js": "8e2aa68d47d6780e4a70822482e3e410924e9d1af843f301a487da0af37ed047"
|
|
36
45
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
20
|
+
if (mod && mod.__esModule) return mod;
|
|
21
|
+
var result = {};
|
|
22
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
23
|
+
__setModuleDefault(result, mod);
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
/**
|
|
28
|
+
* magenta-sentinel — Sentinel Mesh CLI (foundation layer).
|
|
29
|
+
*
|
|
30
|
+
* magenta-canon sentinel repository [--root <dir>] [--json]
|
|
31
|
+
* magenta-canon sentinel artifact <tgz|package-dir> [--expected-version <v>] [--json]
|
|
32
|
+
* magenta-canon sentinel pack [--json] (npm pack, then evaluate the tarball)
|
|
33
|
+
*
|
|
34
|
+
* Exit codes: 0 eligible/diagnostic-only · 1 blocked · 2 requires-independent-review
|
|
35
|
+
* 3 usage / subject unreadable.
|
|
36
|
+
*
|
|
37
|
+
* Honesty: all records emitted here are UNSIGNED LOCAL DIAGNOSTICS. They are
|
|
38
|
+
* structured findings, not third-party-verifiable attestations.
|
|
39
|
+
*/
|
|
40
|
+
const node_child_process_1 = require("node:child_process");
|
|
41
|
+
const node_fs_1 = require("node:fs");
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
43
|
+
const repository_sentinel_1 = require("../server/sentinel/repository-sentinel");
|
|
44
|
+
const artifact_sentinel_1 = require("../server/sentinel/artifact-sentinel");
|
|
45
|
+
const witness_sentinel_1 = require("../server/sentinel/witness-sentinel");
|
|
46
|
+
const promotion_1 = require("../server/sentinel/promotion");
|
|
47
|
+
const invariants_1 = require("../shared/invariants");
|
|
48
|
+
const USAGE = `magenta-sentinel — deterministic invariant evaluation (Sentinel Mesh foundation)
|
|
49
|
+
|
|
50
|
+
Usage:
|
|
51
|
+
sentinel repository [--root <dir>] [--json]
|
|
52
|
+
sentinel artifact <tarball.tgz | package-dir> [--expected-version <v>] [--json]
|
|
53
|
+
sentinel pack [--json]
|
|
54
|
+
sentinel witness --ledger <file> [--keystore <file>] [--passphrase-env <VAR>]
|
|
55
|
+
[--anchor <pubkey-hex>] [--json]
|
|
56
|
+
sentinel invariants [--json]
|
|
57
|
+
|
|
58
|
+
Exit: 0 eligible/diagnostic-only · 1 blocked · 2 requires review · 3 usage/unreadable
|
|
59
|
+
`;
|
|
60
|
+
function flag(args, name) {
|
|
61
|
+
const i = args.indexOf(name);
|
|
62
|
+
if (i >= 0) {
|
|
63
|
+
args.splice(i, 1);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
function option(args, name) {
|
|
69
|
+
const i = args.indexOf(name);
|
|
70
|
+
if (i >= 0 && i + 1 < args.length) {
|
|
71
|
+
const v = args[i + 1];
|
|
72
|
+
args.splice(i, 2);
|
|
73
|
+
return v;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
function printHuman(kind, subject, result, promo) {
|
|
78
|
+
console.log(`magenta-sentinel ${kind}`);
|
|
79
|
+
console.log(` subject: ${subject}`);
|
|
80
|
+
console.log(` invariants evaluated: ${result.evaluated_invariants.length} — passed ${result.passed.length}, failed ${result.failed.length}`);
|
|
81
|
+
for (const f of result.findings) {
|
|
82
|
+
console.log(` [FAIL] ${f.invariant_id} (${f.severity}, ${f.disposition})`);
|
|
83
|
+
console.log(` observed: ${f.observed}`);
|
|
84
|
+
console.log(` paths: ${f.paths.join(", ") || "-"}`);
|
|
85
|
+
}
|
|
86
|
+
for (const v of result.violations) {
|
|
87
|
+
console.log(` evidence: ${v.invariant_id} record ${v.record_hash.slice(0, 16)}… (${v.assurance})`);
|
|
88
|
+
}
|
|
89
|
+
if (result.findings.length === 0)
|
|
90
|
+
console.log(" all evaluated invariants hold");
|
|
91
|
+
console.log(` PROMOTION: ${promo.decision.toUpperCase()}${promo.evidence_complete ? "" : " (evidence INCOMPLETE)"}`);
|
|
92
|
+
}
|
|
93
|
+
function exitFor(promo) {
|
|
94
|
+
switch (promo.decision) {
|
|
95
|
+
case "eligible":
|
|
96
|
+
case "diagnostic-only": return 0;
|
|
97
|
+
case "blocked": return 1;
|
|
98
|
+
case "requires-independent-review": return 2;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const argv = process.argv.slice(2);
|
|
102
|
+
const sub = argv.shift();
|
|
103
|
+
const json = flag(argv, "--json");
|
|
104
|
+
try {
|
|
105
|
+
if (sub === "repository") {
|
|
106
|
+
const root = option(argv, "--root") ?? process.cwd();
|
|
107
|
+
const res = (0, repository_sentinel_1.runRepositorySentinel)({ repoRoot: path.resolve(root) });
|
|
108
|
+
const promo = (0, promotion_1.decidePromotion)({ subject: `repository:${res.subject_identity}`, findings: res.findings, violations: res.violations });
|
|
109
|
+
if (json)
|
|
110
|
+
console.log(JSON.stringify({ result: res, promotion: promo }, null, 2));
|
|
111
|
+
else
|
|
112
|
+
printHuman("repository", res.subject_identity, res, promo);
|
|
113
|
+
process.exit(exitFor(promo));
|
|
114
|
+
}
|
|
115
|
+
else if (sub === "artifact") {
|
|
116
|
+
const target = argv.find((a) => !a.startsWith("-"));
|
|
117
|
+
if (!target) {
|
|
118
|
+
console.error("sentinel artifact: missing <tarball|dir>\n\n" + USAGE);
|
|
119
|
+
process.exit(3);
|
|
120
|
+
}
|
|
121
|
+
const expectedVersion = option(argv, "--expected-version");
|
|
122
|
+
const res = (0, artifact_sentinel_1.runArtifactSentinel)(path.resolve(target), { expectedVersion });
|
|
123
|
+
const promo = (0, promotion_1.decidePromotion)({ subject: `artifact:${res.subject_identity}`, findings: res.findings, violations: res.violations });
|
|
124
|
+
if (json)
|
|
125
|
+
console.log(JSON.stringify({ result: res, promotion: promo }, null, 2));
|
|
126
|
+
else
|
|
127
|
+
printHuman("artifact", `${res.artifact_name ?? "?"}@${res.artifact_version ?? "?"} (${res.subject_identity.slice(0, 19)}…, ${res.file_count} files)`, res, promo);
|
|
128
|
+
process.exit(exitFor(promo));
|
|
129
|
+
}
|
|
130
|
+
else if (sub === "pack") {
|
|
131
|
+
const out = (0, node_child_process_1.execFileSync)("npm", ["pack", "--silent"], { cwd: process.cwd(), encoding: "utf8", shell: process.platform === "win32" });
|
|
132
|
+
const tgz = out.trim().split(/\r?\n/).pop();
|
|
133
|
+
if (!(0, node_fs_1.existsSync)(tgz)) {
|
|
134
|
+
console.error(`sentinel pack: npm pack did not produce ${tgz}`);
|
|
135
|
+
process.exit(3);
|
|
136
|
+
}
|
|
137
|
+
const res = (0, artifact_sentinel_1.runArtifactSentinel)(path.resolve(tgz));
|
|
138
|
+
const promo = (0, promotion_1.decidePromotion)({ subject: `artifact:${res.subject_identity}`, findings: res.findings, violations: res.violations });
|
|
139
|
+
if (json)
|
|
140
|
+
console.log(JSON.stringify({ result: res, promotion: promo, tarball: tgz }, null, 2));
|
|
141
|
+
else {
|
|
142
|
+
console.log(` packed: ${tgz}`);
|
|
143
|
+
printHuman("artifact", `${res.artifact_name}@${res.artifact_version}`, res, promo);
|
|
144
|
+
}
|
|
145
|
+
process.exit(exitFor(promo));
|
|
146
|
+
}
|
|
147
|
+
else if (sub === "witness") {
|
|
148
|
+
const ledger = option(argv, "--ledger");
|
|
149
|
+
if (!ledger) {
|
|
150
|
+
console.error("sentinel witness: missing --ledger <file>\n\n" + USAGE);
|
|
151
|
+
process.exit(3);
|
|
152
|
+
}
|
|
153
|
+
const keystore = option(argv, "--keystore");
|
|
154
|
+
const res = (0, witness_sentinel_1.runWitnessSentinel)({
|
|
155
|
+
ledgerPath: path.resolve(ledger),
|
|
156
|
+
keystorePath: keystore ? path.resolve(keystore) : undefined,
|
|
157
|
+
passphraseEnv: option(argv, "--passphrase-env"),
|
|
158
|
+
anchorPubkey: option(argv, "--anchor"),
|
|
159
|
+
});
|
|
160
|
+
const promo = (0, promotion_1.decidePromotion)({ subject: `witness:${res.subject_identity}`, findings: res.findings, violations: res.violations });
|
|
161
|
+
if (json)
|
|
162
|
+
console.log(JSON.stringify({ result: res, promotion: promo }, null, 2));
|
|
163
|
+
else {
|
|
164
|
+
printHuman("witness", `${res.subject_identity.slice(0, 27)}… (${res.ledger_sth_count} STH, ${res.ledger_rotation_count} rotation(s), tip ${res.tip_fingerprint ?? "-"})`, res, promo);
|
|
165
|
+
for (const n of res.not_evaluated)
|
|
166
|
+
console.log(` [NOT EVALUATED] ${n.invariant_id}: ${n.reason}`);
|
|
167
|
+
if (res.incomplete_tail_ignored)
|
|
168
|
+
console.log(" note: incomplete trailing ledger line ignored (crash-tail semantics)");
|
|
169
|
+
}
|
|
170
|
+
process.exit(exitFor(promo));
|
|
171
|
+
}
|
|
172
|
+
else if (sub === "invariants") {
|
|
173
|
+
if (json)
|
|
174
|
+
console.log(JSON.stringify(invariants_1.INVARIANTS, null, 2));
|
|
175
|
+
else
|
|
176
|
+
for (const i of invariants_1.INVARIANTS)
|
|
177
|
+
console.log(`${i.id} v${i.version} ${i.status.padEnd(9)} ${i.severity.padEnd(8)} ${i.disposition.padEnd(26)} ${i.name}`);
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
process.stdout.write(USAGE);
|
|
182
|
+
process.exit(sub === undefined || sub === "--help" || sub === "-h" ? 0 : 3);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
if (e instanceof artifact_sentinel_1.TarError) {
|
|
187
|
+
console.error(`sentinel: artifact unreadable (refusing to guess): ${e.message}`);
|
|
188
|
+
process.exit(3);
|
|
189
|
+
}
|
|
190
|
+
console.error(`sentinel: ${e.message}`);
|
|
191
|
+
process.exit(3);
|
|
192
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.TarError = exports.runArtifactSentinel = exports.ARTIFACT_SENTINEL_VERSION = exports.ARTIFACT_SENTINEL_TYPE = void 0;
|
|
27
|
+
/**
|
|
28
|
+
* ARTIFACT SENTINEL — deterministic evaluation of artifact/dependency
|
|
29
|
+
* invariants (MC-ART-*, MC-DEP-*) over a packed npm artifact.
|
|
30
|
+
*
|
|
31
|
+
* Three subject modes:
|
|
32
|
+
* - tarball: a .tgz file (read in-memory via the hardened tar reader);
|
|
33
|
+
* - directory: an extracted package directory (or any package root);
|
|
34
|
+
* - the current `npm pack` output (the CLI wires this; the sentinel itself
|
|
35
|
+
* only ever sees a tarball path or directory path).
|
|
36
|
+
*
|
|
37
|
+
* Read-only (MC-REL-004), no network, deterministic, redacting. It does NOT
|
|
38
|
+
* claim npm-registry origin: subject identity is the sha256 the sentinel
|
|
39
|
+
* computed locally; binding that to a registry artifact requires registry
|
|
40
|
+
* metadata supplied and verified independently.
|
|
41
|
+
*/
|
|
42
|
+
const node_crypto_1 = require("node:crypto");
|
|
43
|
+
const node_fs_1 = require("node:fs");
|
|
44
|
+
const path = __importStar(require("node:path"));
|
|
45
|
+
const invariants_1 = require("../../shared/invariants");
|
|
46
|
+
const violation_1 = require("./violation");
|
|
47
|
+
const tar_reader_1 = require("./tar-reader");
|
|
48
|
+
Object.defineProperty(exports, "TarError", { enumerable: true, get: function () { return tar_reader_1.TarError; } });
|
|
49
|
+
const rules_1 = require("./rules");
|
|
50
|
+
exports.ARTIFACT_SENTINEL_TYPE = "artifact-sentinel";
|
|
51
|
+
exports.ARTIFACT_SENTINEL_VERSION = "1.0.0";
|
|
52
|
+
function finding(invId, observed, expected, paths) {
|
|
53
|
+
const inv = (0, invariants_1.getInvariant)(invId);
|
|
54
|
+
return {
|
|
55
|
+
invariant_id: inv.id, invariant_version: inv.version, severity: inv.severity,
|
|
56
|
+
authority_class: inv.authorityClass, disposition: inv.disposition,
|
|
57
|
+
observed, expected, paths: paths.sort(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function loadTarball(tgzPath) {
|
|
61
|
+
const buf = (0, node_fs_1.readFileSync)(tgzPath);
|
|
62
|
+
const sha256 = (0, node_crypto_1.createHash)("sha256").update(buf).digest("hex");
|
|
63
|
+
const entries = (0, tar_reader_1.readTgz)(buf);
|
|
64
|
+
const suspicious = entries.filter((e) => e.suspiciousPath).map((e) => e.name);
|
|
65
|
+
const files = entries
|
|
66
|
+
.filter((e) => e.typeflag === "0" || e.typeflag === "\0")
|
|
67
|
+
.map((e) => ({ rel: e.name.replace(/^package\//, ""), content: e.content }));
|
|
68
|
+
return { files, sha256, suspicious };
|
|
69
|
+
}
|
|
70
|
+
function walkDir(root) {
|
|
71
|
+
const out = [];
|
|
72
|
+
const stack = [root];
|
|
73
|
+
while (stack.length) {
|
|
74
|
+
const dir = stack.pop();
|
|
75
|
+
for (const name of (0, node_fs_1.readdirSync)(dir).sort()) {
|
|
76
|
+
const abs = path.join(dir, name);
|
|
77
|
+
const st = (0, node_fs_1.statSync)(abs);
|
|
78
|
+
if (st.isDirectory()) {
|
|
79
|
+
if (name === "node_modules" || name === ".git")
|
|
80
|
+
continue;
|
|
81
|
+
stack.push(abs);
|
|
82
|
+
}
|
|
83
|
+
else if (st.isFile()) {
|
|
84
|
+
out.push(abs);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out.sort();
|
|
89
|
+
}
|
|
90
|
+
function loadDirectory(dirPath) {
|
|
91
|
+
const abs = walkDir(dirPath);
|
|
92
|
+
const files = abs.map((a) => ({
|
|
93
|
+
rel: path.relative(dirPath, a).split(path.sep).join("/"),
|
|
94
|
+
content: (0, node_fs_1.readFileSync)(a),
|
|
95
|
+
}));
|
|
96
|
+
// Deterministic directory identity: hash of sorted (path, sha256) pairs.
|
|
97
|
+
const h = (0, node_crypto_1.createHash)("sha256");
|
|
98
|
+
for (const f of files) {
|
|
99
|
+
h.update(f.rel);
|
|
100
|
+
h.update("\0");
|
|
101
|
+
h.update((0, node_crypto_1.createHash)("sha256").update(f.content).digest());
|
|
102
|
+
h.update("\0");
|
|
103
|
+
}
|
|
104
|
+
return { files, sha256: `dir:${h.digest("hex")}` };
|
|
105
|
+
}
|
|
106
|
+
function runArtifactSentinel(subjectPath, opts = {}) {
|
|
107
|
+
if (!(0, node_fs_1.existsSync)(subjectPath)) {
|
|
108
|
+
throw new Error(`artifact-sentinel: subject not found: ${subjectPath}`);
|
|
109
|
+
}
|
|
110
|
+
const isDir = (0, node_fs_1.statSync)(subjectPath).isDirectory();
|
|
111
|
+
const findings = [];
|
|
112
|
+
let files;
|
|
113
|
+
let subjectIdentity;
|
|
114
|
+
const mode = isDir ? "directory" : "tarball";
|
|
115
|
+
if (isDir) {
|
|
116
|
+
({ files, sha256: subjectIdentity } = loadDirectory(subjectPath));
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const loaded = loadTarball(subjectPath); // throws TarError on malformed/bomb
|
|
120
|
+
files = loaded.files;
|
|
121
|
+
subjectIdentity = loaded.sha256;
|
|
122
|
+
if (loaded.suspicious.length > 0) {
|
|
123
|
+
// No invariant ID needed to refuse this: a traversal-shaped entry means
|
|
124
|
+
// the archive is not an honest npm artifact. Reported under MC-ART-007
|
|
125
|
+
// (manifest/files integrity) as a structural integrity failure.
|
|
126
|
+
findings.push(finding("MC-ART-007", `archive contains suspicious entry path(s): ${loaded.suspicious.join(", ")}`, "all archive entry paths are relative and traversal-free", loaded.suspicious));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const fileMap = new Map(files.map((f) => [f.rel, f]));
|
|
130
|
+
const relPaths = files.map((f) => f.rel);
|
|
131
|
+
// package.json metadata
|
|
132
|
+
let pkg = null;
|
|
133
|
+
const pkgFile = fileMap.get("package.json");
|
|
134
|
+
if (pkgFile) {
|
|
135
|
+
try {
|
|
136
|
+
pkg = JSON.parse(pkgFile.content.toString("utf8"));
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
pkg = null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const evaluatedDomains = new Set(["artifact", "dependency"]);
|
|
143
|
+
const evaluated = (0, invariants_1.ratifiedInvariants)().filter((i) => evaluatedDomains.has(i.domain)).map((i) => i.id);
|
|
144
|
+
// ── MC-ART-001 / MC-ART-002: secrets & identities ─────────────────────
|
|
145
|
+
const pemHits = [];
|
|
146
|
+
const prints = [];
|
|
147
|
+
for (const f of files) {
|
|
148
|
+
if (f.content.length > rules_1.MAX_SCAN_BYTES || (0, rules_1.isProbablyBinary)(f.content))
|
|
149
|
+
continue;
|
|
150
|
+
const text = f.content.toString("utf8");
|
|
151
|
+
if (rules_1.PEM_PRIVATE_KEY_WITH_BODY.test(text)) {
|
|
152
|
+
pemHits.push(f.rel);
|
|
153
|
+
prints.push(`${f.rel}=${(0, rules_1.matchFingerprint)(text, rules_1.PEM_PRIVATE_KEY_WITH_BODY)}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (pemHits.length > 0) {
|
|
157
|
+
findings.push(finding("MC-ART-001", `PEM private-key material in artifact; fingerprints: ${prints.join(", ")}`, "artifact contains no private-key material", pemHits));
|
|
158
|
+
}
|
|
159
|
+
const identityHits = relPaths.filter((p) => rules_1.FORBIDDEN_IDENTITY_PATHS.some((re) => re.test(p)));
|
|
160
|
+
if (identityHits.length > 0) {
|
|
161
|
+
findings.push(finding("MC-ART-002", "generated identity / keystore entry in artifact", "no identity or keystore entries", identityHits));
|
|
162
|
+
}
|
|
163
|
+
// ── MC-ART-003: tests ──────────────────────────────────────────────────
|
|
164
|
+
const testHits = relPaths.filter((p) => rules_1.TEST_FILE_PATHS.some((re) => re.test(p)));
|
|
165
|
+
if (testHits.length > 0) {
|
|
166
|
+
findings.push(finding("MC-ART-003", "test file(s) in artifact", "no test files ship", testHits));
|
|
167
|
+
}
|
|
168
|
+
// ── MC-ART-004: hosted plane (+ legacy heartbeat surfaces) ────────────
|
|
169
|
+
const hostedHits = relPaths.filter((p) => rules_1.HOSTED_PLANE_PATHS.some((re) => re.test(p)) || rules_1.FORBIDDEN_HEARTBEAT_PATHS.some((re) => re.test(p)));
|
|
170
|
+
if (hostedHits.length > 0) {
|
|
171
|
+
findings.push(finding("MC-ART-004", "hosted-only / legacy module(s) in artifact", "no hosted-plane or legacy heartbeat modules ship", hostedHits));
|
|
172
|
+
}
|
|
173
|
+
// ── MC-ART-005: runtime TS / source maps ──────────────────────────────
|
|
174
|
+
const tsHits = relPaths.filter((p) => rules_1.RUNTIME_TS_PATH.test(p) && !rules_1.TYPE_DECLARATION_PATH.test(p));
|
|
175
|
+
const mapHits = relPaths.filter((p) => rules_1.SOURCE_MAP_PATH.test(p));
|
|
176
|
+
if (tsHits.length + mapHits.length > 0) {
|
|
177
|
+
findings.push(finding("MC-ART-005", "runtime TypeScript or source map(s) in artifact", "precompiled JS only; no .ts (non-decl) or .map", [...tsHits, ...mapHits]));
|
|
178
|
+
}
|
|
179
|
+
// ── MC-ART-006: required entrypoints ──────────────────────────────────
|
|
180
|
+
const missingEntry = rules_1.REQUIRED_DIST_ENTRYPOINTS.filter((e) => !fileMap.has(e));
|
|
181
|
+
if (missingEntry.length > 0) {
|
|
182
|
+
findings.push(finding("MC-ART-006", `missing compiled entrypoint(s): ${missingEntry.join(", ")}`, "all required compiled entrypoints present", missingEntry));
|
|
183
|
+
}
|
|
184
|
+
// ── MC-ART-007: manifest equality ─────────────────────────────────────
|
|
185
|
+
const manifestFile = fileMap.get("dist/MANIFEST.json");
|
|
186
|
+
if (!manifestFile) {
|
|
187
|
+
findings.push(finding("MC-ART-007", "dist/MANIFEST.json missing", "manifest present and matching", ["dist/MANIFEST.json"]));
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
try {
|
|
191
|
+
const manifest = JSON.parse(manifestFile.content.toString("utf8"));
|
|
192
|
+
// build-lean writes a flat { "<dist-relative path>": "<sha256>" } map.
|
|
193
|
+
const listed = typeof manifest.files === "object" && manifest.files !== null ? manifest.files : manifest;
|
|
194
|
+
const distFiles = relPaths.filter((p) => p.startsWith("dist/") && p !== "dist/MANIFEST.json");
|
|
195
|
+
const bad = [];
|
|
196
|
+
for (const [rel, hash] of Object.entries(listed)) {
|
|
197
|
+
const f = fileMap.get(`dist/${rel}`);
|
|
198
|
+
if (!f) {
|
|
199
|
+
bad.push(`missing:dist/${rel}`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const actual = (0, node_crypto_1.createHash)("sha256").update(f.content).digest("hex");
|
|
203
|
+
if (actual !== hash)
|
|
204
|
+
bad.push(`hash-mismatch:dist/${rel}`);
|
|
205
|
+
}
|
|
206
|
+
for (const p of distFiles) {
|
|
207
|
+
const rel = p.replace(/^dist\//, "");
|
|
208
|
+
if (!(rel in listed))
|
|
209
|
+
bad.push(`unlisted:${p}`);
|
|
210
|
+
}
|
|
211
|
+
if (bad.length > 0) {
|
|
212
|
+
findings.push(finding("MC-ART-007", `manifest disagreement: ${bad.join(", ")}`, "dist/MANIFEST.json lists exactly the dist files with matching sha256", bad));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
findings.push(finding("MC-ART-007", "dist/MANIFEST.json unparseable", "manifest present and matching", ["dist/MANIFEST.json"]));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// ── MC-ART-008: version consistency ───────────────────────────────────
|
|
220
|
+
const artifactVersion = pkg?.version ?? null;
|
|
221
|
+
if (!pkg) {
|
|
222
|
+
findings.push(finding("MC-ART-008", "package.json missing or unparseable", "artifact carries valid package metadata", ["package.json"]));
|
|
223
|
+
}
|
|
224
|
+
else if (opts.expectedVersion && artifactVersion !== opts.expectedVersion) {
|
|
225
|
+
findings.push(finding("MC-ART-008", `artifact version ${artifactVersion} != expected ${opts.expectedVersion}`, "artifact version equals the version under evaluation", ["package.json"]));
|
|
226
|
+
}
|
|
227
|
+
// ── MC-DEP-*: dependency posture ──────────────────────────────────────
|
|
228
|
+
if (pkg) {
|
|
229
|
+
const deps = pkg.dependencies ?? {};
|
|
230
|
+
const depNames = Object.keys(deps).sort();
|
|
231
|
+
const unexpected = depNames.filter((d) => !rules_1.APPROVED_RUNTIME_DEPENDENCIES.includes(d));
|
|
232
|
+
if (unexpected.length > 0) {
|
|
233
|
+
findings.push(finding("MC-DEP-001", `unexpected runtime dependencies: ${unexpected.map((d) => `${d}@${deps[d]}`).join(", ")}`, `runtime dependencies exactly: ${rules_1.APPROVED_RUNTIME_DEPENDENCIES.join(", ")}`, ["package.json"]));
|
|
234
|
+
}
|
|
235
|
+
const installScripts = rules_1.INSTALL_SCRIPT_NAMES.filter((s) => pkg.scripts && s in pkg.scripts);
|
|
236
|
+
if (installScripts.length > 0) {
|
|
237
|
+
findings.push(finding("MC-DEP-002", `install lifecycle script(s) declared: ${installScripts.join(", ")}`, "no install lifecycle scripts", ["package.json"]));
|
|
238
|
+
}
|
|
239
|
+
const transpilers = depNames.filter((d) => rules_1.FORBIDDEN_TRANSPILERS.includes(d));
|
|
240
|
+
if (transpilers.length > 0) {
|
|
241
|
+
findings.push(finding("MC-DEP-004", `runtime transpiler dependency: ${transpilers.join(", ")}`, "no runtime transpiler/loader dependencies", ["package.json"]));
|
|
242
|
+
}
|
|
243
|
+
const hostedDeps = depNames.filter((d) => rules_1.HOSTED_DEPENDENCIES.includes(d));
|
|
244
|
+
if (hostedDeps.length > 0) {
|
|
245
|
+
findings.push(finding("MC-DEP-005", `hosted-plane dependency leaked: ${hostedDeps.join(", ")}`, "no hosted-plane runtime dependencies", ["package.json"]));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const nativeHits = [];
|
|
249
|
+
for (const f of files) {
|
|
250
|
+
const cls = (0, rules_1.nativeBinaryClass)(f.content);
|
|
251
|
+
if (cls || f.rel.endsWith(".node"))
|
|
252
|
+
nativeHits.push(`${f.rel}${cls ? ` (${cls})` : ""}`);
|
|
253
|
+
}
|
|
254
|
+
if (nativeHits.length > 0) {
|
|
255
|
+
findings.push(finding("MC-DEP-003", `native binary entries: ${nativeHits.join(", ")}`, "no native binaries in artifact", nativeHits.map((s) => s.split(" ")[0])));
|
|
256
|
+
}
|
|
257
|
+
const failed = Array.from(new Set(findings.map((f) => f.invariant_id))).sort();
|
|
258
|
+
const passed = evaluated.filter((id) => !failed.includes(id)).sort();
|
|
259
|
+
const violations = findings.map((f) => (0, violation_1.buildViolationRecord)({
|
|
260
|
+
sentinelType: exports.ARTIFACT_SENTINEL_TYPE,
|
|
261
|
+
sentinelVersion: exports.ARTIFACT_SENTINEL_VERSION,
|
|
262
|
+
subjectType: "artifact",
|
|
263
|
+
subjectIdentity,
|
|
264
|
+
finding: f,
|
|
265
|
+
detectedAt: opts.detectedAt,
|
|
266
|
+
detector: opts.detector,
|
|
267
|
+
}));
|
|
268
|
+
const blocking = findings.some((f) => f.disposition !== "report");
|
|
269
|
+
return {
|
|
270
|
+
sentinel_type: exports.ARTIFACT_SENTINEL_TYPE,
|
|
271
|
+
sentinel_version: exports.ARTIFACT_SENTINEL_VERSION,
|
|
272
|
+
subject_type: "artifact",
|
|
273
|
+
subject_identity: subjectIdentity,
|
|
274
|
+
subject_mode: mode,
|
|
275
|
+
artifact_name: pkg?.name ?? null,
|
|
276
|
+
artifact_version: artifactVersion,
|
|
277
|
+
file_count: files.length,
|
|
278
|
+
evaluated_invariants: evaluated.sort(),
|
|
279
|
+
passed,
|
|
280
|
+
failed,
|
|
281
|
+
findings,
|
|
282
|
+
violations,
|
|
283
|
+
release_promotion_eligible: !blocking,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
exports.runArtifactSentinel = runArtifactSentinel;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.decidePromotion = void 0;
|
|
4
|
+
const violation_1 = require("./violation");
|
|
5
|
+
const REVIEW_AUTHORITY_CLASSES = new Set(["witness", "issuer", "release", "operator"]);
|
|
6
|
+
function decidePromotion(input) {
|
|
7
|
+
const blocking = [];
|
|
8
|
+
const review = [];
|
|
9
|
+
const diagnostic = [];
|
|
10
|
+
for (const f of input.findings) {
|
|
11
|
+
if (f.disposition === "report") {
|
|
12
|
+
diagnostic.push(f.invariant_id);
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (f.disposition === "require-independent-review" || REVIEW_AUTHORITY_CLASSES.has(f.authority_class)) {
|
|
16
|
+
review.push(f.invariant_id);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
blocking.push(f.invariant_id);
|
|
20
|
+
}
|
|
21
|
+
// MC-REL-003: every blocking/review finding must have a verifiable record.
|
|
22
|
+
const recordsByInvariant = new Map();
|
|
23
|
+
for (const v of input.violations) {
|
|
24
|
+
const list = recordsByInvariant.get(v.invariant_id) ?? [];
|
|
25
|
+
list.push(v);
|
|
26
|
+
recordsByInvariant.set(v.invariant_id, list);
|
|
27
|
+
}
|
|
28
|
+
let evidenceComplete = true;
|
|
29
|
+
for (const id of [...blocking, ...review]) {
|
|
30
|
+
const recs = recordsByInvariant.get(id) ?? [];
|
|
31
|
+
if (recs.length === 0 || !recs.every(violation_1.verifyViolationRecord))
|
|
32
|
+
evidenceComplete = false;
|
|
33
|
+
}
|
|
34
|
+
let decision;
|
|
35
|
+
if (review.length > 0)
|
|
36
|
+
decision = "requires-independent-review";
|
|
37
|
+
else if (blocking.length > 0)
|
|
38
|
+
decision = "blocked";
|
|
39
|
+
else if (diagnostic.length > 0)
|
|
40
|
+
decision = "diagnostic-only";
|
|
41
|
+
else
|
|
42
|
+
decision = "eligible";
|
|
43
|
+
// MC-REL-002/003: a blocking state without complete evidence is still a
|
|
44
|
+
// freeze — and escalates, because a silent or unevidenced block is itself
|
|
45
|
+
// a sentinel failure that needs human eyes.
|
|
46
|
+
if (!evidenceComplete && decision !== "eligible")
|
|
47
|
+
decision = "requires-independent-review";
|
|
48
|
+
return {
|
|
49
|
+
subject: input.subject,
|
|
50
|
+
decision,
|
|
51
|
+
blocking_invariants: Array.from(new Set(blocking)).sort(),
|
|
52
|
+
review_invariants: Array.from(new Set(review)).sort(),
|
|
53
|
+
diagnostic_invariants: Array.from(new Set(diagnostic)).sort(),
|
|
54
|
+
evidence_record_hashes: input.violations.map((v) => v.record_hash).sort(),
|
|
55
|
+
evidence_complete: evidenceComplete,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
exports.decidePromotion = decidePromotion;
|