# The rule requiring the designated owners of the changed code to approve before it merges, the file that names who those owners are, the platform's own report of whether that file actually parses — and, per change, who approved, on which commit, and when.

> FedRAMP Consolidated Rules for 2026 v2026.07.14.01 · updated 2026-07-14
> Canonical page: /collect/security-representative-change-approval

Recipe id: `security-representative-change-approval` · cadence continuous · 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-RVP`
- KSI `KSI-PIY-RSD`
- control `cm-3.4`

## 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>"
# codeowners-errors
gh api "/repos/<ORG>/<REPO>/codeowners/errors?ref=<DEFAULT_BRANCH>"
# codeowners-file
gh api -H "Accept: application/vnd.github.raw" "/repos/<ORG>/<REPO>/contents/.github/CODEOWNERS?ref=<DEFAULT_BRANCH>"
# pulls-closed
gh api --paginate "/repos/<ORG>/<REPO>/pulls?state=closed&base=<DEFAULT_BRANCH>&per_page=100"
# pull-reviews
gh api --paginate "/repos/<ORG>/<REPO>/pulls/<PULL_NUMBER>/reviews?per_page=100"
```

## Expected output

Report names used below, and the command each comes from: `repos` is the first command, `branch-rules` the second, `codeowners-errors` the third, `codeowners-file` the fourth, `pulls-closed` the fifth, `reviews` the sixth.

RUN COMMANDS 2 THROUGH 5 ONCE PER REPOSITORY AND ASSESSED BRANCH, and command 6 once per pull request selected from the fifth. The fourth and fifth commands are a SAMPLE AND WALK for a human reader, not a population any clause below reads — the closed pull request list is unbounded and no assertion here is written over it.

From `branch-rules`, the rule objects in effect on the branch, of which this recipe reads the `pull_request` type and its parameters `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`.

From `codeowners-errors`, an object with an `errors` array, each error carrying `line`, `column`, `kind`, `source`, `suggestion`, `message` and `path`. The `ref` parameter is "A branch, tag or commit name used to determine which version of the CODEOWNERS file to use" and DEFAULTS TO THE REPOSITORY'S DEFAULT BRANCH — the command passes it explicitly, because a recipe assessing any other branch would otherwise be checking a different file from the one in force there.

From `codeowners-file`, the file itself. GitHub documents three legal locations — "the .github/, root, or docs/ directory of the repository" — so a 404 from the path above is not an absence, it is the wrong one of the three; try `CODEOWNERS` and `docs/CODEOWNERS` before concluding anything.

From `pulls-closed`, the closed pull requests targeting the branch, and from `reviews`, the review records: `id`, `user`, `body`, a `state` — the page documents it as a required string and enumerates nothing, so `APPROVED`, `COMMENTED`, `DISMISSED` and `PENDING` are the values to expect rather than a closed set, and no clause here depends on the enum being complete, `html_url`, `submitted_at`, `commit_id` and an `author_association` of `COLLABORATOR`, `CONTRIBUTOR`, `FIRST_TIMER`, `FIRST_TIME_CONTRIBUTOR`, `MANNEQUIN`, `MEMBER`, `NONE` or `OWNER`. `commit_id` is the field that makes a review an answer rather than a timestamp: it says WHICH revision was approved.

## Assertions

