coderifts 4.8.0 → 4.9.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/CHANGELOG.md +31 -0
- package/README.md +11 -7
- package/dist/cli.js +399 -31
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.9.0
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **P0 / ID621 — `enforce --check` no longer reports `inescapable_deploy: true`
|
|
7
|
+
for an unenforced configuration.** External audit 2026-08-24 reproduced two
|
|
8
|
+
holes on the published CLI: (a) the required check matched by **name only**
|
|
9
|
+
(`required_status_checks.checks[]` with no `app_id`), so any repository
|
|
10
|
+
writer could satisfy it with a same-named success status; (b) the two local
|
|
11
|
+
workflow checks were **text searches**, so `# uses: coderifts/contract-gate@v0`
|
|
12
|
+
in a comment counted as VERIFIED. Both now fail closed.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **`--json` spec `provider-enforcement-evidence.v2`.** The meaning of
|
|
16
|
+
`VERIFIED` changed. A layer whose evidence cannot be bound to an issuer
|
|
17
|
+
(CodeRifts GitHub App id, public `GET /apps/coderifts`) or to an **active**
|
|
18
|
+
workflow job is `NOT_VERIFIED` or `UNVERIFIABLE` — never `VERIFIED`.
|
|
19
|
+
`inescapable_deploy` is true only when every layer is VERIFIED under these
|
|
20
|
+
rules.
|
|
21
|
+
- **Issuer binding:** `required_check` is VERIFIED only when `checks[]`
|
|
22
|
+
carries our App's numeric `app_id`. Legacy `contexts: string[]` is
|
|
23
|
+
`legacy_contexts_no_issuer_binding`. Name-only `checks[]` is
|
|
24
|
+
`no_issuer_binding`. A different `app_id` is `issuer_app_id_mismatch`.
|
|
25
|
+
- **YAML AST, not grep:** workflow files are parsed with `js-yaml` (already a
|
|
26
|
+
CLI dependency — no new package). VERIFIED requires a job that is not
|
|
27
|
+
`if: false` and a step whose `uses:` is `coderifts/contract-gate` at a
|
|
28
|
+
pinned major, or a `run:` that actually invokes `coderifts deploy-gate`.
|
|
29
|
+
Comments, string literals in other keys, and commented-out steps fail.
|
|
30
|
+
- **Environment correlation:** `environment_protection` is VERIFIED only when
|
|
31
|
+
`--env` names a protected GitHub Environment **and** an active deploy-gate
|
|
32
|
+
job sets that same `environment:` key (`environment_mismatch` otherwise).
|
|
33
|
+
|
|
3
34
|
## 4.8.0
|
|
4
35
|
|
|
5
36
|
### Fixed
|
package/README.md
CHANGED
|
@@ -437,12 +437,14 @@ Provider-native pipeline-protection evidence. This is **not** a host claim:
|
|
|
437
437
|
|
|
438
438
|
| Layer | VERIFIED when |
|
|
439
439
|
|-------|----------------|
|
|
440
|
-
| `required_check` | Default-branch protection
|
|
440
|
+
| `required_check` | Default-branch protection `required_status_checks.checks[]` lists `CodeRifts / contract-gate` **bound to the CodeRifts GitHub App `app_id`** (public metadata: `GET https://api.github.com/apps/coderifts`). Name-only `checks[]` (`app_id` null) is `no_issuer_binding`. The legacy `contexts` array has no issuer field → `legacy_contexts_no_issuer_binding`. Both are **NOT_VERIFIED** (ID621). |
|
|
441
441
|
| `enforce_admins` | `enforce_admins.enabled` is **true** (admins cannot bypass). `false` = NOT_VERIFIED (admin bypass open) |
|
|
442
442
|
| `strict_status_checks` | `required_status_checks.strict` is true |
|
|
443
|
-
| `environment_protection` | `--env` names a GitHub Environment with protection rules
|
|
444
|
-
| `workflow_contract_gate` |
|
|
445
|
-
| `workflow_deploy_gate` |
|
|
443
|
+
| `environment_protection` | `--env` names a GitHub Environment with protection rules **and** an active deploy-gate job sets that same `environment:` (mismatch → `environment_mismatch`) |
|
|
444
|
+
| `workflow_contract_gate` | an **active** job (not `if: false`) has a step whose `uses:` is `coderifts/contract-gate` at a pinned major. Comments, string literals, and commented-out steps are **NOT_VERIFIED** |
|
|
445
|
+
| `workflow_deploy_gate` | an **active** job step **invokes** `coderifts deploy-gate` (not `echo` of the string; not a comment) |
|
|
446
|
+
|
|
447
|
+
**Invariant:** any layer whose evidence cannot be bound to an issuer (our GitHub App id) or to an active job is `NOT_VERIFIED` or `UNVERIFIABLE` — never `VERIFIED`. `inescapable_deploy` remains true only when every layer is `VERIFIED` under that rule.
|
|
446
448
|
|
|
447
449
|
Statuses: `VERIFIED` / `NOT_VERIFIED` / `UNVERIFIABLE(reason)`. Auth:
|
|
448
450
|
`GITHUB_TOKEN` (never printed). **No token** → API layers `UNVERIFIABLE` with
|
|
@@ -451,9 +453,11 @@ CodeRifts GitHub App **cannot** query branch protection (permissions are
|
|
|
451
453
|
contents / pull requests / checks — not `administration:read`).
|
|
452
454
|
|
|
453
455
|
`--json` is the quotable evidence document (`spec`:
|
|
454
|
-
`provider-enforcement-evidence.
|
|
455
|
-
|
|
456
|
-
|
|
456
|
+
`provider-enforcement-evidence.v2` — VERIFIED now requires issuer binding and
|
|
457
|
+
an active YAML-AST job; v1 treated name-only checks and text search as
|
|
458
|
+
VERIFIED): each layer's status, `reason_code`, the API endpoint consulted,
|
|
459
|
+
`queried_at`. Exit **0** when a report was produced (evidence, not a gate).
|
|
460
|
+
GitLab/Bitbucket: no adapter — `UNVERIFIABLE`, never stubbed.
|
|
457
461
|
|
|
458
462
|
### Agent host files (`coderifts agent-setup`)
|
|
459
463
|
|
package/dist/cli.js
CHANGED
|
@@ -3028,7 +3028,7 @@ var require_package = __commonJS({
|
|
|
3028
3028
|
"package.json"(exports2, module2) {
|
|
3029
3029
|
module2.exports = {
|
|
3030
3030
|
name: "coderifts",
|
|
3031
|
-
version: "4.
|
|
3031
|
+
version: "4.9.0",
|
|
3032
3032
|
description: "Detect breaking API changes from the command line. Works locally or with the CodeRifts cloud API.",
|
|
3033
3033
|
author: "CodeRifts <hello@coderifts.com>",
|
|
3034
3034
|
license: "MIT",
|
|
@@ -200889,9 +200889,20 @@ var require_decision_result_v1_producer = __commonJS({
|
|
|
200889
200889
|
source: {
|
|
200890
200890
|
type: "string",
|
|
200891
200891
|
enum: [
|
|
200892
|
-
"github_compare"
|
|
200892
|
+
"github_compare",
|
|
200893
|
+
"gitlab_compare",
|
|
200894
|
+
"bitbucket_compare"
|
|
200893
200895
|
],
|
|
200894
|
-
description: "How the server listed the change-set. github_compare = GitHub Compare API via the App installation."
|
|
200896
|
+
description: "How the server listed the change-set. github_compare = GitHub Compare API via the App installation. gitlab_compare / bitbucket_compare = platform Compare via a caller-supplied short-lived SCM token (never stored)."
|
|
200897
|
+
},
|
|
200898
|
+
platform: {
|
|
200899
|
+
type: "string",
|
|
200900
|
+
enum: [
|
|
200901
|
+
"github",
|
|
200902
|
+
"gitlab",
|
|
200903
|
+
"bitbucket"
|
|
200904
|
+
],
|
|
200905
|
+
description: "Additive P1-6. SCM platform that produced this SERVER_DERIVED set. Taken from the proven binding / context.platform \u2014 never guessed from the repository string. Absent on envelopes minted before this field existed."
|
|
200895
200906
|
},
|
|
200896
200907
|
base_sha: {
|
|
200897
200908
|
type: "string",
|
|
@@ -201799,6 +201810,9 @@ var require_change_set_completeness = __commonJS({
|
|
|
201799
201810
|
if (v == null) return "";
|
|
201800
201811
|
return typeof v === "string" ? v : JSON.stringify(v);
|
|
201801
201812
|
}
|
|
201813
|
+
function afterContentHash(content) {
|
|
201814
|
+
return sha256hexLocal(specStr(content));
|
|
201815
|
+
}
|
|
201802
201816
|
function artifactPath(artifact) {
|
|
201803
201817
|
if (!artifact || typeof artifact !== "object") return "";
|
|
201804
201818
|
if (typeof artifact.path === "string" && artifact.path.trim()) return artifact.path.trim();
|
|
@@ -202097,7 +202111,8 @@ var require_change_set_completeness = __commonJS({
|
|
|
202097
202111
|
contract_selector_hash,
|
|
202098
202112
|
completeness_count: normalizedLeaves.length,
|
|
202099
202113
|
submitted_set_digest,
|
|
202100
|
-
expected_channel: "webhook_full_file_list"
|
|
202114
|
+
expected_channel: "webhook_full_file_list",
|
|
202115
|
+
leaves: normalizedLeaves
|
|
202101
202116
|
};
|
|
202102
202117
|
}
|
|
202103
202118
|
function assertCompletenessRequirement(block, requireLevel) {
|
|
@@ -202181,9 +202196,45 @@ var require_change_set_completeness = __commonJS({
|
|
|
202181
202196
|
contract_selector_hash: computeContractSelectorHash(DEFAULT_CONTRACT_SELECTOR),
|
|
202182
202197
|
completeness_count: leaves.length,
|
|
202183
202198
|
submitted_set_digest,
|
|
202184
|
-
expected_channel: null
|
|
202199
|
+
expected_channel: null,
|
|
202200
|
+
leaves
|
|
202185
202201
|
};
|
|
202186
202202
|
}
|
|
202203
|
+
function authorizedPathsFromArtifacts(artifacts) {
|
|
202204
|
+
const list = Array.isArray(artifacts) ? artifacts : [];
|
|
202205
|
+
const seen = /* @__PURE__ */ new Set();
|
|
202206
|
+
const rows = [];
|
|
202207
|
+
for (const a of list) {
|
|
202208
|
+
const path = artifactPath(a);
|
|
202209
|
+
if (!path || seen.has(path)) continue;
|
|
202210
|
+
seen.add(path);
|
|
202211
|
+
rows.push({
|
|
202212
|
+
path,
|
|
202213
|
+
after_hex: sha256hexLocal(specStr(a && a.after)),
|
|
202214
|
+
change_type: deriveChangeType(a && a.before, a && a.after)
|
|
202215
|
+
});
|
|
202216
|
+
}
|
|
202217
|
+
return rows;
|
|
202218
|
+
}
|
|
202219
|
+
function authorizedPathsForPersist(block, artifacts) {
|
|
202220
|
+
const submitted = authorizedPathsFromArtifacts(artifacts);
|
|
202221
|
+
const leaves = block && Array.isArray(block.leaves) ? block.leaves : null;
|
|
202222
|
+
if (!leaves || leaves.length === 0) return submitted;
|
|
202223
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
202224
|
+
for (const L of leaves) {
|
|
202225
|
+
if (!L || typeof L !== "object") continue;
|
|
202226
|
+
const path = String(L.path == null ? "" : L.path).trim();
|
|
202227
|
+
if (!path) continue;
|
|
202228
|
+
const after_hex = String(L.after_hex == null ? "" : L.after_hex).replace(/^sha256:/i, "").toLowerCase();
|
|
202229
|
+
if (!/^[0-9a-f]{64}$/.test(after_hex)) continue;
|
|
202230
|
+
byPath.set(path, {
|
|
202231
|
+
path,
|
|
202232
|
+
after_hex,
|
|
202233
|
+
change_type: normalizeChangeType(L.change_type)
|
|
202234
|
+
});
|
|
202235
|
+
}
|
|
202236
|
+
return submitted.map((row) => byPath.get(row.path) || row);
|
|
202237
|
+
}
|
|
202187
202238
|
function completenessToEnvelopeFields(block) {
|
|
202188
202239
|
if (!block || block.completeness_mode === "UNBOUND") {
|
|
202189
202240
|
return {
|
|
@@ -202230,7 +202281,10 @@ var require_change_set_completeness = __commonJS({
|
|
|
202230
202281
|
assertNoBooleanCompleteness,
|
|
202231
202282
|
completenessRequiredError,
|
|
202232
202283
|
sha256hexLocal,
|
|
202233
|
-
specStr
|
|
202284
|
+
specStr,
|
|
202285
|
+
afterContentHash,
|
|
202286
|
+
authorizedPathsFromArtifacts,
|
|
202287
|
+
authorizedPathsForPersist
|
|
202234
202288
|
};
|
|
202235
202289
|
}
|
|
202236
202290
|
});
|
|
@@ -202795,7 +202849,8 @@ var require_change_set = __commonJS({
|
|
|
202795
202849
|
assertCompletenessRequirement,
|
|
202796
202850
|
completenessToEnvelopeFields,
|
|
202797
202851
|
applyProvenRepoBinding,
|
|
202798
|
-
assertNoBooleanCompleteness
|
|
202852
|
+
assertNoBooleanCompleteness,
|
|
202853
|
+
authorizedPathsForPersist
|
|
202799
202854
|
} = require_change_set_completeness();
|
|
202800
202855
|
var { authorFingerprintBindingEnvelopeFields } = require_fingerprint_binding_gate();
|
|
202801
202856
|
var NUL = "";
|
|
@@ -203522,6 +203577,7 @@ var require_change_set = __commonJS({
|
|
|
203522
203577
|
}
|
|
203523
203578
|
}
|
|
203524
203579
|
const completenessEnvelope = completenessToEnvelopeFields(completenessBlock);
|
|
203580
|
+
const authorizedPaths = preflightMode === "authorize" ? authorizedPathsForPersist(completenessBlock, artifacts) : [];
|
|
203525
203581
|
const fingerprintBindingEnvelope = authorFingerprintBindingEnvelopeFields({
|
|
203526
203582
|
preflightMode
|
|
203527
203583
|
});
|
|
@@ -203583,6 +203639,7 @@ var require_change_set = __commonJS({
|
|
|
203583
203639
|
// body_hash stays byte-identical. Covered by body_hash when set; NOT in fingerprint.
|
|
203584
203640
|
...input && input.derivation_meta && typeof input.derivation_meta === "object" ? { derivation: {
|
|
203585
203641
|
source: String(input.derivation_meta.source || "github_compare"),
|
|
203642
|
+
platform: String(input.derivation_meta.platform || "github"),
|
|
203586
203643
|
base_sha: String(input.derivation_meta.base_sha || ""),
|
|
203587
203644
|
head_sha: String(input.derivation_meta.head_sha || "")
|
|
203588
203645
|
} } : {},
|
|
@@ -203777,6 +203834,12 @@ var require_change_set = __commonJS({
|
|
|
203777
203834
|
}
|
|
203778
203835
|
} catch (_) {
|
|
203779
203836
|
}
|
|
203837
|
+
if (preflightMode === "authorize" && Array.isArray(authorizedPaths) && authorizedPaths.length > 0) {
|
|
203838
|
+
Object.defineProperty(out, "_authorized_paths", {
|
|
203839
|
+
value: Object.freeze(authorizedPaths.map((r) => Object.freeze({ ...r }))),
|
|
203840
|
+
enumerable: false
|
|
203841
|
+
});
|
|
203842
|
+
}
|
|
203780
203843
|
return out;
|
|
203781
203844
|
}
|
|
203782
203845
|
module2.exports = {
|
|
@@ -237167,6 +237230,162 @@ var require_status = __commonJS({
|
|
|
237167
237230
|
}
|
|
237168
237231
|
});
|
|
237169
237232
|
|
|
237233
|
+
// src/provider/workflow-scan.js
|
|
237234
|
+
var require_workflow_scan = __commonJS({
|
|
237235
|
+
"src/provider/workflow-scan.js"(exports2, module2) {
|
|
237236
|
+
"use strict";
|
|
237237
|
+
var yaml = require_js_yaml();
|
|
237238
|
+
var CONTRACT_GATE_ACTION = "coderifts/contract-gate";
|
|
237239
|
+
var DEPLOY_GATE_INVOCATION = /^(?:npx(?:\s+--yes)?\s+|npm\s+exec\s+|pnpm\s+(?:dlx|exec)\s+|yarn(?:\s+(?:dlx|exec))?\s+)?coderifts\s+deploy-gate(?:\s|$)/;
|
|
237240
|
+
var NON_SHELL = /* @__PURE__ */ new Set(["python", "python3", "node", "ruby", "perl", "php", "pwsh", "powershell"]);
|
|
237241
|
+
function isPinnedMajor(ref) {
|
|
237242
|
+
const r = String(ref || "");
|
|
237243
|
+
if (/^v\d+(\.\d+){0,2}$/.test(r)) return true;
|
|
237244
|
+
if (/^[0-9a-f]{40}$/i.test(r)) return true;
|
|
237245
|
+
return false;
|
|
237246
|
+
}
|
|
237247
|
+
function parseActionUses(uses) {
|
|
237248
|
+
const raw = String(uses || "").trim();
|
|
237249
|
+
const at = raw.lastIndexOf("@");
|
|
237250
|
+
if (at <= 0) return { name: raw, ref: "", pinnedMajor: false };
|
|
237251
|
+
return {
|
|
237252
|
+
name: raw.slice(0, at),
|
|
237253
|
+
ref: raw.slice(at + 1),
|
|
237254
|
+
pinnedMajor: isPinnedMajor(raw.slice(at + 1))
|
|
237255
|
+
};
|
|
237256
|
+
}
|
|
237257
|
+
function isContractGateUses(uses) {
|
|
237258
|
+
const parsed = parseActionUses(uses);
|
|
237259
|
+
return parsed.name === CONTRACT_GATE_ACTION && parsed.pinnedMajor === true;
|
|
237260
|
+
}
|
|
237261
|
+
function isExplicitlyDisabled(node) {
|
|
237262
|
+
if (!node || typeof node !== "object" || !Object.prototype.hasOwnProperty.call(node, "if")) {
|
|
237263
|
+
return false;
|
|
237264
|
+
}
|
|
237265
|
+
const ifVal = node.if;
|
|
237266
|
+
if (ifVal === false || ifVal === 0 || ifVal === null) return true;
|
|
237267
|
+
if (typeof ifVal === "string") {
|
|
237268
|
+
const s = ifVal.trim().toLowerCase();
|
|
237269
|
+
if (s === "false" || s === "0" || s === "" || s === "null") return true;
|
|
237270
|
+
if (/^\$\{\{\s*(?:false|0|null)\s*\}\}$/.test(s)) return true;
|
|
237271
|
+
}
|
|
237272
|
+
return false;
|
|
237273
|
+
}
|
|
237274
|
+
function jobEnvironmentName(job) {
|
|
237275
|
+
if (!job || typeof job !== "object") return null;
|
|
237276
|
+
const env = job.environment;
|
|
237277
|
+
if (typeof env === "string") {
|
|
237278
|
+
const name = env.trim();
|
|
237279
|
+
return name || null;
|
|
237280
|
+
}
|
|
237281
|
+
if (env && typeof env === "object" && typeof env.name === "string") {
|
|
237282
|
+
const name = env.name.trim();
|
|
237283
|
+
return name || null;
|
|
237284
|
+
}
|
|
237285
|
+
return null;
|
|
237286
|
+
}
|
|
237287
|
+
function isNonPosixShell(shell) {
|
|
237288
|
+
if (shell == null || shell === "") return false;
|
|
237289
|
+
const first = String(shell).trim().split(/\s+/)[0].replace(/^.*\//, "").toLowerCase();
|
|
237290
|
+
return NON_SHELL.has(first);
|
|
237291
|
+
}
|
|
237292
|
+
function isDeployGateInvocation(run, shell) {
|
|
237293
|
+
if (isNonPosixShell(shell)) return false;
|
|
237294
|
+
const text = String(run || "");
|
|
237295
|
+
let heredoc = null;
|
|
237296
|
+
let quote = null;
|
|
237297
|
+
for (const rawLine of text.split("\n")) {
|
|
237298
|
+
let line = String(rawLine || "");
|
|
237299
|
+
if (heredoc) {
|
|
237300
|
+
if (line.trim() === heredoc) heredoc = null;
|
|
237301
|
+
continue;
|
|
237302
|
+
}
|
|
237303
|
+
const hd = /<<[-]?\s*['"]?(\w+)['"]?/.exec(line);
|
|
237304
|
+
if (hd) heredoc = hd[1];
|
|
237305
|
+
if (quote) {
|
|
237306
|
+
for (let i = 0; i < line.length; i++) {
|
|
237307
|
+
const ch = line[i];
|
|
237308
|
+
if (ch === quote && line[i - 1] !== "\\") {
|
|
237309
|
+
quote = null;
|
|
237310
|
+
break;
|
|
237311
|
+
}
|
|
237312
|
+
}
|
|
237313
|
+
if (quote) continue;
|
|
237314
|
+
}
|
|
237315
|
+
const trimmed = line.trim();
|
|
237316
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
237317
|
+
let cmd = trimmed.replace(/^(?:\w+=\S+\s+)+/, "");
|
|
237318
|
+
if (/^(echo|printf|cat|false|true|:)\b/.test(cmd)) {
|
|
237319
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
237320
|
+
const ch = cmd[i];
|
|
237321
|
+
if ((ch === "'" || ch === '"') && cmd[i - 1] !== "\\") {
|
|
237322
|
+
quote = quote ? quote === ch ? null : quote : ch;
|
|
237323
|
+
}
|
|
237324
|
+
}
|
|
237325
|
+
continue;
|
|
237326
|
+
}
|
|
237327
|
+
if (DEPLOY_GATE_INVOCATION.test(cmd)) return true;
|
|
237328
|
+
}
|
|
237329
|
+
return false;
|
|
237330
|
+
}
|
|
237331
|
+
function stepsOf(job) {
|
|
237332
|
+
if (!job || typeof job !== "object") return [];
|
|
237333
|
+
return Array.isArray(job.steps) ? job.steps : [];
|
|
237334
|
+
}
|
|
237335
|
+
function inspectJob(job) {
|
|
237336
|
+
const out = {
|
|
237337
|
+
contractGate: false,
|
|
237338
|
+
deployGate: false,
|
|
237339
|
+
environment: jobEnvironmentName(job),
|
|
237340
|
+
disabled: isExplicitlyDisabled(job)
|
|
237341
|
+
};
|
|
237342
|
+
if (out.disabled) return out;
|
|
237343
|
+
for (const step of stepsOf(job)) {
|
|
237344
|
+
if (!step || typeof step !== "object") continue;
|
|
237345
|
+
if (isExplicitlyDisabled(step)) continue;
|
|
237346
|
+
if (step.uses != null && isContractGateUses(step.uses)) out.contractGate = true;
|
|
237347
|
+
if (step.run != null && isDeployGateInvocation(step.run, step.shell)) out.deployGate = true;
|
|
237348
|
+
}
|
|
237349
|
+
return out;
|
|
237350
|
+
}
|
|
237351
|
+
function inspectWorkflowYaml(body) {
|
|
237352
|
+
const empty = { contractGate: false, deployGate: false, deployEnvironments: [] };
|
|
237353
|
+
let doc;
|
|
237354
|
+
try {
|
|
237355
|
+
doc = yaml.load(String(body || ""));
|
|
237356
|
+
} catch {
|
|
237357
|
+
return empty;
|
|
237358
|
+
}
|
|
237359
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return empty;
|
|
237360
|
+
const jobs = doc.jobs && typeof doc.jobs === "object" && !Array.isArray(doc.jobs) ? doc.jobs : null;
|
|
237361
|
+
if (!jobs) return empty;
|
|
237362
|
+
let contractGate = false;
|
|
237363
|
+
let deployGate = false;
|
|
237364
|
+
const deployEnvironments = [];
|
|
237365
|
+
for (const name of Object.keys(jobs)) {
|
|
237366
|
+
const info = inspectJob(jobs[name]);
|
|
237367
|
+
if (info.contractGate) contractGate = true;
|
|
237368
|
+
if (info.deployGate) {
|
|
237369
|
+
deployGate = true;
|
|
237370
|
+
if (info.environment) deployEnvironments.push(info.environment);
|
|
237371
|
+
}
|
|
237372
|
+
}
|
|
237373
|
+
return { contractGate, deployGate, deployEnvironments };
|
|
237374
|
+
}
|
|
237375
|
+
module2.exports = {
|
|
237376
|
+
CONTRACT_GATE_ACTION,
|
|
237377
|
+
isPinnedMajor,
|
|
237378
|
+
parseActionUses,
|
|
237379
|
+
isContractGateUses,
|
|
237380
|
+
isExplicitlyDisabled,
|
|
237381
|
+
jobEnvironmentName,
|
|
237382
|
+
isDeployGateInvocation,
|
|
237383
|
+
inspectJob,
|
|
237384
|
+
inspectWorkflowYaml
|
|
237385
|
+
};
|
|
237386
|
+
}
|
|
237387
|
+
});
|
|
237388
|
+
|
|
237170
237389
|
// src/provider/github-enforcement.js
|
|
237171
237390
|
var require_github_enforcement = __commonJS({
|
|
237172
237391
|
"src/provider/github-enforcement.js"(exports2, module2) {
|
|
@@ -237178,7 +237397,107 @@ var require_github_enforcement = __commonJS({
|
|
|
237178
237397
|
extractRequiredContexts
|
|
237179
237398
|
} = require_setup_required_check();
|
|
237180
237399
|
var { GATE_ACTION } = require_contract_gate_workflow();
|
|
237181
|
-
var
|
|
237400
|
+
var { inspectWorkflowYaml } = require_workflow_scan();
|
|
237401
|
+
var EVIDENCE_SPEC = "provider-enforcement-evidence.v2";
|
|
237402
|
+
var CODERIFTS_GITHUB_APP_SLUG = "coderifts";
|
|
237403
|
+
var CODERIFTS_GITHUB_APP_ID = 2860592;
|
|
237404
|
+
var REASON = Object.freeze({
|
|
237405
|
+
ISSUER_BOUND: "issuer_bound",
|
|
237406
|
+
NO_ISSUER_BINDING: "no_issuer_binding",
|
|
237407
|
+
LEGACY_CONTEXTS_NO_ISSUER_BINDING: "legacy_contexts_no_issuer_binding",
|
|
237408
|
+
ISSUER_APP_ID_MISMATCH: "issuer_app_id_mismatch",
|
|
237409
|
+
ISSUER_APP_ID_UNRESOLVABLE: "issuer_app_id_unresolvable",
|
|
237410
|
+
REQUIRED_CHECK_ABSENT: "required_check_absent",
|
|
237411
|
+
ACTIVE_JOB: "active_job",
|
|
237412
|
+
NO_ACTIVE_JOB: "no_active_job",
|
|
237413
|
+
ENVIRONMENT_CORRELATED: "environment_correlated",
|
|
237414
|
+
ENVIRONMENT_MISMATCH: "environment_mismatch"
|
|
237415
|
+
});
|
|
237416
|
+
function verifiedRequiresBinding(layer2) {
|
|
237417
|
+
if (!layer2 || layer2.status !== STATUS.VERIFIED) return true;
|
|
237418
|
+
if (layer2.id === "required_check") return layer2.reason_code === REASON.ISSUER_BOUND;
|
|
237419
|
+
if (layer2.id === "workflow_contract_gate" || layer2.id === "workflow_deploy_gate") {
|
|
237420
|
+
return layer2.reason_code === REASON.ACTIVE_JOB;
|
|
237421
|
+
}
|
|
237422
|
+
if (layer2.id === "environment_protection") {
|
|
237423
|
+
return layer2.reason_code === REASON.ENVIRONMENT_CORRELATED;
|
|
237424
|
+
}
|
|
237425
|
+
return true;
|
|
237426
|
+
}
|
|
237427
|
+
function expectedIssuerAppId() {
|
|
237428
|
+
const id = Number(CODERIFTS_GITHUB_APP_ID);
|
|
237429
|
+
return Number.isInteger(id) && id > 0 ? id : null;
|
|
237430
|
+
}
|
|
237431
|
+
function ourCheckEntries(rsc) {
|
|
237432
|
+
if (!rsc || !Array.isArray(rsc.checks)) return [];
|
|
237433
|
+
return rsc.checks.filter((c) => c && typeof c.context === "string" && c.context === CHECK_NAME);
|
|
237434
|
+
}
|
|
237435
|
+
function numericAppId(value) {
|
|
237436
|
+
if (value == null || value === "") return null;
|
|
237437
|
+
const n = Number(value);
|
|
237438
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
237439
|
+
}
|
|
237440
|
+
function evaluateRequiredCheck(protection) {
|
|
237441
|
+
const expected = expectedIssuerAppId();
|
|
237442
|
+
const rsc = protection && protection.required_status_checks;
|
|
237443
|
+
const contexts = extractRequiredContexts(protection);
|
|
237444
|
+
if (expected == null) {
|
|
237445
|
+
return {
|
|
237446
|
+
status: STATUS.UNVERIFIABLE,
|
|
237447
|
+
reason_code: REASON.ISSUER_APP_ID_UNRESOLVABLE,
|
|
237448
|
+
reason: "CodeRifts GitHub App id could not be determined from public metadata; required_check is UNVERIFIABLE (never VERIFIED without an issuer id)",
|
|
237449
|
+
required_contexts: contexts,
|
|
237450
|
+
app_id: null
|
|
237451
|
+
};
|
|
237452
|
+
}
|
|
237453
|
+
const ours = ourCheckEntries(rsc);
|
|
237454
|
+
if (ours.length > 0) {
|
|
237455
|
+
const bound = ours.find((c) => numericAppId(c.app_id) === expected);
|
|
237456
|
+
if (bound) {
|
|
237457
|
+
return {
|
|
237458
|
+
status: STATUS.VERIFIED,
|
|
237459
|
+
reason_code: REASON.ISSUER_BOUND,
|
|
237460
|
+
reason: `required_status_checks.checks binds "${CHECK_NAME}" to GitHub App id ${expected} (slug ${CODERIFTS_GITHUB_APP_SLUG})`,
|
|
237461
|
+
required_contexts: contexts,
|
|
237462
|
+
app_id: expected
|
|
237463
|
+
};
|
|
237464
|
+
}
|
|
237465
|
+
const other = ours.find((c) => numericAppId(c.app_id) != null);
|
|
237466
|
+
if (other) {
|
|
237467
|
+
return {
|
|
237468
|
+
status: STATUS.NOT_VERIFIED,
|
|
237469
|
+
reason_code: REASON.ISSUER_APP_ID_MISMATCH,
|
|
237470
|
+
reason: `required_status_checks.checks binds "${CHECK_NAME}" to app_id ${numericAppId(other.app_id)}, not CodeRifts App id ${expected}`,
|
|
237471
|
+
required_contexts: contexts,
|
|
237472
|
+
app_id: numericAppId(other.app_id),
|
|
237473
|
+
expected_app_id: expected
|
|
237474
|
+
};
|
|
237475
|
+
}
|
|
237476
|
+
return {
|
|
237477
|
+
status: STATUS.NOT_VERIFIED,
|
|
237478
|
+
reason_code: REASON.NO_ISSUER_BINDING,
|
|
237479
|
+
reason: `required_status_checks.checks lists "${CHECK_NAME}" with no app_id (name-only; any repository writer can post a success status with that context)`,
|
|
237480
|
+
required_contexts: contexts,
|
|
237481
|
+
app_id: null
|
|
237482
|
+
};
|
|
237483
|
+
}
|
|
237484
|
+
if (contexts.includes(CHECK_NAME)) {
|
|
237485
|
+
return {
|
|
237486
|
+
status: STATUS.NOT_VERIFIED,
|
|
237487
|
+
reason_code: REASON.LEGACY_CONTEXTS_NO_ISSUER_BINDING,
|
|
237488
|
+
reason: `required_status_checks.contexts lists "${CHECK_NAME}" but the legacy contexts array has no app_id at all (legacy_contexts_no_issuer_binding)`,
|
|
237489
|
+
required_contexts: contexts,
|
|
237490
|
+
app_id: null
|
|
237491
|
+
};
|
|
237492
|
+
}
|
|
237493
|
+
return {
|
|
237494
|
+
status: STATUS.NOT_VERIFIED,
|
|
237495
|
+
reason_code: REASON.REQUIRED_CHECK_ABSENT,
|
|
237496
|
+
reason: `required_status_checks does not include "${CHECK_NAME}" (have: ${contexts.join(", ") || "none"})`,
|
|
237497
|
+
required_contexts: contexts,
|
|
237498
|
+
app_id: null
|
|
237499
|
+
};
|
|
237500
|
+
}
|
|
237182
237501
|
var APP_INSTALLATION = Object.freeze({
|
|
237183
237502
|
can_query_protection: false,
|
|
237184
237503
|
permissions: Object.freeze([
|
|
@@ -237344,12 +237663,14 @@ var require_github_enforcement = __commonJS({
|
|
|
237344
237663
|
} else {
|
|
237345
237664
|
const protection = prot.body && typeof prot.body === "object" ? prot.body : {};
|
|
237346
237665
|
const endpoint = prot.endpoint ? `GET ${prot.endpoint}` : null;
|
|
237347
|
-
const
|
|
237348
|
-
|
|
237349
|
-
|
|
237350
|
-
|
|
237666
|
+
const required = evaluateRequiredCheck(protection);
|
|
237667
|
+
layers.push(layer("required_check", required.status, {
|
|
237668
|
+
reason: required.reason,
|
|
237669
|
+
reason_code: required.reason_code,
|
|
237351
237670
|
endpoint,
|
|
237352
|
-
required_contexts:
|
|
237671
|
+
required_contexts: required.required_contexts,
|
|
237672
|
+
app_id: required.app_id,
|
|
237673
|
+
...required.expected_app_id != null ? { expected_app_id: required.expected_app_id } : {}
|
|
237353
237674
|
}));
|
|
237354
237675
|
const admins = enforceAdminsEnabled(protection);
|
|
237355
237676
|
layers.push(layer("enforce_admins", admins ? STATUS.VERIFIED : STATUS.NOT_VERIFIED, {
|
|
@@ -237401,24 +237722,55 @@ var require_github_enforcement = __commonJS({
|
|
|
237401
237722
|
}));
|
|
237402
237723
|
} else {
|
|
237403
237724
|
const ok = environmentProtected(environmentRead.body);
|
|
237404
|
-
|
|
237405
|
-
|
|
237406
|
-
|
|
237407
|
-
|
|
237408
|
-
|
|
237725
|
+
const referenced = (localFiles.deployGateEnvironments || []).map(String);
|
|
237726
|
+
const correlated = ok && environmentName && referenced.includes(environmentName);
|
|
237727
|
+
if (correlated) {
|
|
237728
|
+
layers.push(layer("environment_protection", STATUS.VERIFIED, {
|
|
237729
|
+
reason: `GitHub Environment "${environmentName}" has protection rules and is referenced by an active deploy-gate job (environment:)`,
|
|
237730
|
+
reason_code: REASON.ENVIRONMENT_CORRELATED,
|
|
237731
|
+
endpoint: environmentRead.endpoint ? `GET ${environmentRead.endpoint}` : null,
|
|
237732
|
+
environment: environmentName,
|
|
237733
|
+
workflow_environments: referenced
|
|
237734
|
+
}));
|
|
237735
|
+
} else if (!ok) {
|
|
237736
|
+
layers.push(layer("environment_protection", STATUS.NOT_VERIFIED, {
|
|
237737
|
+
reason: `GitHub Environment "${environmentName}" exists but has no protection_rules and no deployment_branch_policy`,
|
|
237738
|
+
reason_code: REASON.ENVIRONMENT_MISMATCH,
|
|
237739
|
+
endpoint: environmentRead.endpoint ? `GET ${environmentRead.endpoint}` : null,
|
|
237740
|
+
environment: environmentName,
|
|
237741
|
+
workflow_environments: referenced
|
|
237742
|
+
}));
|
|
237743
|
+
} else {
|
|
237744
|
+
layers.push(layer("environment_protection", STATUS.NOT_VERIFIED, {
|
|
237745
|
+
reason: `GitHub Environment "${environmentName}" is protected but no active deploy-gate job sets environment: ${environmentName} (have: ${referenced.join(", ") || "none"})`,
|
|
237746
|
+
reason_code: REASON.ENVIRONMENT_MISMATCH,
|
|
237747
|
+
endpoint: environmentRead.endpoint ? `GET ${environmentRead.endpoint}` : null,
|
|
237748
|
+
environment: environmentName,
|
|
237749
|
+
workflow_environments: referenced
|
|
237750
|
+
}));
|
|
237751
|
+
}
|
|
237409
237752
|
}
|
|
237410
237753
|
const cg = localFiles.contractGateFiles || [];
|
|
237411
237754
|
layers.push(layer("workflow_contract_gate", cg.length ? STATUS.VERIFIED : STATUS.NOT_VERIFIED, {
|
|
237412
|
-
reason: cg.length ? `
|
|
237755
|
+
reason: cg.length ? `active job step uses ${GATE_ACTION} (${cg.join(", ")})` : `no active .github/workflows job uses ${GATE_ACTION} (comments, disabled jobs, and string literals do not count)`,
|
|
237756
|
+
reason_code: cg.length ? REASON.ACTIVE_JOB : REASON.NO_ACTIVE_JOB,
|
|
237413
237757
|
endpoint: "local:.github/workflows",
|
|
237414
237758
|
files: cg
|
|
237415
237759
|
}));
|
|
237416
237760
|
const dg = localFiles.deployGateFiles || [];
|
|
237417
237761
|
layers.push(layer("workflow_deploy_gate", dg.length ? STATUS.VERIFIED : STATUS.NOT_VERIFIED, {
|
|
237418
|
-
reason: dg.length ? `
|
|
237762
|
+
reason: dg.length ? `active job step invokes coderifts deploy-gate (${dg.join(", ")})` : "no active .github/workflows job invokes coderifts deploy-gate (comments, disabled jobs, and string literals do not count)",
|
|
237763
|
+
reason_code: dg.length ? REASON.ACTIVE_JOB : REASON.NO_ACTIVE_JOB,
|
|
237419
237764
|
endpoint: "local:.github/workflows",
|
|
237420
237765
|
files: dg
|
|
237421
237766
|
}));
|
|
237767
|
+
for (const l of layers) {
|
|
237768
|
+
if (l.status === STATUS.VERIFIED && !verifiedRequiresBinding(l)) {
|
|
237769
|
+
l.status = STATUS.NOT_VERIFIED;
|
|
237770
|
+
l.reason_code = l.reason_code || REASON.NO_ISSUER_BINDING;
|
|
237771
|
+
l.reason = `${l.reason || l.id} \u2014 downgraded: VERIFIED requires issuer or active-job binding`;
|
|
237772
|
+
}
|
|
237773
|
+
}
|
|
237422
237774
|
const byId = Object.fromEntries(layers.map((l) => [l.id, l]));
|
|
237423
237775
|
const inescapable = LAYER_IDS.every((id) => byId[id] && byId[id].status === STATUS.VERIFIED);
|
|
237424
237776
|
return {
|
|
@@ -237426,7 +237778,7 @@ var require_github_enforcement = __commonJS({
|
|
|
237426
237778
|
inescapable_deploy: inescapable,
|
|
237427
237779
|
claim: {
|
|
237428
237780
|
inescapable_deploy: inescapable,
|
|
237429
|
-
basis: inescapable ? "all six layers VERIFIED
|
|
237781
|
+
basis: inescapable ? "all six layers VERIFIED (issuer-bound required check + active workflow jobs + correlated environment)" : "inescapable_deploy stays false until every layer is VERIFIED"
|
|
237430
237782
|
}
|
|
237431
237783
|
};
|
|
237432
237784
|
}
|
|
@@ -237437,12 +237789,13 @@ var require_github_enforcement = __commonJS({
|
|
|
237437
237789
|
const dir = path.join(cwd, ".github", "workflows");
|
|
237438
237790
|
const contractGateFiles = [];
|
|
237439
237791
|
const deployGateFiles = [];
|
|
237440
|
-
|
|
237792
|
+
const deployGateEnvironments = [];
|
|
237793
|
+
if (!exists(dir)) return { contractGateFiles, deployGateFiles, deployGateEnvironments };
|
|
237441
237794
|
let names = [];
|
|
237442
237795
|
try {
|
|
237443
237796
|
names = readdir(dir);
|
|
237444
237797
|
} catch {
|
|
237445
|
-
return { contractGateFiles, deployGateFiles };
|
|
237798
|
+
return { contractGateFiles, deployGateFiles, deployGateEnvironments };
|
|
237446
237799
|
}
|
|
237447
237800
|
for (const name of names) {
|
|
237448
237801
|
if (!/\.ya?ml$/i.test(name)) continue;
|
|
@@ -237453,10 +237806,18 @@ var require_github_enforcement = __commonJS({
|
|
|
237453
237806
|
} catch {
|
|
237454
237807
|
continue;
|
|
237455
237808
|
}
|
|
237456
|
-
|
|
237457
|
-
if (
|
|
237809
|
+
const evidence = inspectWorkflowYaml(body);
|
|
237810
|
+
if (evidence.contractGate) contractGateFiles.push(rel);
|
|
237811
|
+
if (evidence.deployGate) {
|
|
237812
|
+
deployGateFiles.push(rel);
|
|
237813
|
+
for (const envName of evidence.deployEnvironments) {
|
|
237814
|
+
if (envName && !deployGateEnvironments.includes(envName)) {
|
|
237815
|
+
deployGateEnvironments.push(envName);
|
|
237816
|
+
}
|
|
237817
|
+
}
|
|
237818
|
+
}
|
|
237458
237819
|
}
|
|
237459
|
-
return { contractGateFiles, deployGateFiles };
|
|
237820
|
+
return { contractGateFiles, deployGateFiles, deployGateEnvironments };
|
|
237460
237821
|
}
|
|
237461
237822
|
async function queryGitHubEnforcement({
|
|
237462
237823
|
owner,
|
|
@@ -237491,12 +237852,17 @@ var require_github_enforcement = __commonJS({
|
|
|
237491
237852
|
NO_TOKEN_INSTRUCTION,
|
|
237492
237853
|
LAYER_IDS,
|
|
237493
237854
|
STATUS,
|
|
237855
|
+
REASON,
|
|
237494
237856
|
CHECK_NAME,
|
|
237495
237857
|
GATE_ACTION,
|
|
237858
|
+
CODERIFTS_GITHUB_APP_ID,
|
|
237859
|
+
CODERIFTS_GITHUB_APP_SLUG,
|
|
237496
237860
|
resolveGitHubAuth,
|
|
237497
237861
|
redactSecrets,
|
|
237498
237862
|
createGitHubHttpClient,
|
|
237499
237863
|
evaluateLayers,
|
|
237864
|
+
evaluateRequiredCheck,
|
|
237865
|
+
verifiedRequiresBinding,
|
|
237500
237866
|
scanLocalWorkflows,
|
|
237501
237867
|
queryGitHubEnforcement,
|
|
237502
237868
|
enforceAdminsEnabled,
|
|
@@ -237572,7 +237938,8 @@ var require_enforce_check = __commonJS({
|
|
|
237572
237938
|
const missing = report.layers.filter((l) => l.status !== "VERIFIED");
|
|
237573
237939
|
for (const l of missing) {
|
|
237574
237940
|
if (l.id === "required_check") {
|
|
237575
|
-
lines.push(chalk.dim(' require "CodeRifts / contract-gate"
|
|
237941
|
+
lines.push(chalk.dim(' require "CodeRifts / contract-gate" bound to the CodeRifts GitHub App (app_id)'));
|
|
237942
|
+
lines.push(chalk.dim(" on default-branch protection \u2014 name-only and legacy contexts[] are NOT_VERIFIED"));
|
|
237576
237943
|
lines.push(chalk.dim(" (coderifts setup-required-check --apply)"));
|
|
237577
237944
|
} else if (l.id === "enforce_admins") {
|
|
237578
237945
|
lines.push(chalk.dim(" set enforce_admins: true so administrators cannot bypass"));
|
|
@@ -237580,12 +237947,13 @@ var require_enforce_check = __commonJS({
|
|
|
237580
237947
|
} else if (l.id === "strict_status_checks") {
|
|
237581
237948
|
lines.push(chalk.dim(" set required_status_checks.strict: true"));
|
|
237582
237949
|
} else if (l.id === "environment_protection") {
|
|
237583
|
-
lines.push(chalk.dim(" add a GitHub Environment with protection rules, pass --env <name
|
|
237950
|
+
lines.push(chalk.dim(" add a GitHub Environment with protection rules, pass --env <name>,"));
|
|
237951
|
+
lines.push(chalk.dim(" and set that same name on the deploy-gate job's environment: key"));
|
|
237584
237952
|
} else if (l.id === "workflow_contract_gate") {
|
|
237585
|
-
lines.push(chalk.dim(" add
|
|
237586
|
-
lines.push(chalk.dim(" (coderifts init --agents)"));
|
|
237953
|
+
lines.push(chalk.dim(" add an active job step: uses: coderifts/contract-gate@v0"));
|
|
237954
|
+
lines.push(chalk.dim(" (comments / if: false / wrong org do not count; coderifts init --agents)"));
|
|
237587
237955
|
} else if (l.id === "workflow_deploy_gate") {
|
|
237588
|
-
lines.push(chalk.dim(" add
|
|
237956
|
+
lines.push(chalk.dim(" add an active CD step that invokes: coderifts deploy-gate --env <env> --artifact <id> --receipt <file>"));
|
|
237589
237957
|
}
|
|
237590
237958
|
}
|
|
237591
237959
|
}
|
package/package.json
CHANGED