# For every change that reached the assessed branch, the automated verification that ran against it — which workflows ran, on which commit, and what each concluded — together with the two things that decide whether those runs were a condition of the change or merely adjacent to it: the rule that made the checks required, and the platform's own per-push record of whether that rule held, failed, or was bypassed. The runs alone are activity; the rule and the per-push record are what make them a gate.

> FedRAMP Consolidated Rules for 2026 v2026.07.14.01 · updated 2026-07-14
> Canonical page: /collect/change-verification-status-checks-and-run-records

Recipe id: `change-verification-status-checks-and-run-records` · cadence weekly · partial

> **Authored opinion.** pipeline overlay v0.8.0, written
> against dataset 2026.07.14.01. The upstream
> FedRAMP rules name none of these tools; this mapping is ours.

## What it proves

- KSI `KSI-CMT-VTD`
- KSI `KSI-CMT-LMC`
- control `cm-4.2`

## Collection

Kind: `api`

```sh
# repos
gh api --paginate "/orgs/<ORG>/repos?per_page=100"
# branch-rules
gh api "/repos/<ORG>/<REPO>/rules/branches/<DEFAULT_BRANCH>"
# rulesets
gh api --paginate "/repos/<ORG>/<REPO>/rulesets?includes_parents=true&per_page=100"
# rule-suites
gh api --paginate "/repos/<ORG>/<REPO>/rulesets/rule-suites?ref=refs/heads/<DEFAULT_BRANCH>&time_period=month&evaluate_status=active&per_page=100"
# runs-completed
gh api --paginate "/repos/<ORG>/<REPO>/actions/runs?branch=<DEFAULT_BRANCH>&status=completed&per_page=100"
# runs-failure
gh api --paginate "/repos/<ORG>/<REPO>/actions/runs?branch=<DEFAULT_BRANCH>&status=failure&per_page=100"
```

## Expected output

Report names used below, and the command each comes from: `repos` is the first command, `branch-rules` the second, `rulesets` the third, `rule-suites` the fourth, `runs-completed` the fifth, `runs-failure` the sixth.

RUN COMMANDS 2 THROUGH 6 ONCE PER REPOSITORY AND ONCE PER ASSESSED BRANCH, not once for the organization. The first command returns the repository list and the rest take a single `<REPO>` and a single `<DEFAULT_BRANCH>`; every clause below speaks only for the repository and branch actually walked, and collecting one repository's rules and reading them as the organization's is the arithmetic error this recipe is most likely to be assessed with.

From `branch-rules`, an array of the repository rule objects IN EFFECT on that branch, each carrying a `type` drawn from `creation`, `update`, `deletion`, `required_linear_history`, `required_deployments`, `required_signatures`, `pull_request`, `required_status_checks`, `non_fast_forward`, `commit_message_pattern`, `commit_author_email_pattern`, `committer_email_pattern`, `branch_name_pattern`, `tag_name_pattern`, `file_path_restriction`, `max_file_path_length`, `file_extension_restriction`, `max_file_size`, `workflows`, `code_scanning` and `copilot_code_review`, and a `parameters` object whose shape depends on the type. For `required_status_checks` the parameters are a `required_status_checks` array of `{context, integration_id}` entries, `strict_required_status_checks_policy` and `do_not_enforce_on_create`.

From `rulesets`, the rulesets that apply to the repository — `includes_parents` defaults to TRUE, so organization and enterprise rulesets are included and `source_type` (`Repository`, `Organization`, `Enterprise`) says which is which. Each carries `id`, `name`, a `target` of `branch`, `tag`, `push` or `repository`, an `enforcement` of `disabled`, `active` or `evaluate`, `bypass_actors` entries with an `actor_type` of `Integration`, `OrganizationAdmin`, `RepositoryRole`, `Team`, `DeployKey` or `User` and a `bypass_mode` of `always`, `pull_request` or `exempt`, plus `current_user_can_bypass`, `created_at` and `updated_at`.

