@event4u/agent-config 17.3.0 → 17.4.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +164 -342
- package/CREDITS.md +2 -2
- package/README.md +1 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +3 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +2 -2
- package/dist/cli-delegate/{chunk-DHSSP7V3.js → chunk-5AYJGUA3.js} +0 -2
- package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
- package/dist/cli-delegate/{chunk-MT2WOOCY.js → chunk-WY5ZZPGL.js} +2 -2
- package/dist/cli-delegate/{chunk-ORJPBER2.js → chunk-X6LZ7W5A.js} +73 -10
- package/dist/cli-delegate/cmd_conformance.js +175 -183
- package/dist/cli-delegate/cmd_doctor.js +5 -5
- package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_update.js +24 -13
- package/dist/cli-delegate/cmd_upgrade.js +2 -2
- package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
- package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
- package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
- package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +3 -3
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/code_graph_refresh.chunk.cjs +56 -27
- package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -1
- package/dist/hooks/dispatch.js +982 -929
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/hooks/egress_engine.chunk.cjs +56 -6
- package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
- package/dist/hooks/update_notice.chunk.cjs +914 -0
- package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +71259 -66437
- package/dist/scripts/condense.js +9 -6
- package/package.json +4 -1
- package/src/config/evaluator-budgets.json +9 -8
- package/src/config/gate-violation-baselines.json +2 -2
- package/src/config/hook-bundle-budget.json +31 -6
- package/src/config/packed-binary-manifest.json +9 -0
- package/src/config/sigstore/npm_keys.json +50 -0
- package/src/config/sigstore/trust-material.json +9 -0
- package/src/config/sigstore/trusted_root.json +126 -0
- package/src/config/skill-body-budget.json +7 -1
- package/src/scripts/_cli/cmd_update.ts +48 -20
- package/src/scripts/_cli/cmd_upgrade.ts +7 -1
- package/src/scripts/_cli/upgrade_unattended.ts +90 -76
- package/src/scripts/_dispatch.bash +3 -2
- package/src/scripts/_lib/confusables.ts +0 -19
- package/src/scripts/_lib/egress_normalize.ts +53 -23
- package/src/scripts/_lib/egress_text.ts +5 -3
- package/src/scripts/_lib/egress_token.ts +7 -5
- package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
- package/src/scripts/_lib/git_common_dir.ts +10 -0
- package/src/scripts/_lib/graph_feeder_record.ts +3 -2
- package/src/scripts/_lib/install_reach_checks.ts +32 -2
- package/src/scripts/_lib/lineage_backfill.ts +136 -0
- package/src/scripts/_lib/lineage_ledger.ts +211 -0
- package/src/scripts/_lib/lineage_map.ts +55 -0
- package/src/scripts/_lib/lineage_query.ts +140 -0
- package/src/scripts/_lib/lineage_seed.ts +116 -0
- package/src/scripts/_lib/lineage_watch.ts +249 -0
- package/src/scripts/_lib/link_crypto.ts +8 -4
- package/src/scripts/_lib/production_reach.ts +3 -3
- package/src/scripts/_lib/project_registry.ts +227 -0
- package/src/scripts/_lib/provenance_verify.ts +14 -242
- package/src/scripts/_lib/sigstore_provenance.ts +177 -0
- package/src/scripts/_lib/source_id.ts +48 -0
- package/src/scripts/_lib/source_shape.ts +4 -0
- package/src/scripts/_lib/unattended_gates.ts +6 -1
- package/src/scripts/advisories.ts +26 -10
- package/src/scripts/ai_council/council_egress.ts +5 -6
- package/src/scripts/bench_graph_feeder_latency.ts +10 -3
- package/src/scripts/check_dependency_floors.ts +7 -0
- package/src/scripts/check_egress_mutations.ts +1 -0
- package/src/scripts/code_graph/build.ts +67 -3
- package/src/scripts/code_graph/cli.ts +131 -28
- package/src/scripts/code_graph/conventions_default.ts +20 -1
- package/src/scripts/code_graph/detect.ts +5 -5
- package/src/scripts/code_graph/estate_cli.ts +135 -0
- package/src/scripts/code_graph/estate_index.ts +394 -0
- package/src/scripts/code_graph/estate_manifests.ts +223 -0
- package/src/scripts/code_graph/estate_map.ts +141 -0
- package/src/scripts/code_graph/extract.ts +4 -1
- package/src/scripts/code_graph/extract_more.ts +137 -2
- package/src/scripts/code_graph/graph_budget.ts +158 -0
- package/src/scripts/code_graph/graph_home.ts +80 -0
- package/src/scripts/code_graph/lineage_cli.ts +315 -0
- package/src/scripts/code_graph/map.ts +22 -13
- package/src/scripts/code_graph/map_model.ts +67 -12
- package/src/scripts/code_graph/map_verify.ts +68 -5
- package/src/scripts/code_graph/map_viewer/viewer.css +12 -0
- package/src/scripts/code_graph/map_viewer/viewer.js +45 -15
- package/src/scripts/code_graph/project_ref.ts +25 -0
- package/src/scripts/code_graph/projects_cli.ts +254 -0
- package/src/scripts/code_graph/providers/catalog.ts +14 -5
- package/src/scripts/code_graph/providers/django.ts +52 -9
- package/src/scripts/code_graph/providers/index.ts +2 -0
- package/src/scripts/code_graph/providers/rails.ts +204 -0
- package/src/scripts/code_graph/providers/spring.ts +38 -1
- package/src/scripts/code_graph/query.ts +9 -9
- package/src/scripts/code_graph/refresh_lock.ts +12 -8
- package/src/scripts/code_graph/repo_identity.ts +169 -0
- package/src/scripts/code_graph/sqlite_store.ts +22 -25
- package/src/scripts/code_graph/types.ts +6 -2
- package/src/scripts/condense.ts +6 -6
- package/src/scripts/hook_chunks.mjs +7 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +20 -17
- package/src/scripts/hooks/code_graph_refresh_hook.ts +10 -7
- package/src/scripts/hooks/dispatch_hook.ts +6 -7
- package/src/scripts/hooks/host_semantics.ts +72 -0
- package/src/scripts/hooks/lazy_concern.ts +4 -0
- package/src/scripts/hooks/user_notice.ts +105 -0
- package/src/scripts/lint_harvest_provenance.ts +137 -11
- package/src/scripts/mcp_server/consumer_tool_catalog.json +69 -15
- package/src/scripts/mcp_server/graph_tools.ts +161 -41
- package/src/scripts/network_sources.ts +24 -6
- package/src/scripts/schemas/provenance-mechanism.schema.json +46 -0
- package/src/scripts/update_notice_hook.ts +75 -7
- package/src/vendor/grammars/README.md +7 -6
- package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
- /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
- /package/dist/cli-delegate/{chunk-BMGUGSY6.js → chunk-TQG6UMPC.js} +0 -0
|
@@ -1,31 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Which workflow, in which repository, a legitimate release of this package is
|
|
3
|
+
* built by — and the check that a SIGNED provenance payload names them.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* step that opened this file (`road-to-an-update-that-installs-itself-only-when-it-is-safe`
|
|
11
|
-
* 2.1) named exactly that gap — *"does not verify the attestation's signature
|
|
12
|
-
* or tie it to `publish-npm.yml`"* — and this module closes the second half.
|
|
13
|
-
*
|
|
14
|
-
* **The first half is only partly closed here, and the distinction is
|
|
15
|
-
* load-bearing.** `provenance_matches_publish` parses an UNSIGNED reading of
|
|
16
|
-
* the DSSE payload. `verify_attestation_signature` below checks the envelope's
|
|
17
|
-
* signature against the certificate the bundle carries, and requires the
|
|
18
|
-
* payload it verified to name the same workflow — but checks no trust root
|
|
19
|
-
* (see its own note). The registry-key half is `npm audit signatures`, which
|
|
20
|
-
* verifies against npm's own trusted keys and is run by the caller as a
|
|
21
|
-
* separate step; these functions are only meaningful WITH it, and the caller
|
|
22
|
-
* says so where it composes them.
|
|
23
|
-
*
|
|
24
|
-
* Why not verify the signature in-process: it needs a sigstore/cosign
|
|
25
|
-
* verification path, which is a dependency this package does not carry and
|
|
26
|
-
* which the hook and pack byte budgets would both have to absorb. Shelling out
|
|
27
|
-
* to the npm client — already a hard dependency of an upgrade — buys the same
|
|
28
|
-
* property for nothing.
|
|
5
|
+
* Signatures, trust roots and the tie to the tarball are verified in
|
|
6
|
+
* `sigstore_provenance.ts`; this module only reads a payload that has already
|
|
7
|
+
* been verified there. It used to carry a certificate-bound signature check of
|
|
8
|
+
* its own, which checked no issuer and so passed a self-signed certificate
|
|
9
|
+
* carrying the expected SAN; that check is gone rather than kept as a fallback.
|
|
29
10
|
*/
|
|
30
11
|
|
|
31
12
|
/**
|
|
@@ -44,65 +25,16 @@ function asRecord(v: unknown): Record<string, unknown> | null {
|
|
|
44
25
|
return v !== null && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : null;
|
|
45
26
|
}
|
|
46
27
|
|
|
47
|
-
/** Decode the DSSE payload of the first attestation; `null` when unreadable. */
|
|
48
|
-
function decode_payload(doc: unknown): Record<string, unknown> | null {
|
|
49
|
-
const list = asRecord(doc)?.['attestations'];
|
|
50
|
-
if (!Array.isArray(list) || list.length === 0) return null;
|
|
51
|
-
for (const entry of list) {
|
|
52
|
-
const payload = asRecord(asRecord(asRecord(entry)?.['bundle'])?.['dsseEnvelope'])?.['payload'];
|
|
53
|
-
if (typeof payload !== 'string' || payload === '') continue;
|
|
54
|
-
try {
|
|
55
|
-
const parsed: unknown = JSON.parse(Buffer.from(payload, 'base64').toString('utf8'));
|
|
56
|
-
const rec = asRecord(parsed);
|
|
57
|
-
if (rec !== null) return rec;
|
|
58
|
-
} catch {
|
|
59
|
-
// A payload that is not base64 JSON is not evidence of anything.
|
|
60
|
-
// Keep looking: npm may serve several attestations and only one of
|
|
61
|
-
// them has to be the provenance statement.
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function external_workflow(doc: unknown): Record<string, unknown> | null {
|
|
68
|
-
const payload = decode_payload(doc);
|
|
69
|
-
if (payload === null) return null;
|
|
70
|
-
const predicate = asRecord(payload['predicate']);
|
|
71
|
-
const external = asRecord(asRecord(predicate?.['buildDefinition'])?.['externalParameters']);
|
|
72
|
-
return asRecord(external?.['workflow']);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* The workflow path named by the attestation, or `null` when the document
|
|
77
|
-
* carries none that can be read.
|
|
78
|
-
*/
|
|
79
|
-
export function attestation_workflow_ref(doc: unknown): string | null {
|
|
80
|
-
const path_value = external_workflow(doc)?.['path'];
|
|
81
|
-
return typeof path_value === 'string' && path_value !== '' ? path_value : null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** The repository named by the attestation, or `null`. */
|
|
85
|
-
export function attestation_repository(doc: unknown): string | null {
|
|
86
|
-
const repo = external_workflow(doc)?.['repository'];
|
|
87
|
-
return typeof repo === 'string' && repo !== '' ? repo : null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
28
|
/**
|
|
91
|
-
* Does this
|
|
92
|
-
* workflow, in this package's repository?
|
|
29
|
+
* Does this provenance payload say the tarball was built by this package's
|
|
30
|
+
* publish workflow, in this package's repository?
|
|
93
31
|
*
|
|
94
|
-
* Both halves are required and both reject on absence: a
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* signal.
|
|
32
|
+
* Both halves are required and both reject on absence: a payload that names no
|
|
33
|
+
* workflow is not one that names the right one. Repository is compared with
|
|
34
|
+
* the trailing `.git` and any trailing slash removed, because the same remote
|
|
35
|
+
* is written both ways and neither spelling is an attacker signal.
|
|
99
36
|
*/
|
|
100
|
-
export function
|
|
101
|
-
return payload_names_publish(decode_payload(doc));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** The workflow and repository check, over one already-decoded payload. */
|
|
105
|
-
function payload_names_publish(payload: Record<string, unknown> | null): boolean {
|
|
37
|
+
export function payload_names_publish(payload: Record<string, unknown> | null): boolean {
|
|
106
38
|
const predicate = asRecord(payload?.['predicate']);
|
|
107
39
|
const workflow = asRecord(asRecord(asRecord(predicate?.['buildDefinition'])?.['externalParameters'])?.['workflow']);
|
|
108
40
|
if (workflow?.['path'] !== EXPECTED_WORKFLOW_REF) return false;
|
|
@@ -110,163 +42,3 @@ function payload_names_publish(payload: Record<string, unknown> | null): boolean
|
|
|
110
42
|
if (typeof repo !== 'string' || repo === '') return false;
|
|
111
43
|
return repo.replace(/\.git$/, '').replace(/\/$/, '') === EXPECTED_REPOSITORY;
|
|
112
44
|
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Does the certificate's SAN carry the workflow identity as a whole URI entry?
|
|
116
|
-
* Anchored: the entry is the expected workflow followed by `@<ref>`, never a
|
|
117
|
-
* SAN that merely contains the string.
|
|
118
|
-
*/
|
|
119
|
-
function san_names_workflow(san: string): boolean {
|
|
120
|
-
return san.split(/,\s*/).some((e) => e.startsWith(`URI:${SAN_WORKFLOW_MARKER}@`));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// --- the signature half -------------------------------------------------------
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* What the clauses below do and do not establish.
|
|
127
|
-
*
|
|
128
|
-
* The header above says the signature half is not closed here. It is now
|
|
129
|
-
* HALF-closed, and the half that is missing is stated first so nothing below
|
|
130
|
-
* reads as more than it is.
|
|
131
|
-
*
|
|
132
|
-
* **Closed:** the DSSE envelope's signature is verified against the public key
|
|
133
|
-
* of the certificate the bundle itself carries, over the DSSE
|
|
134
|
-
* pre-authentication encoding — so a payload edited after signing is caught,
|
|
135
|
-
* and a bundle whose signature does not belong to its own certificate is
|
|
136
|
-
* caught. And the certificate's own SAN is required to name the expected
|
|
137
|
-
* workflow, so a bundle whose PAYLOAD claims `publish-npm.yml` while its
|
|
138
|
-
* certificate was issued to some other identity is caught. Before this, both
|
|
139
|
-
* were a base64 decode and a string compare.
|
|
140
|
-
*
|
|
141
|
-
* **Not closed, and this is the residue:** there is no trust root. Nothing
|
|
142
|
-
* here checks that the certificate was issued by Fulcio, and nothing checks a
|
|
143
|
-
* Rekor inclusion proof. An attacker who can serve a registry response can mint
|
|
144
|
-
* a self-signed certificate carrying the expected SAN, sign their own payload
|
|
145
|
-
* with it, and pass every check in this file. Closing that needs a sigstore
|
|
146
|
-
* verification path with pinned roots — a dependency decision, not a line in
|
|
147
|
-
* this function, and it is registered as the `dsse-trust-root` blocker on
|
|
148
|
-
* `road-to-an-update-that-installs-itself-only-when-it-is-safe`.
|
|
149
|
-
*
|
|
150
|
-
* So the honest statement of what the provenance gate is worth today: it
|
|
151
|
-
* defeats a partial compromise — a mis-scoped workflow, an edited payload, a
|
|
152
|
-
* bundle reused from another build — and it does not defeat a forged bundle
|
|
153
|
-
* from someone who already controls what the install reads.
|
|
154
|
-
*/
|
|
155
|
-
|
|
156
|
-
/** Why the signature does not verify, or `null` when it does. */
|
|
157
|
-
export type SignatureVerdict = string | null;
|
|
158
|
-
|
|
159
|
-
/** The SAN extension OID the Fulcio leaf carries the workflow identity in. */
|
|
160
|
-
const SAN_WORKFLOW_MARKER = `${EXPECTED_REPOSITORY}/${EXPECTED_WORKFLOW_REF}`;
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* The DSSE pre-authentication encoding — the exact bytes a DSSE signature is
|
|
164
|
-
* taken over.
|
|
165
|
-
*
|
|
166
|
-
* `DSSEv1 SP len(payloadType) SP payloadType SP len(payload) SP payload`, with
|
|
167
|
-
* both lengths in BYTES rather than characters. Built as a Buffer throughout
|
|
168
|
-
* because the payload is arbitrary bytes and a string round-trip would mangle
|
|
169
|
-
* any that are not valid UTF-8.
|
|
170
|
-
*/
|
|
171
|
-
export function dssePae(payloadType: string, payload: Buffer): Buffer {
|
|
172
|
-
const type = Buffer.from(payloadType, 'utf8');
|
|
173
|
-
return Buffer.concat([
|
|
174
|
-
Buffer.from(`DSSEv1 ${String(type.length)} `, 'utf8'),
|
|
175
|
-
type,
|
|
176
|
-
Buffer.from(` ${String(payload.length)} `, 'utf8'),
|
|
177
|
-
payload,
|
|
178
|
-
]);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
interface Envelope {
|
|
182
|
-
readonly payloadType: string;
|
|
183
|
-
readonly payload: Buffer;
|
|
184
|
-
readonly sig: Buffer;
|
|
185
|
-
readonly certDer: Buffer;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function readEnvelope(entry: unknown): Envelope | null {
|
|
189
|
-
const bundle = asRecord(asRecord(entry)?.['bundle']);
|
|
190
|
-
const dsse = asRecord(bundle?.['dsseEnvelope']);
|
|
191
|
-
if (dsse === null) return null;
|
|
192
|
-
const payloadB64 = dsse['payload'];
|
|
193
|
-
const payloadType = dsse['payloadType'];
|
|
194
|
-
const sigs = dsse['signatures'];
|
|
195
|
-
if (typeof payloadB64 !== 'string' || typeof payloadType !== 'string' || !Array.isArray(sigs) || sigs.length === 0) return null;
|
|
196
|
-
const sigB64 = asRecord(sigs[0])?.['sig'];
|
|
197
|
-
if (typeof sigB64 !== 'string' || sigB64 === '') return null;
|
|
198
|
-
|
|
199
|
-
const material = asRecord(bundle?.['verificationMaterial']);
|
|
200
|
-
// Both shapes sigstore has used: a single certificate, and a chain.
|
|
201
|
-
const single = asRecord(material?.['certificate'])?.['rawBytes'];
|
|
202
|
-
const chain = asRecord(material?.['x509CertificateChain'])?.['certificates'];
|
|
203
|
-
const first = Array.isArray(chain) ? asRecord(chain[0])?.['rawBytes'] : undefined;
|
|
204
|
-
const certB64 = typeof single === 'string' ? single : typeof first === 'string' ? first : null;
|
|
205
|
-
if (certB64 === null || certB64 === '') return null;
|
|
206
|
-
|
|
207
|
-
return {
|
|
208
|
-
payloadType,
|
|
209
|
-
payload: Buffer.from(payloadB64, 'base64'),
|
|
210
|
-
sig: Buffer.from(sigB64, 'base64'),
|
|
211
|
-
certDer: Buffer.from(certB64, 'base64'),
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Verify the first readable attestation's DSSE signature against its own
|
|
217
|
-
* certificate, and require that certificate to name this package's workflow.
|
|
218
|
-
*
|
|
219
|
-
* Returns a reason on every failure rather than a boolean, because the caller
|
|
220
|
-
* prints it: a refusal a user cannot act on is a refusal they route around.
|
|
221
|
-
* `crypto` is imported lazily so a caller that never verifies a signature does
|
|
222
|
-
* not pay for the module — this runs on an install path, never in a hook.
|
|
223
|
-
*/
|
|
224
|
-
export async function verify_attestation_signature(doc: unknown): Promise<SignatureVerdict> {
|
|
225
|
-
const list = asRecord(doc)?.['attestations'];
|
|
226
|
-
if (!Array.isArray(list) || list.length === 0) return 'the attestation bundle carries no attestations';
|
|
227
|
-
const crypto = await import('node:crypto');
|
|
228
|
-
|
|
229
|
-
let lastReason = 'no attestation carried a readable DSSE envelope';
|
|
230
|
-
for (const entry of list) {
|
|
231
|
-
const env = readEnvelope(entry);
|
|
232
|
-
if (env === null) continue;
|
|
233
|
-
let cert: InstanceType<typeof crypto.X509Certificate>;
|
|
234
|
-
try {
|
|
235
|
-
cert = new crypto.X509Certificate(env.certDer);
|
|
236
|
-
} catch {
|
|
237
|
-
lastReason = 'the signing certificate could not be parsed';
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
// The certificate's own identity, not the payload's claim about it.
|
|
241
|
-
const san = cert.subjectAltName ?? '';
|
|
242
|
-
if (!san_names_workflow(san)) {
|
|
243
|
-
lastReason = `the signing certificate names ${san === '' ? 'no identity' : san}, not ${SAN_WORKFLOW_MARKER}`;
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
let ok = false;
|
|
247
|
-
try {
|
|
248
|
-
ok = crypto.verify(null, dssePae(env.payloadType, env.payload), cert.publicKey, env.sig);
|
|
249
|
-
} catch {
|
|
250
|
-
ok = false;
|
|
251
|
-
}
|
|
252
|
-
if (!ok) {
|
|
253
|
-
lastReason = 'the DSSE signature does not verify against the certificate in the bundle';
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
// The payload this signature covers must itself name the workflow:
|
|
257
|
-
// otherwise a valid signature on one entry vouches for a claim made in
|
|
258
|
-
// another, which is all the unsigned check reads.
|
|
259
|
-
let verified: Record<string, unknown> | null = null;
|
|
260
|
-
try {
|
|
261
|
-
verified = asRecord(JSON.parse(env.payload.toString('utf8')));
|
|
262
|
-
} catch {
|
|
263
|
-
verified = null;
|
|
264
|
-
}
|
|
265
|
-
if (!payload_names_publish(verified)) {
|
|
266
|
-
lastReason = 'the signed payload does not name this package\'s publish workflow';
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
return null;
|
|
270
|
-
}
|
|
271
|
-
return lastReason;
|
|
272
|
-
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify a release's two attestations against pinned sigstore trust material,
|
|
3
|
+
* and bind both to the exact tarball bytes that will be installed.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS REPLACES THE CERTIFICATE-BOUND CHECK. `provenance_verify.ts` checked
|
|
6
|
+
* the DSSE signature against the certificate a bundle carries and that
|
|
7
|
+
* certificate's SAN — and nothing about who issued it, so anyone who could
|
|
8
|
+
* serve the registry answer could mint a self-signed certificate with the
|
|
9
|
+
* expected SAN and pass. Pinned roots close that; a root fetched at run time
|
|
10
|
+
* would let the same attacker choose what the bundle is checked against.
|
|
11
|
+
*
|
|
12
|
+
* WHAT IS VERIFIED, both attestations required:
|
|
13
|
+
* - the SLSA provenance bundle: Fulcio chain to the pinned root, the signing
|
|
14
|
+
* certificate's validity at the time the transparency log recorded it, a
|
|
15
|
+
* Rekor entry and an SCT checked against the pinned logs, the DSSE signature,
|
|
16
|
+
* and an identity policy — issuer `token.actions.githubusercontent.com` and a
|
|
17
|
+
* SAN that is exactly this repository's `publish-npm.yml` at the tag of the
|
|
18
|
+
* version being installed;
|
|
19
|
+
* - the npm publish attestation: its signature against npm's pinned
|
|
20
|
+
* `npm:attestations` keys, and its Rekor entry — a second trust domain,
|
|
21
|
+
* verified separately;
|
|
22
|
+
* - in both payloads, exactly one subject naming this package at this version,
|
|
23
|
+
* whose sha512 equals the sha512 of the bytes about to be installed.
|
|
24
|
+
*
|
|
25
|
+
* WHAT IT NEVER DOES: fetch trust material (the files are read from the
|
|
26
|
+
* installed package and their digests checked against the recorded ones), fall
|
|
27
|
+
* back to the old check or to `npm audit signatures`, or pass anything it could
|
|
28
|
+
* not verify. Every failure is a reason string, because the caller prints it.
|
|
29
|
+
*
|
|
30
|
+
* The sigstore modules are imported lazily: this runs on the unattended upgrade
|
|
31
|
+
* path only, and nothing else should pay for loading them.
|
|
32
|
+
*/
|
|
33
|
+
import { createHash } from 'node:crypto';
|
|
34
|
+
import * as fs from 'node:fs';
|
|
35
|
+
import * as path from 'node:path';
|
|
36
|
+
|
|
37
|
+
import { resolvePackageRoot } from './package_root.js';
|
|
38
|
+
import { EXPECTED_REPOSITORY, EXPECTED_WORKFLOW_REF, payload_names_publish } from './provenance_verify.js';
|
|
39
|
+
|
|
40
|
+
export const SLSA_PROVENANCE_V1 = 'https://slsa.dev/provenance/v1';
|
|
41
|
+
export const NPM_PUBLISH_V01 = 'https://github.com/npm/attestation/tree/main/specs/publish/v0.1';
|
|
42
|
+
export const GITHUB_OIDC_ISSUER = 'https://token.actions.githubusercontent.com';
|
|
43
|
+
export const PACKAGE_PURL_BASE = 'pkg:npm/%40event4u/agent-config';
|
|
44
|
+
|
|
45
|
+
/** Why verification refused, or `null` when everything held. */
|
|
46
|
+
export type ProvenanceVerdict = string | null;
|
|
47
|
+
|
|
48
|
+
function asRecord(v: unknown): Record<string, unknown> | null {
|
|
49
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function escapeRegExp(s: string): string {
|
|
53
|
+
return s.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The only SAN accepted: this repository's publish workflow at the release tag
|
|
58
|
+
* of exactly the version being installed. Anchored at both ends — the library
|
|
59
|
+
* treats the policy as a pattern, and an unanchored one would accept any SAN
|
|
60
|
+
* that merely contains it.
|
|
61
|
+
*/
|
|
62
|
+
export function expectedSan(version: string): RegExp {
|
|
63
|
+
return new RegExp(`^${escapeRegExp(`${EXPECTED_REPOSITORY}/${EXPECTED_WORKFLOW_REF}@refs/tags/${version}`)}$`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface TrustFiles {
|
|
67
|
+
readonly trustedRoot: unknown;
|
|
68
|
+
readonly npmKeys: unknown;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Read the pinned trust material and refuse when a file's digest differs from
|
|
73
|
+
* the recorded one — a release can only be checked against the material it
|
|
74
|
+
* shipped with, as it shipped.
|
|
75
|
+
*/
|
|
76
|
+
export function readTrustFiles(packageRoot: string = resolvePackageRoot(import.meta.url)): TrustFiles | string {
|
|
77
|
+
const dir = path.join(packageRoot, 'src', 'config', 'sigstore');
|
|
78
|
+
try {
|
|
79
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'trust-material.json'), 'utf-8')) as {
|
|
80
|
+
files?: Record<string, string>;
|
|
81
|
+
};
|
|
82
|
+
const read = (name: string): unknown => {
|
|
83
|
+
const bytes = fs.readFileSync(path.join(dir, name));
|
|
84
|
+
const want = manifest.files?.[name];
|
|
85
|
+
const got = createHash('sha256').update(bytes).digest('hex');
|
|
86
|
+
if (want !== got) throw new Error(`${name} has sha256 ${got}, the recorded one is ${String(want)}`);
|
|
87
|
+
return JSON.parse(bytes.toString('utf-8')) as unknown;
|
|
88
|
+
};
|
|
89
|
+
return { trustedRoot: read('trusted_root.json'), npmKeys: read('npm_keys.json') };
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return `the pinned sigstore trust material is unusable: ${err instanceof Error ? err.message : String(err)}`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Exactly one subject, naming this package at this version, with this sha512. */
|
|
96
|
+
function subjectMatches(payload: Record<string, unknown> | null, version: string, sha512Hex: string): string | null {
|
|
97
|
+
const subjects = payload?.['subject'];
|
|
98
|
+
if (!Array.isArray(subjects) || subjects.length !== 1) return 'it does not name exactly one subject';
|
|
99
|
+
const subject = asRecord(subjects[0]);
|
|
100
|
+
if (subject?.['name'] !== `${PACKAGE_PURL_BASE}@${version}`) return `its subject is ${String(subject?.['name'])}, not ${version}`;
|
|
101
|
+
const digest = asRecord(subject['digest'])?.['sha512'];
|
|
102
|
+
if (typeof digest !== 'string' || digest.toLowerCase() !== sha512Hex.toLowerCase()) {
|
|
103
|
+
return 'its subject digest does not match the tarball about to be installed';
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface ReleaseToVerify {
|
|
109
|
+
readonly version: string;
|
|
110
|
+
/** sha512 of the exact tarball bytes that will be installed, lower-case hex. */
|
|
111
|
+
readonly tarballSha512Hex: string;
|
|
112
|
+
/** The registry's attestation document for this version. */
|
|
113
|
+
readonly attestations: unknown;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function verify_release_provenance(
|
|
117
|
+
release: ReleaseToVerify,
|
|
118
|
+
trust: TrustFiles | string = readTrustFiles(),
|
|
119
|
+
): Promise<ProvenanceVerdict> {
|
|
120
|
+
if (typeof trust === 'string') return trust;
|
|
121
|
+
const list = asRecord(release.attestations)?.['attestations'];
|
|
122
|
+
if (!Array.isArray(list)) return 'the attestation document carries no attestations';
|
|
123
|
+
const byType = new Map<string, unknown>();
|
|
124
|
+
for (const entry of list) {
|
|
125
|
+
const type = asRecord(entry)?.['predicateType'];
|
|
126
|
+
if (typeof type === 'string' && !byType.has(type)) byType.set(type, asRecord(entry)?.['bundle']);
|
|
127
|
+
}
|
|
128
|
+
for (const type of [SLSA_PROVENANCE_V1, NPM_PUBLISH_V01]) {
|
|
129
|
+
if (!byType.has(type)) return `the release carries no ${type} attestation`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const { Verifier, toSignedEntity, toTrustMaterial } = await import('@sigstore/verify');
|
|
133
|
+
const { bundleFromJSON } = await import('@sigstore/bundle');
|
|
134
|
+
const { PublicKey, TrustedRoot } = await import('@sigstore/protobuf-specs');
|
|
135
|
+
|
|
136
|
+
let material: ReturnType<typeof toTrustMaterial>;
|
|
137
|
+
try {
|
|
138
|
+
const keys: Record<string, ReturnType<typeof PublicKey.fromJSON>> = {};
|
|
139
|
+
const listed = asRecord(trust.npmKeys)?.['keys'];
|
|
140
|
+
for (const k of Array.isArray(listed) ? listed : []) {
|
|
141
|
+
const key = asRecord(k);
|
|
142
|
+
if (key?.['keyUsage'] === 'npm:attestations' && typeof key['keyId'] === 'string') {
|
|
143
|
+
keys[key['keyId']] = PublicKey.fromJSON(key['publicKey']);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
material = toTrustMaterial(TrustedRoot.fromJSON(trust.trustedRoot), keys);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
return `the pinned sigstore trust material could not be loaded: ${err instanceof Error ? err.message : String(err)}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const type of [SLSA_PROVENANCE_V1, NPM_PUBLISH_V01]) {
|
|
152
|
+
let payload: Record<string, unknown> | null;
|
|
153
|
+
try {
|
|
154
|
+
const bundle = bundleFromJSON(byType.get(type));
|
|
155
|
+
const verifier = new Verifier(material, { tlogThreshold: 1, ctlogThreshold: 1, timestampThreshold: 1 });
|
|
156
|
+
const policy =
|
|
157
|
+
type === SLSA_PROVENANCE_V1
|
|
158
|
+
? { subjectAlternativeName: expectedSan(release.version), extensions: { issuer: GITHUB_OIDC_ISSUER } }
|
|
159
|
+
: undefined;
|
|
160
|
+
verifier.verify(toSignedEntity(bundle), policy);
|
|
161
|
+
if (bundle.content.$case !== 'dsseEnvelope') return `the ${type} attestation is not a DSSE envelope`;
|
|
162
|
+
payload = asRecord(JSON.parse(bundle.content.dsseEnvelope.payload.toString('utf-8')));
|
|
163
|
+
} catch (err) {
|
|
164
|
+
const code = asRecord(err)?.['code'];
|
|
165
|
+
return `the ${type} attestation does not verify${typeof code === 'string' ? ` (${code})` : ''}: ${
|
|
166
|
+
err instanceof Error ? err.message : String(err)
|
|
167
|
+
}`;
|
|
168
|
+
}
|
|
169
|
+
if (payload?.['predicateType'] !== type) return `the ${type} attestation's statement names another predicate type`;
|
|
170
|
+
const subject = subjectMatches(payload, release.version, release.tarballSha512Hex);
|
|
171
|
+
if (subject !== null) return `the ${type} attestation is not for this tarball: ${subject}`;
|
|
172
|
+
if (type === SLSA_PROVENANCE_V1 && !payload_names_publish(payload)) {
|
|
173
|
+
return `the signed provenance does not name ${EXPECTED_WORKFLOW_REF} in ${EXPECTED_REPOSITORY}`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable, keyed source ids — `src_` plus 10 hex of an HMAC over the canonical identity.
|
|
3
|
+
*
|
|
4
|
+
* `ENC1` tokens are salted per call, so two tokens for one source never compare
|
|
5
|
+
* equal; a round-scoped `S1…` code is renumbered every round. Neither can key a
|
|
6
|
+
* ledger. This id can: it is stable across rounds and URL spellings, and keyed
|
|
7
|
+
* with `secrets.link_encryption_key`, so nobody without the key can confirm a
|
|
8
|
+
* guessed source by hashing its URL. Without a key there is no id — callers
|
|
9
|
+
* refuse rather than mint an unkeyed one.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as crypto from 'node:crypto';
|
|
13
|
+
|
|
14
|
+
import { canonicalize } from '../harvest_reference_tokens.js';
|
|
15
|
+
|
|
16
|
+
export const SOURCE_ID_PREFIX = 'src_';
|
|
17
|
+
export const SOURCE_ID_RE = /^src_[0-9a-f]{10}$/;
|
|
18
|
+
// Domain-separates this HMAC from any other use of the same key.
|
|
19
|
+
const ID_CONTEXT = 'agent-config/source-id/v1\0';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Repository URLs collapse through `canonicalize`; anything else (an article,
|
|
23
|
+
* a video, a paper) keeps host, path and query with scheme, credentials,
|
|
24
|
+
* fragment and a trailing slash dropped. A string that is not a URL is used
|
|
25
|
+
* trimmed, as written — an opaque reference names itself.
|
|
26
|
+
*/
|
|
27
|
+
export function canonicalSourceIdentity(raw: string): string {
|
|
28
|
+
const repo = canonicalize(raw);
|
|
29
|
+
if (repo !== null) return repo;
|
|
30
|
+
const s = raw.trim();
|
|
31
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) return s;
|
|
32
|
+
let u: URL;
|
|
33
|
+
try {
|
|
34
|
+
u = new URL(s);
|
|
35
|
+
} catch {
|
|
36
|
+
return s;
|
|
37
|
+
}
|
|
38
|
+
const host = u.hostname.toLowerCase().replace(/^www\./, '');
|
|
39
|
+
const port = u.port === '' ? '' : `:${u.port}`;
|
|
40
|
+
const pathname = u.pathname.replace(/\/+$/, '');
|
|
41
|
+
return `${host}${port}${pathname}${u.search}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function sourceId(identity: string, key: string): string {
|
|
45
|
+
if (!key) throw new Error('no link_encryption_key — a source id is never minted without the key');
|
|
46
|
+
const mac = crypto.createHmac('sha256', key).update(ID_CONTEXT + canonicalSourceIdentity(identity)).digest('hex');
|
|
47
|
+
return SOURCE_ID_PREFIX + mac.slice(0, 10);
|
|
48
|
+
}
|
|
@@ -155,6 +155,10 @@ const ALLOWED_OWNERS: readonly string[] = [
|
|
|
155
155
|
// looked; this is the answer. Fixing them here rather than by widening the
|
|
156
156
|
// dedup rule is what keeps that rule strict.
|
|
157
157
|
'someone', 'o',
|
|
158
|
+
// `someone-else/<repo>` is the wrong-repository fixture of the provenance
|
|
159
|
+
// tests; a committed council prompt quoting those tests verbatim was the
|
|
160
|
+
// measured false positive (2026-10-24). Same shape as the two above.
|
|
161
|
+
'someone-else',
|
|
158
162
|
// Vendors whose tools this suite integrates or recommends (rule carve-out).
|
|
159
163
|
'anthropics', 'anthropic', 'microsoft', 'github', 'modelcontextprotocol',
|
|
160
164
|
'cloudflare', 'openai', 'nodejs', 'vitest-dev', 'vercel', 'tailwindlabs',
|
|
@@ -30,9 +30,12 @@ export interface RegistryCandidate {
|
|
|
30
30
|
* workflow produced the document. Tying it to this package's publish
|
|
31
31
|
* workflow needs the bundle itself, which the packument only points at —
|
|
32
32
|
* so the pointer travels with the candidate and the caller fetches it
|
|
33
|
-
* (`
|
|
33
|
+
* (`sigstore_provenance.verify_release_provenance`).
|
|
34
34
|
*/
|
|
35
35
|
readonly attestationsUrl: string | null;
|
|
36
|
+
/** Where npm serves this version's tarball, and the integrity it records for it. */
|
|
37
|
+
readonly tarballUrl: string | null;
|
|
38
|
+
readonly integrity: string | null;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
function asRecord(v: unknown): Record<string, unknown> | null {
|
|
@@ -58,6 +61,8 @@ export function read_candidate(packument: unknown): RegistryCandidate | null {
|
|
|
58
61
|
provenance: typeof predicate === 'string' && predicate.startsWith('https://slsa.dev/provenance/'),
|
|
59
62
|
impactMap: asRecord(manifest['agentConfig'])?.['impact'] ?? null,
|
|
60
63
|
attestationsUrl: typeof attestations?.['url'] === 'string' ? (attestations['url'] as string) : null,
|
|
64
|
+
tarballUrl: typeof asRecord(manifest['dist'])?.['tarball'] === 'string' ? (asRecord(manifest['dist'])?.['tarball'] as string) : null,
|
|
65
|
+
integrity: typeof asRecord(manifest['dist'])?.['integrity'] === 'string' ? (asRecord(manifest['dist'])?.['integrity'] as string) : null,
|
|
61
66
|
};
|
|
62
67
|
}
|
|
63
68
|
|
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
emptyLedger,
|
|
64
64
|
validateAcceptance,
|
|
65
65
|
} from './_lib/advisory_verdicts.js';
|
|
66
|
+
import { readRegistry, recordProject } from './_lib/project_registry.js';
|
|
66
67
|
import { event4u_root } from './_lib/user_global_paths.js';
|
|
67
68
|
import {
|
|
68
69
|
advance,
|
|
@@ -363,25 +364,40 @@ export function markOwnActionsPrivate<T extends { entries: Array<{ ecosystem: st
|
|
|
363
364
|
return inv;
|
|
364
365
|
}
|
|
365
366
|
|
|
366
|
-
/**
|
|
367
|
+
/**
|
|
368
|
+
* The project list is the shared user-global registry (`_lib/project_registry`,
|
|
369
|
+
* ADR-299): a sibling of the advisories machine dir, so an injected dir keeps
|
|
370
|
+
* its registry beside it. Roots and fingerprints only (5.4).
|
|
371
|
+
*/
|
|
372
|
+
function registryDirFor(mdir: string): string {
|
|
373
|
+
return path.join(path.dirname(mdir), 'projects');
|
|
374
|
+
}
|
|
375
|
+
|
|
367
376
|
function recordProjects(mdir: string, rows: Array<{ root: string; fingerprint: string }>, now: Date): void {
|
|
377
|
+
for (const r of rows) recordProject(r.root, { fingerprint: r.fingerprint }, { dir: registryDirFor(mdir), now });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** The pre-registry `projects.json`, imported once and then moved aside. */
|
|
381
|
+
function migrateLegacyProjects(mdir: string): void {
|
|
368
382
|
const file = path.join(mdir, 'projects.json');
|
|
369
|
-
let doc: Record<string, { fingerprint
|
|
383
|
+
let doc: Record<string, { fingerprint?: string; at?: string }>;
|
|
370
384
|
try {
|
|
371
385
|
doc = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
372
386
|
} catch {
|
|
373
|
-
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
for (const [root, e] of Object.entries(doc ?? {})) {
|
|
390
|
+
const at = typeof e?.at === 'string' && !Number.isNaN(Date.parse(e.at)) ? new Date(e.at) : new Date();
|
|
391
|
+
recordProject(root, { fingerprint: typeof e?.fingerprint === 'string' ? e.fingerprint : null }, { dir: registryDirFor(mdir), now: at });
|
|
374
392
|
}
|
|
375
|
-
|
|
376
|
-
writeJson(file, doc);
|
|
393
|
+
fs.renameSync(file, `${file}.migrated`);
|
|
377
394
|
}
|
|
378
395
|
|
|
379
396
|
export function knownProjects(mdir: string = machineDir()): string[] {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
397
|
+
migrateLegacyProjects(mdir);
|
|
398
|
+
return Object.values(readRegistry({ dir: registryDirFor(mdir) }).projects)
|
|
399
|
+
.map((e) => e.root)
|
|
400
|
+
.filter((r) => fs.existsSync(r));
|
|
385
401
|
}
|
|
386
402
|
|
|
387
403
|
// ---------------------------------------------------------------------------
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
* decision D8, which this module does not take.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { detectNormalized, detectWithExtent } from '../_lib/egress_normalize.js';
|
|
20
21
|
import {
|
|
21
|
-
detect,
|
|
22
22
|
EGRESS_RULES,
|
|
23
23
|
type EgressRule,
|
|
24
24
|
type EgressSpan,
|
|
@@ -26,8 +26,6 @@ import {
|
|
|
26
26
|
prefixesOf,
|
|
27
27
|
rawKeyPrefixPattern,
|
|
28
28
|
replaceSpans,
|
|
29
|
-
resolveOverlaps,
|
|
30
|
-
findSpans,
|
|
31
29
|
REVIEW_CREDENTIAL_ROWS,
|
|
32
30
|
} from '../_lib/egress_rules.js';
|
|
33
31
|
import { parseTokens, TokenScope } from '../_lib/egress_token.js';
|
|
@@ -88,9 +86,10 @@ export class CouncilEgress {
|
|
|
88
86
|
* proven is replaced through the nearest END marker, as the line masks did.
|
|
89
87
|
*/
|
|
90
88
|
replace(text: string): string {
|
|
91
|
-
const
|
|
89
|
+
const at = detectWithExtent(text, BUNDLE_RULES);
|
|
90
|
+
const spans = at.spans
|
|
92
91
|
.filter((s) => s.class === 'credential' || this.opts.identifiers)
|
|
93
|
-
.map((s) => ({ ...s, end:
|
|
92
|
+
.map((s) => ({ ...s, end: at.end(s, maskingEnd) ?? s.end }));
|
|
94
93
|
spans.sort((a, b) => a.start - b.start);
|
|
95
94
|
const merged: EgressSpan[] = [];
|
|
96
95
|
for (const s of spans) {
|
|
@@ -124,7 +123,7 @@ export class CouncilEgress {
|
|
|
124
123
|
for (const value of this.replaced) {
|
|
125
124
|
if (body.includes(value)) return { reason: 'replaced-value-present', what: 'value replaced in this run' };
|
|
126
125
|
}
|
|
127
|
-
for (const s of
|
|
126
|
+
for (const s of detectNormalized(body)) {
|
|
128
127
|
const high = s.class === 'credential' ? s.rule !== 'generic-assignment' : s.validated && this.opts.identifiers;
|
|
129
128
|
if (high) return { reason: 'high-confidence-finding', what: s.rule };
|
|
130
129
|
}
|