# The rules that made every change to the mainline arrive through a reviewed pull request, as a signed commit, onto a history that cannot be rewritten — together with the record of when those rules themselves last changed and who changed them, and the platform's own per-push record of any change that got past them.

> FedRAMP Consolidated Rules for 2026 v2026.07.14.01 · updated 2026-07-14
> Canonical page: /collect/developer-change-control-and-integrity

Recipe id: `developer-change-control-and-integrity` · 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-SCR-MIT`
- control `sa-10`

## 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"
# ruleset-history
gh api --paginate "/repos/<ORG>/<REPO>/rulesets/<RULESET_ID>/history?per_page=100"
# ruleset-history-version
gh api "/repos/<ORG>/<REPO>/rulesets/<RULESET_ID>/history/<VERSION_ID>"
# rule-suites
gh api --paginate "/repos/<ORG>/<REPO>/rulesets/rule-suites?ref=refs/heads/<DEFAULT_BRANCH>&time_period=month&evaluate_status=active&rule_suite_result=bypass&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, `ruleset-history` the fourth, `ruleset-version` the fifth, `rule-suites-bypassed` the sixth.

RUN COMMANDS 2, 3 AND 6 ONCE PER REPOSITORY AND ASSESSED BRANCH; run command 4 ONCE PER RULESET returned by command 3, and command 5 once per version of a ruleset you actually want to read. Every clause below speaks only for the repository and branch walked.

`branch-rules` and `rulesets` return the shapes described in the sibling recipe `change-verification-status-checks-and-run-records`, and the rule types this recipe filters on are `pull_request`, `required_signatures` and `non_fast_forward`. The `pull_request` rule's parameters are `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`. `non_fast_forward` is the rule GitHub describes in its interface as blocking force pushes, and `required_signatures` the one under which "contributors and bots can only push commits that have been signed and verified to the branch".

From `ruleset-history`, one entry per version of the ruleset: a `version_id`, an `actor` with an `id` and a `type`, and an `updated_at`. It says WHEN the change-control rules changed and WHO changed them, and it does not say what they changed to. From `ruleset-version`, the same three fields plus a `state` object, which is the ruleset as it stood at that version — this is the command to run when a history entry needs to become an answer.

From `rule-suites-bypassed`, the push-evaluation records filtered by GitHub's own `rule_suite_result` parameter, documented as accepting `pass`, `fail`, `bypass` and `all`. The command passes `bypass`, so the response is built to contain only the pushes that got past the rules, each with `actor_name`, `before_sha`, `after_sha`, `ref` and `pushed_at`. `time_period` tops out at `month` and no retention period is documented for these records. The command also passes `evaluate_status=active` — documented as returning "only rule suites resulting from rulesets in active (non-evaluate) mode" — so the bypasses counted are bypasses of rules that were actually enforcing.

## Assertions

- {"field":"branch-rules[?type=='pull_request'] | [0]","op":"exists","controls":["sa-10"],"description":"A pull-request rule is in effect on the assessed branch, so no change reaches the mainline except as a proposal — GitHub's wording is that the rule requires \"that all changes to the target branch be associated with a pull request\". Read from the endpoint that returns rules in effect, so an organization-level ruleset satisfies it exactly as a repository-level one does. It says changes are routed through a review mechanism; the next clause says whether that mechanism requires anybody."}
- {"field":"branch-rules[?type=='pull_request' && (!parameters || parameters.required_approving_review_count==`0`)]","op":"count_eq","value":0,"controls":["sa-10"],"description":"No pull-request rule in effect requires zero approvals. This is the clause above's other half: zero is a legal, selectable value for `required_approving_review_count`, and a rule set to it routes every change through a pull request the author can merge unread. Both limbs are needed because JMESPath treats the number zero as truthy — `!parameters.required_approving_review_count` cannot catch a count of zero, and the equality test alone cannot catch a rule object with no parameters — so the disjunction catches the empty process and the malformed row alike."}
- {"field":"branch-rules[?type=='required_signatures'] | [0]","op":"exists","controls":["sa-10"],"description":"A required-signatures rule is in effect, so that under it \"contributors and bots can only push commits that have been signed and verified to the branch\". This is the integrity of a change made checkable after the fact: each change carries a cryptographic claim about its author. It asserts that the RULE is in force, not that any given commit is signed — history predating the rule is untouched by it — and a signature attributes a change without authorizing it."}
- {"field":"branch-rules[?type=='non_fast_forward'] | [0]","op":"exists","controls":["sa-10"],"description":"A rule blocking force pushes is in effect on the assessed branch. It is asserted rather than left as hygiene because every other artifact this recipe collects is a claim ABOUT a history — a merge record, a signature, an approval — and all of them are statements about commits a force push can silently replace. This clause is the precondition of the others being worth reading, rather than a fourth independent property."}
- {"field":"rulesets[?enforcement=='evaluate' && target=='branch']","op":"count_eq","value":0,"controls":["sa-10"],"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."}

## Outside the boundary

Platform: GitHub Enterprise Cloud
SA-10 is a control about the DEVELOPER's configuration management, and on this plane the developer's configuration management system is a third party's hosted service. Adopting it to collect this evidence makes the provider's own change-control mechanism an external information resource under SA-09 and CA-03, and one that belongs in the external-system inventory AC-20 already covers. The consequence is sharper here than on a scanning control, because what is outsourced is not the detection of a problem but the ENFORCEMENT of the process: the rules that decide which changes may reach the mainline are held, evaluated and logged by the vendor, and the ruleset history that says who changed those rules is held by the same vendor. SA-10 is a class c and class d control, so every reader 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. No status value is written down here, because a certification status is a dated fact that changes.

## Notes

SA-10 requires the provider to make the developer perform configuration management during development, implementation and operation; to document, manage and control the integrity of changes; to implement ONLY ORGANIZATION-APPROVED changes; to document approved changes and their potential security impacts; and to track flaws. This output answers the integrity-and-control limb with mechanism rather than with assertion: a rule that every change arrives through a pull request, a rule that its commits are signed, a rule that the history behind it cannot be rewritten, and the platform's own record of anything that got past all three. The gap the disposition named is the word ORGANIZATION-APPROVED. The configuration management plan naming the approving authority and the classes of change it covers is a document, and a merge record cannot show that the person who approved held that authority. Nothing here closes that, and the rating is unchanged by having written the commands down.

A PULL-REQUEST RULE THAT REQUIRES NO APPROVAL. `required_approving_review_count` is a documented, selectable parameter and zero is a legal value for it. A `pull_request` rule with zero required approvals routes every change through a pull request and lets its author merge it unread — it satisfies the first clause, appears in every listing, and is a process with nobody in it. The second clause counts it, and it carries `!parameters` as a disjunct because the zero test alone cannot see a rule object with no parameters at all: JMESPath treats the number zero as truthy, so `!parameters.required_approving_review_count` would NOT catch a count of zero and `parameters.required_approving_review_count == 0` would not catch an absent object. The two together catch both, and a rule that legitimately carries no parameters fails loudly rather than passing silently.

THE DRY-RUN TRAP is closed by the endpoint the first four clauses read, not by the fifth, and the first draft of this batch had that backwards in both siblings. GitHub documents `/rules/branches/{branch}` as returning "all active rules that apply to the specified branch" and says that "Rules in rulesets with \"evaluate\" or \"disabled\" enforcement statuses are not returned" — so a `pull_request`, `required_signatures` or `non_fast_forward` rule found there is enforcing, and a ruleset in dry-run is invisible to those clauses rather than satisfying them. That matters more here than in the sibling recipe, because four of this recipe's five clauses read that endpoint. The fifth is kept as an independent finding — a branch ruleset believed to be in force that blocks nothing — and not as the guard the draft called it.

SIGNED COMMITS PROVE ATTRIBUTION, NOT AUTHORITY. Under `required_signatures`, "contributors and bots can only push commits that have been signed and verified to the branch", which is the integrity limb of SA-10 in the most literal available sense: every change carries a cryptographic claim about who made it, checkable long after the fact. What a signature does not carry is that the signer was entitled to make that change, which is the organization-approved gap again one layer down. Note also what the clause asserts and does not: that the RULE is in effect, not that any particular commit on the branch is signed. A history that predates the rule is unaffected by it, and nothing in this recipe reads the commits themselves.

BLOCKING FORCE PUSHES IS WHAT MAKES THE REST OF THE RECORD EVIDENCE. `non_fast_forward` is the rule GitHub describes as preventing force pushes, and it is asserted here rather than treated as hygiene because every other artifact this recipe collects is a claim about a history: a merge record, a signature, an approval. All of them are statements about commits that a force push can replace. The fourth clause is therefore not a fourth control property but the precondition of the other three being worth reading.

THE HISTORY ENDPOINT IS THE ONE PIECE OF EVIDENCE ABOUT THE CONTROL ITSELF, AND IT CARRIES NO CLAUSE ON PURPOSE. `/rulesets/{ruleset_id}/history` returns a `version_id`, an `actor` and an `updated_at` per version — who changed the change-control rules, and when. That is as close as this plane comes to configuration management OF the configuration management, and no threshold over it would mean anything: one entry is a ruleset created once and never touched, which is a good state, and forty entries is a ruleset under active maintenance, which is also a good state. It is collected because an assessment reading a green ruleset today needs to know whether it was green last quarter, and the version list is the only thing here that answers. Turning an entry into an answer takes the per-version command, which returns the ruleset's `state` as it stood.

THE BYPASS LIST IS FILTERED BY THE VENDOR, AND THAT IS WHY ITS EMPTINESS MEANS SOMETHING. The sixth command passes `rule_suite_result=bypass`, so the response is constructed to hold only the pushes that got past the rules; an empty response is the meaningful kind of empty rather than the ambiguous kind, in the way a `status=failure` list is and a full list is not. Its bounds are the same as the sibling recipe's — at most a month, and only as much of that month as the platform retains — which is why the cadence here is weekly rather than monthly despite everything else in the recipe being configuration that changes rarely.

KSI-SCR-MIT asks the provider to persistently identify, review and mitigate potential supply chain risks, and this recipe is claimed on the MITIGATE limb alone. The developer is a supply chain participant, and constraining what any developer — or any credential belonging to one — can put into the mainline is a mitigation of that participant's risk. It is not an identification of any risk and not a review of one: nothing here surfaces a finding, ranks it, or records a decision about it. The identify and review limbs of this indicator are carried on this plane by `dependency-vulnerability-monitoring`, and an indicator page carrying this recipe should be read as carrying evidence for a part.

The evidence platform is itself an external system; see `external_system`. Two sibling recipes in the same batch read overlapping commands for different controls, and are separate recipes for the reason given in `change-verification-status-checks-and-run-records`.

ONE LIMB OF THE DISPOSITION IS NOT CARRIED FORWARD, and it is recorded here because deleting the disposition deletes the only other place it was written down. The rationale this recipe spends argued from "branch protection, required reviews, signed commits AND MERGE RECORDS", and there is no merge record in this recipe: no `/pulls`, no `/commits`, no review endpoint. What replaced that limb is the rule that makes a pull request compulsory and the bypass list that says nothing got around it, which is the mechanism rather than the instances. The instances are collected on this plane by `security-representative-change-approval`, under a different control, and an assessment wanting per-change evidence for SA-10 should read that recipe's fifth and sixth commands beside this one. The rating is unaffected — `partial` was right on either reading — but the covered ground is narrower than the register said it would be.

The type-string-to-interface-name join is unsourced and is stated here rather than implied. `required_signatures` and `non_fast_forward` are the API `type` values, taken from the organization-rules reference; "Require signed commits" and "Block force pushes" are the names the ruleset documentation uses for the same two rules. No page cited here maps one spelling to the other. The mapping is not in doubt, and it is not cited either, which is the honest way to leave it.

## 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: 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 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: 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/"}