From `rule-suites`, one record per push evaluated against the rules: `id`, `actor_id`, `actor_name`, `before_sha`, `after_sha`, `ref`, `repository_id`, `repository_name`, `pushed_at`, a `result` of `pass`, `fail` or `bypass`, and an `evaluation_result`. The per-suite endpoint adds `rule_evaluations`, each with a `rule_source`, a `rule_type`, a `result` and an `enforcement` of `active`, `evaluate` or `deleted ruleset` — worth fetching for any suite this recipe's clauses flag, because it is the only place that says WHICH rule decided. `time_period` accepts `hour`, `day`, `week` and `month` and defaults to `day`; the command above passes `month`, which is the documented ceiling, and `evaluate_status=active`, documented as returning "only rule suites resulting from rulesets in active (non-evaluate) mode" — without it the response mixes pushes measured against enforcing rulesets with pushes measured against dry-run ones, and nothing on the page describes what `result` holds for the second kind.

From `runs-completed` and `runs-failure`, workflow runs with `id`, `name`, `head_branch`, `head_sha`, `path`, `run_number`, `run_attempt`, `event`, `status`, a `conclusion` — documented on the workflow-runs page as a string or null and NOT as an enum, so treat `success`, `failure`, `neutral`, `cancelled`, `skipped`, `timed_out` and `action_required` as the values to expect rather than as a closed set this citation establishes — `workflow_id`, `created_at`, `updated_at`, `run_started_at`, `triggering_actor`, `head_commit` and `referenced_workflows`. `failure` is a value of the `status` FILTER as well as of `conclusion`, so the sixth command returns a list built to hold only failing runs.

## Assertions

- {"field":"branch-rules[?type=='required_status_checks'] | [0]","op":"exists","controls":["cm-4.2"],"description":"A required-status-checks rule is in effect on the assessed branch — GitHub's own words for it are that such checks \"ensure that all required CI tests are passing before collaborators can make changes to a branch\". Read from `/rules/branches/{branch}`, which returns the rules in effect rather than the rulesets that might contain them, so an organization-level ruleset counts here exactly as a repository-level one does. It says a gate is declared; it does not say the gate names any check, which is the next clause, and it does not say the ruleset behind it enforces anything, which is the one after."}
- {"field":"branch-rules[?type=='required_status_checks' && !(parameters.required_status_checks)]","op":"count_eq","value":0,"controls":["cm-4.2"],"description":"No required-status-checks rule in effect declares an empty set of checks. This is the clause the one above cannot be trusted without: the rule is present in every listing, reads as a gate in every screenshot, and requires nothing. Written as an offender count over rows present in the same response. THE PARENTHESES ARE LOAD-BEARING and were absent in the first draft of this batch: `!parameters.X` parses as `(!parameters).X`, which evaluates to null, which is falsy, so the unparenthesised clause selects no row on any input and is green by construction. `!(parameters.required_status_checks)` is the form that works, and it catches the empty array and the absent key alike, because both are falsy — where `length()` would raise on the row that has no `parameters` at all."}
- {"field":"rulesets[?enforcement=='evaluate' && target=='branch']","op":"count_eq","value":0,"controls":["cm-4.2"],"description":"No BRANCH ruleset applying to this repository is in dry-run mode. This clause is NOT what makes the clauses above trustworthy, and the first draft of this batch claimed it was. The endpoint those clauses read settles it instead: GitHub documents `/rules/branches/{branch}` as returning \"all active rules that apply to the specified branch\" and states that \"Rules in rulesets with \\\"evaluate\\\" or \\\"disabled\\\" enforcement statuses are not returned\" — so a rule that appears there is enforcing, and an evaluating ruleset is invisible to every `exists` clause rather than passing one. What this clause is, then, is a separate finding: a repository operating under a branch ruleset that somebody believes is in force and that blocks nothing. `evaluate` is the status the rule-suites API distinguishes when it offers to return \"only rule suites resulting from rulesets in active (non-evaluate) mode\". Narrowed to `target=='branch'` because the same response carries `tag`, `push` and `repository` rulesets, and a tag ruleset left evaluating has no bearing on the change path this recipe assesses."}
- {"field":"rule-suites[?result=='bypass']","op":"count_eq","value":0,"controls":["cm-4.2"],"description":"No change reached the assessed branch in the retrieved window by bypassing the rules that verify it. The list is the platform's own per-push evaluation record, so a bypassed push is present-and-labelled rather than absent — this is the rare clause on this plane whose population is built by the vendor to contain the failure. The command passes `evaluate_status=active`, documented as returning \"only rule suites resulting from rulesets in active (non-evaluate) mode\", so a push measured against a dry-run ruleset is not counted here. Its window is at most a month, because `time_period` tops out there and no retention period is documented for these records at all, and it is green on a repository with no ruleset at all — which is why it is read with the first clause and not alone. Unlike a push-protection bypass, this record carries no reason field, so it cannot be read as a per-event authorized exception; what it can be read against is `bypass_actors` in the `rulesets` response, which is the STANDING authorization, and a provider with a designated break-glass integration will fail this clause while operating as configured."}