- {"field":"branch-rules[?type=='pull_request' && parameters.require_code_owner_review==`true`] | [0]","op":"exists","controls":["cm-3.4"],"description":"A pull-request rule in effect on the assessed branch requires review from the code owners of the changed files — the platform's mechanism for putting a designated reviewer on every change that touches what they own. This is the membership claim in its enforceable form. It says a designated owner must approve; it does not say any owner is actually designated, which is the next clause, and it does not say the owner is the security representative CM-03 (04) names, which is the gap no clause closes."}
- {"field":"codeowners-errors.errors","op":"count_eq","value":0,"controls":["cm-3.4"],"description":"The CODEOWNERS file in force on the assessed branch parses without a single syntax error. This is the clause the one above is worthless without, and the reason is the vendor's own sentence: \"If any line in your CODEOWNERS file contains invalid syntax, that line will be skipped.\" A skipped line does not fail anything — it silently removes the owners of the paths it covered, so the rule above stays green while requiring approval from nobody for exactly the directory whose line was broken. The endpoint that reports these errors is the only thing in this recipe that can see it. Its silence has a residue: nothing documents what it returns when there is no CODEOWNERS file at all, so read a zero here together with the file the third command fetches."}
- {"field":"branch-rules[?type=='pull_request' && !(parameters.dismiss_stale_reviews_on_push)]","op":"count_eq","value":0,"controls":["cm-3.4"],"description":"No pull-request rule in effect lets an approval survive a later push. Without this parameter a code owner approves, the author pushes again, and the approval carries over to code no owner has seen — the record afterwards shows a designated reviewer approving the pull request, which is true, and does not show that they approved what merged. Offender form over rows present in the same response, and the parentheses are load-bearing: `!parameters.X` parses as `(!parameters).X`, which is null, which is falsy, so the unparenthesised form selects nothing at all. `!(parameters.dismiss_stale_reviews_on_push)` catches the parameter set to false and the parameter absent alike, both of which leave approvals standing."}

## Outside the boundary

Platform: GitHub Enterprise Cloud
What this recipe reads is a membership claim, and a membership claim is an identity claim: the platform's account of which of its users approved which change. Bringing it into the assessment makes the platform an information resource under SA-09 and CA-03 and an entry in the external-system inventory AC-20 already covers, and it does so for a control about WHO IS ON a body rather than about what a scanner found — so the dependency runs through the provider's identity federation as well as through the platform itself. A code owner is a platform identity, and the chain from that identity to a named person holding a named role runs through the identity provider, the group memberships synchronised into the platform, and the platform's own account of who clicked approve. CM-03 (04) 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

CM-03 (04) requires an information security representative to be a MEMBER of the configuration change control element. This is the closest this plane comes to a membership claim it can check: a rule requiring the designated owners of the changed code to approve, a file naming who those owners are, and per-change approval records carrying identities, revisions and timestamps. What none of it establishes is that those identities ARE the security and privacy representatives the control names. That mapping lives in the change control charter, and a repository team called `security` is an assertion about a name. The gap is an identity mapping rather than a missing signal, which is exactly what the disposition said, and no amount of further collection on this platform closes it.

THE SKIPPED LINE — this recipe's vacuity trap, and unusually it is documented by the vendor rather than inferred from an API shape. GitHub states plainly that "If any line in your CODEOWNERS file contains invalid syntax, that line will be skipped." So a `pull_request` rule with `require_code_owner_review` set to true, sitting over a CODEOWNERS file whose one line covering the infrastructure directory has a typo, requires review from the code owners of that directory — of which there are now none. The rule is green, the platform requests no reviewer, and the change merges on whatever approvals its author could find. Nothing in the rules API can see this, and nothing in a screenshot of the branch protection settings can either. `GET /repos/{owner}/{repo}/codeowners/errors` is the only endpoint in this recipe that sees it, and it is the entire reason the second clause exists.

A MISSING FILE AND A CLEAN FILE ARE NOT DISTINGUISHED BY THE CLAUSE, AND THE THIRD COMMAND IS WHY. Nothing fetched documents what the errors endpoint returns when there is no CODEOWNERS file at all, so an empty `errors` array is the passing shape for a valid file and the plausible shape for an absent one — the plane's standing emptiness problem, arriving through a door that looks like a validator. The third command reads the file itself for exactly this reason: it is collected so a human can confirm there IS a file and see who it names. No clause is written over its contents, because who ought to own which path is the identity mapping this recipe cannot make, and a clause counting lines would be a clause asserting that a file is long.

WHY THE STALE-APPROVAL CLAUSE IS HERE AND A LAST-PUSH ONE IS NOT. `dismiss_stale_reviews_on_push` is what stops an approval from outliving the change it approved: without it, a code owner approves, the author pushes again, and the approval carries over to code no owner has seen — a membership failure wearing the appearance of a membership success, and the one failure mode in this recipe that leaves a complete and convincing paper trail. `require_last_push_approval`, GitHub's option to "require an approval from someone other than the last person to push to a branch", is deliberately NOT asserted: it is a separation-of-duty property rather than a membership one, a small team can be operating correctly without it, and CM-03 (04) asks who sits on the element rather than whether two distinct people acted.