## Outside the boundary

Platform: GitHub Enterprise Cloud
There is no independent observer anywhere in this recipe, and that is a sharper external-system question than the usual one. The rule that makes verification a condition of a change, the evaluation of that rule, and the log of that evaluation are three views held by one party, and the workflow runs the rule points at are executed by the same party on the same infrastructure. A provider's assurance that its changes were verified therefore rests entirely on an outside vendor's account of its own enforcement, which makes the platform an information resource whose potential impact has to be addressed under SA-09 and CA-03 and accounted for in the external-system inventory AC-20 already covers. CM-04 (02) is a class c and class d control, so every reader of this recipe runs a Moderate or High system, and the offering named above is not necessarily the one such a reader can use: the Marketplace listing cited below is the listing for GitHub Enterprise Cloud, and a class c or class d reader has to check it against the class they operate at and look to whichever offering answers it — GitHub Enterprise Cloud for Government is a different product on different infrastructure with its own listing. That is the SA-09 question here. It is answered by reading the listing rather than by this sentence, and no status value is written down, because a certification status is a dated fact that changes.

## Notes

CM-04 (02) requires the provider to verify that the IMPACTED CONTROLS are implemented correctly, operating as intended, and producing the desired outcome. This output shows that automated checks ran against each change, that passing them was a condition of the change, and what each concluded. Which controls a change impacts, and whether a passing test verifies one of them, is a mapping from test to control that a human writes; a pipeline that publishes such a mapping is publishing an authored artifact rather than a measurement. The disposition this recipe spends said exactly that, and writing the commands down has not changed the rating.

THE DRY-RUN TRAP, AND THE ENDPOINT CHOICE THAT ALREADY CLOSED IT. A ruleset in `evaluate` mode is a complete, well-formed set of rules that enforces nothing, and it is this plane's empty list wearing a different coat: it appears in `/rulesets` intact and generates rule suite records. The first draft of this batch hedged about whether `/rules/branches/{branch}` returns such rules, called it a live risk, and told the reader to make one call against a live repository to settle it. That was a manufactured uncertainty, and the page the recipe already cites settles it in one sentence: the endpoint "Returns all active rules that apply to the specified branch", and "Rules in rulesets with \"evaluate\" or \"disabled\" enforcement statuses are not returned." So reading the rules IN EFFECT rather than the rulesets that contain them is what closes the trap here, and it was closed before any clause was written. The consequence runs both ways and both are worth stating: the first two clauses are stronger than the draft claimed, because a rule they find is enforcing by construction; and the evaluate clause is weaker, because it guards nothing above it. It stays as a finding in its own right — a branch ruleset somebody believes is in force and that blocks nothing — narrowed to `target=='branch'`, since the same response carries tag, push and repository rulesets whose dry-run state says nothing about this change path.

A REQUIRED-CHECKS RULE THAT REQUIRES NOTHING. The `required_status_checks` rule takes a `required_status_checks` array of contexts, and an empty one is a rule that is present in every listing, satisfies the first clause, and gates on no check whatsoever. The second clause is spelled `!parameters.required_status_checks` rather than with `length()` deliberately: an empty array and an absent key are both falsy under that spelling and both are genuine offenders, while `length()` raises on the row where `parameters` is missing entirely — which is the row most worth seeing. A rule object that legitimately carries no `parameters` would fail this clause; that is a false red that makes somebody look, which is the direction this repo has chosen every time it has had the choice.

WHY THE BYPASS CLAUSE IS ASSERTED HERE AND THE PUSH-PROTECTION ONE WAS NOT. `secret-exposure-detection-and-push-protection` deliberately does not count push-protection bypasses to zero, because a bypass there is a documented decision with a named actor and one of three stated reasons, and a provider with a legitimate test fixture would fail such a clause while behaving correctly. A rule suite bypass has no reason field in anything fetched: the record carries `actor_name`, `result` and the rule evaluations, and nothing that says why. It therefore cannot be read as an authorized exception — all it says is that a change reached the assessed branch without the verification the rule required, which for CM-04 (02) is the finding rather than a footnote. What it CAN be read against, and the first draft of this batch missed this, is `bypass_actors` — returned in the same `rulesets` response this recipe already collects, with a `bypass_mode` of `always`, `pull_request` or `exempt`. That is a standing authorization to bypass, configured and visible, so a provider with a designated break-glass integration fails this clause while operating exactly as configured. The per-event reason is absent; the standing permission is not, and a failure of this clause is read against that list before it is read as a finding. Note one collection caveat that would make any future clause over `bypass_actors` vacuous: GitHub documents that "To prevent leaking sensitive information, the bypass_actors property is only returned if the user making the API request has write access to the ruleset", so a read-only assessment token sees an empty bypass list rather than an error.

THE WINDOW, AND WHY THE CADENCE IS WEEKLY. `time_period` accepts `hour`, `day`, `week` and `month`, defaults to `day`, and `month` is the ceiling — and no retention period is documented for rule suite records at all. The bypass clause is therefore a statement about at most a month, and about however much of that month the platform still holds. Collecting monthly against a monthly ceiling leaves no margin: a collection that slips by a day loses records silently rather than erroring, and the records it loses are exactly the ones nobody saw. Weekly is four chances at the same evidence.

WHY NO CLAUSE COUNTS THE FAILED RUNS. The sixth command returns a list built to contain offenders, and it carries no assertion. A workflow run that failed on the protected branch is the verification apparatus working — the desired state is that failures happen and are addressed, not that none occur — and a scheduled nightly that failed once says nothing about whether any change was verified. Both run lists are corroboration for a human: that runs exist at all, that they run on the branch the rules protect, and that their names correspond to the contexts the required-checks rule names. Nothing here checks that last correspondence, and it is the join an assessment most wants — a required context is a string, a workflow run has a name, and no response in this recipe says the string was produced by the run.

NO CLAUSE READS A POSITION, deliberately. Nothing fetched documents the sort order of `/actions/runs`, so nothing here reads `[0]` as "the most recent". The immediately preceding batch on this plane put a freshness clause on `analyses[0]` and had to be corrected, because position zero was whichever language finished last. The same shape is available here and is not taken; a freshness claim over these runs would need a `created` range in the query rather than an index into the response.

KSI-CMT-VTD is the direct claim: persistent testing and validation of changes throughout deployment, automated. KSI-CMT-LMC — modifications to the offering are logged and monitored — is claimed on the LOGGED limb only. The rule suite record is a per-push log of every change measured against the rules, and the workflow runs are a per-change record of what ran against it, which is logging in the most literal sense. Monitored is weaker and is not claimed: nothing in this recipe routes any of it to a person or to an alert, and no threshold in it fires.

The evidence platform is itself an external system; see `external_system`. Two sibling recipes in the same batch — `developer-change-control-and-integrity` and `security-representative-change-approval` — read overlapping commands for different controls. They are separate recipes rather than one, because a single recipe carrying all three would put one `automatable` rating and one assertion set over three separate arguments, and the argument is the thing being assessed.

## References