WHAT A REVIEW RECORD PROVES AND WHERE IT STOPS. A review carries `state`, `user`, `submitted_at`, `commit_id` and `author_association`, so the record says a named platform identity approved a named revision at a named time — and `commit_id` is what makes it an approval of something rather than a timestamp. It does not say that the identity belongs to a person, that the person holds the role, or that the approval was informed. `author_association` is the closest thing to a role anywhere in the response and it is a repository-relationship enum — `MEMBER`, `COLLABORATOR`, `OWNER` — describing a relationship to the repository rather than a position in the organization, so no clause is written over it. The fourth and fifth commands are collected as a sample for a human to read against the charter, and no assertion reads them; a clause over an unbounded list of closed pull requests would be a clause about how many were sampled.

NO `enforcement` CLAUSE APPEARS IN THIS RECIPE, and after the audit of this batch that is the correct answer for a better reason than the one first written. Every clause here except the second reads `/rules/branches/{branch}`, which GitHub documents as returning "all active rules that apply to the specified branch" and from which "Rules in rulesets with \"evaluate\" or \"disabled\" enforcement statuses are not returned" — so a dry-run ruleset cannot satisfy the first or third clause, and no guard against it is needed. The second clause reads a file parser that no ruleset governs at all: a CODEOWNERS file with a syntax error is broken whether the ruleset requiring code-owner review is active, evaluating or absent. An assessment wanting the dry-run finding itself gets it from `developer-change-control-and-integrity` on the same repository, where it belongs, being a property of the repository rather than of this control.

KSI-CMT-RVP — the effectiveness of documented change management procedures is persistently reviewed — is claimed on the MECHANISM limb. A required approval per change, with the approver and the revision recorded, is the material a review of procedure effectiveness reads, and the rule is the documented procedure in executable form. Whether the procedure is EFFECTIVE is a judgement, and no API in this recipe makes it. KSI-PIY-RSD — the effectiveness of building security and privacy considerations into the Software Development Lifecycle and aligning with CISA Secure By Design principles is persistently reviewed — is claimed narrowly and should be read narrowly: requiring a security owner's approval at merge is one security consideration built into one point of the lifecycle. The indicator is about the whole lifecycle and about alignment with an external body of principles that nothing here reads.

The evidence platform is itself an external system, and on this control the dependency runs through the identity provider as well; see `external_system`.

## References

- {"title":"GitHub: about code owners — a CODEOWNERS file lives in \"the .github/, root, or docs/ directory of the repository\"; \"Code owners are automatically requested for review when someone opens a pull request that modifies code that they own\"; \"The people you choose as code owners must have write permissions for the repository\"; \"When the code owner is a team, that team must be visible and it must have write permissions, even if all the individual members of the team already have write permissions directly\"; and the sentence this recipe's second clause exists for — \"If any line in your CODEOWNERS file contains invalid syntax, that line will be skipped\"","url":"https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners"}
- {"title":"GitHub REST: repositories — GET /repos/{owner}/{repo}/codeowners/errors, which lists \"any syntax errors that are detected in the CODEOWNERS file\"; its ref parameter is \"A branch, tag or commit name used to determine which version of the CODEOWNERS file to use. Default: the repository's default branch\", and each error carries line, column, kind, source, suggestion, message and path","url":"https://docs.github.com/en/rest/repos/repos"}
- {"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: pull request reviews — GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews; review fields id, node_id, user, body, state (documented as a required string with NO enum on this page; APPROVED, COMMENTED, DISMISSED and PENDING are the values to expect, not a closed set this citation establishes), html_url, pull_request_url, submitted_at, commit_id, and author_association, which IS enumerated: COLLABORATOR, CONTRIBUTOR, FIRST_TIMER, FIRST_TIME_CONTRIBUTOR, MANNEQUIN, MEMBER, NONE, OWNER","url":"https://docs.github.com/en/rest/pulls/reviews"}
- {"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/"}