- {"title":"GitHub REST: repository rules — GET /repos/{owner}/{repo}/rules/branches/{branch} returning the rule objects in effect on a branch, GET /repos/{owner}/{repo}/rulesets with includes_parents defaulting to true, and the ruleset history endpoints; ruleset fields id, name, target branch | tag | push | repository, source_type Repository | Organization | Enterprise, enforcement disabled | active | evaluate, bypass_actors with actor_type Integration | OrganizationAdmin | RepositoryRole | Team | DeployKey | User and bypass_mode always | pull_request | exempt, current_user_can_bypass, rules, created_at, updated_at","url":"https://docs.github.com/en/rest/repos/rules"}
- {"title":"GitHub REST: organization rules — the same ruleset schema at organization scope, and the source of the exact rule `type` strings this recipe filters on: creation, update, deletion, required_linear_history, required_deployments, required_signatures, pull_request, required_status_checks, non_fast_forward, commit_message_pattern, branch_name_pattern, file_path_restriction, workflows, code_scanning; the pull_request rule's parameters, of which this batch reads required_approving_review_count, dismiss_stale_reviews_on_push, require_code_owner_review, require_last_push_approval, required_review_thread_resolution and allowed_merge_methods, the page also documenting dismissal_restriction and required_reviewers; the required_status_checks rule's parameters required_status_checks (context, integration_id), strict_required_status_checks_policy and do_not_enforce_on_create","url":"https://docs.github.com/en/rest/orgs/rules"}
- {"title":"GitHub REST: rule suites — GET /repos/{owner}/{repo}/rulesets/rule-suites and .../rule-suites/{rule_suite_id}; query parameters ref, time_period hour | day | week | month defaulting to day, actor_name, rule_suite_result \"Can be one of: pass, fail, bypass, all\" defaulting to all, and evaluate_status documented as \"active - only rule suites resulting from rulesets in active (non-evaluate) mode will be returned\"; response fields id, actor_id, actor_name, before_sha, after_sha, ref, repository_id, repository_name, pushed_at, result pass | fail | bypass and evaluation_result, with rule_evaluations carrying rule_source, rule_type, result and an enforcement of active | evaluate | \"deleted ruleset\"; NO retention period is stated for these records","url":"https://docs.github.com/en/rest/repos/rule-suites"}
- {"title":"GitHub REST: workflow runs — GET /repos/{owner}/{repo}/actions/runs; the status filter accepting completed, action_required, cancelled, failure, neutral, skipped, stale, success, timed_out, in_progress, queued, requested, waiting and pending, plus actor, branch, event, created, exclude_pull_requests, check_suite_id and head_sha; run fields id, name, head_branch, head_sha, path, run_number, run_attempt, event, status, conclusion (documented as a string or null, WITHOUT an enum on this page — the familiar success/failure/neutral/cancelled/skipped/timed_out/action_required list belongs to check runs and is not sourced here), workflow_id, created_at, updated_at, run_started_at, triggering_actor, head_commit and referenced_workflows","url":"https://docs.github.com/en/rest/actions/workflow-runs"}
- {"title":"GitHub: available rules for rulesets — \"Required status checks ensure that all required CI tests are passing before collaborators can make changes to a branch or tag\", the strict \"Require branches to be up to date before merging\" option, \"You can require that all changes to the target branch be associated with a pull request\" with its approving-review count, stale-approval dismissal, code-owner review and last-push options, \"When you enable required commit signing on a branch, contributors and bots can only push commits that have been signed and verified to the branch\", \"You can prevent users from force pushing to the targeted branches or tags. This rule is enabled by default\", and the bypass-permission paragraph","url":"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets"}
- {"title":"GitHub: creating rulesets for a repository — the enforcement statuses, \"Active: your ruleset will be enforced upon creation\" and \"Disabled: your ruleset will not be enforced\", and the third status this recipe's clause exists for, offered \"to test the effects of any metadata restrictions without impacting contributors\"; also \"You can grant certain roles, teams, or apps bypass permissions for your ruleset\"","url":"https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/creating-rulesets-for-a-repository"}
- {"title":"FedRAMP Marketplace: GitHub Enterprise Cloud — the listing to re-read rather than quote, and the place to check the impact level against the class c and class d systems this control applies to; a certification status is a dated fact about a product, not about your deployment","url":"https://www.fedramp.gov/marketplace/products/FR1812058188/"}
