Audit Logs, Security Policies, Compliance Evidence, Repository Governance, and Enterprise Controls: Guided Hands-On Workflow and Core Operations
Build a disposable governance-evidence repository, normalize realistic audit events, and map repository controls to expected evidence without requiring paid enterprise features.
Learning objectives
- Create a disposable public governance-evidence repository.
- Add human-readable policy and ownership files without confusing them with enforcement.
- Normalize time-bounded audit events into an evidence report while retaining raw input.
- Map expected GitHub audit events to ruleset/team/security-configuration changes.
- Verify local Git state, hosted repository state, hashes, and optional organization audit-log evidence.
1. Scenario and availability
You are preparing evidence for a fictional service named
service-api. The mandatory lab needs only GitHub Free,
a public disposable personal repository, GitHub CLI, Git, and Python
3. Organization audit-log mutation/API access is not required. If
you already control a disposable organization, an optional section
lets you inspect its audit-log UI. If that organization is on
Enterprise Cloud, you can additionally use the REST audit-log
endpoint.
learner-example. Do not export employer audit logs,
collaborator identities, IP addresses, token metadata, or
security-event details into a public training repository.
2. Preflight: prove the target before creating it
gh auth status
gh api -H "X-GitHub-Api-Version: 2026-03-10" user --jq '{login,id}'
OWNER="YOUR_LOGIN"
REPO_NAME="c29-governance-evidence-lab"
REPO="$OWNER/$REPO_NAME"
gh repo view "$REPO" --json nameWithOwner,visibility 2>/dev/null || echo "Expected: disposable repo does not exist yet"
The first command proves which account the CLI will use. The repository lookup prevents accidentally reusing a valuable repository with a similar name. No organization is necessary because the live GitHub changes in the mandatory path are ordinary repository files only.
3. Create the disposable repository and capture baseline state
gh repo create "$REPO" --public --clone --description "Chapter 29 governance evidence lab"
cd "$REPO_NAME"
printf '# Chapter 29 governance evidence lab
' > README.md
git add README.md
git commit -m "Initialize Chapter 29 lab"
git push -u origin HEAD
BASE_SHA=$(git rev-parse HEAD)
printf 'baseline_commit=%s
' "$BASE_SHA"
gh repo view "$REPO" --json nameWithOwner,visibility,defaultBranchRef,url
gh api -H "X-GitHub-Api-Version: 2026-03-10" "repos/$REPO/rulesets" --jq '.'
Expected state: one public disposable repository, one initial
commit, and normally an empty ruleset array. Record
BASE_SHA; Git commit identity is evidence about
repository content, not evidence about every hosted setting.
4. Add policy and ownership documents through Git reviewable state
SECURITY.md communicates vulnerability-reporting
expectations. CODEOWNERS declares ownership. A control
matrix connects policy intent to enforcement/evidence. These are
intentionally separate files so their different roles remain
visible.
mkdir -p .github governance/evidence
cat > SECURITY.md <<'EOF'
# Security Policy
## Supported versions
This disposable training repository supports only the current default branch.
## Reporting
Do not post real credentials or vulnerabilities here.
For the lab, use only fictional examples and learner@example.invalid.
EOF
printf '/governance/ @%s
/SECURITY.md @%s
' "$OWNER" "$OWNER" > .github/CODEOWNERS
cat > governance/CONTROL_MATRIX.md <<'EOF'
# Control Matrix
| Objective | Declared control | Enforcement | Evidence | Owner | Review |
|---|---|---|---|---|---|
| Security reports follow a private path | SECURITY.md | Human process | file SHA + reviewed commit | Security | quarterly |
| Governance changes have an owner | CODEOWNERS | Optional ruleset review | CODEOWNERS SHA + PR/check evidence | Platform | quarterly |
| Protected refs follow merge policy | Repository ruleset | GitHub ruleset | ruleset state + audit event where available | Platform | quarterly |
EOF
git add SECURITY.md .github/CODEOWNERS governance/CONTROL_MATRIX.md
git diff --cached --check
git commit -m "Add governance policy and ownership evidence"
git push
POLICY_SHA=$(git rev-parse HEAD)
printf 'policy_commit=%s
' "$POLICY_SHA"
Prediction before pushing: the Git ref will advance and the three files will exist at the new commit, but adding Markdown and CODEOWNERS does not automatically create a branch ruleset. Verify that prediction independently.
git show --stat --oneline "$POLICY_SHA"
gh api -H "X-GitHub-Api-Version: 2026-03-10" "repos/$REPO/contents/SECURITY.md?ref=$POLICY_SHA" --jq '{path,sha,size}'
gh api -H "X-GitHub-Api-Version: 2026-03-10" "repos/$REPO/contents/.github/CODEOWNERS?ref=$POLICY_SHA" --jq '{path,sha,size}'
gh api -H "X-GitHub-Api-Version: 2026-03-10" "repos/$REPO/rulesets" --jq '[.[] | {id,name,enforcement}]'
5. Build a realistic but synthetic raw audit-event set
The following action names and field families are documented GitHub audit events. The actors, organization, repository, timestamps, IDs, and request IDs are fictional. Save the raw fixture exactly as collected; normalization happens into a separate file.
[
{
"_document_id": "fixture-team-add-001",
"@timestamp": 1787175000000,
"action": "team.add_repository",
"actor": "learner-example",
"org": "octo-c29-fixture",
"repo": "octo-c29-fixture/service-api",
"team": "octo-c29-fixture/release",
"permission": "push",
"request_id": "REQ-FIXTURE-001"
},
{
"_document_id": "fixture-team-update-002",
"@timestamp": 1787175600000,
"action": "team.update_repository_permission",
"actor": "learner-example",
"org": "octo-c29-fixture",
"repo": "octo-c29-fixture/service-api",
"team": "octo-c29-fixture/release",
"old_repo_permission": "push",
"new_repo_permission": "maintain",
"request_id": "REQ-FIXTURE-002"
},
{
"_document_id": "fixture-ruleset-003",
"@timestamp": 1787176200000,
"action": "repository_ruleset.create",
"actor": "learner-example",
"org": "octo-c29-fixture",
"repo": "octo-c29-fixture/service-api",
"ruleset_name": "main-governance",
"ruleset_enforcement": "active",
"request_id": "REQ-FIXTURE-003"
},
{
"_document_id": "fixture-security-config-004",
"@timestamp": 1787176800000,
"action": "security_configuration.create",
"actor": "learner-example",
"org": "octo-c29-fixture",
"security_configuration_name": "public-baseline",
"request_id": "REQ-FIXTURE-004"
}
]
Save it as governance/evidence/audit-raw.json. The raw
file is evidence input. Do not “clean” it in place.
6. Normalize fields without destroying the source
This standard-library Python script hashes the raw input, parses it, sorts by timestamp, writes a stable CSV report, then hashes the report. It intentionally excludes high-risk fields such as token identifiers from the normalized report; the raw source remains the place to revisit additional metadata.
from __future__ import annotations
import csv
import hashlib
import json
from pathlib import Path
from datetime import datetime, timezone
raw_path = Path("governance/evidence/audit-raw.json")
out_path = Path("governance/evidence/audit-normalized.csv")
manifest_path = Path("governance/evidence/SHA256SUMS.txt")
def sha256(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
events = json.loads(raw_path.read_text(encoding="utf-8"))
events.sort(key=lambda e: e.get("@timestamp", 0))
fields = ["timestamp_utc", "document_id", "action", "actor", "org", "repo", "target", "request_id"]
with out_path.open("w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=fields)
w.writeheader()
for e in events:
ms = int(e.get("@timestamp", 0))
stamp = datetime.fromtimestamp(ms / 1000, tz=timezone.utc).isoformat()
target = e.get("team") or e.get("ruleset_name") or e.get("security_configuration_name") or e.get("user") or ""
w.writerow({
"timestamp_utc": stamp,
"document_id": e.get("_document_id", ""),
"action": e.get("action", ""),
"actor": e.get("actor", ""),
"org": e.get("org", ""),
"repo": e.get("repo", ""),
"target": target,
"request_id": e.get("request_id", ""),
})
manifest_path.write_text(
f"{sha256(raw_path)} {raw_path.as_posix()}\n"
f"{sha256(out_path)} {out_path.as_posix()}\n",
encoding="utf-8",
)
print(f"normalized_events={len(events)}")
print(manifest_path.read_text(encoding="utf-8"), end="")
Expected output begins with
normalized_events=4 followed by two SHA-256 lines. The
manifest proves whether your retained local copies changed after
collection; it does not magically authenticate the origin of a
synthetic fixture.
7. Optional live organization audit-log inspection
If you control a disposable organization, organization owners can inspect the audit log in the current GitHub UI. Use a narrow time range and action qualifier. Do not perform privileged changes merely to manufacture an event. If you already made a harmless lab action, search for its documented event category.
Organization → Settings → Logs → Audit log
Example search qualifiers:
action:team.add_repository created:>=2026-08-19
repo:YOUR_ORG/service-api created:2026-08-19..2026-08-20
Enterprise Cloud optional REST path: the current organization audit-log REST endpoint requires organization-owner authorization. The query below is read-only, versioned, time-bounded, and paginated.
ORG="YOUR_DISPOSABLE_ORG"
gh api --method GET --paginate -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2026-03-10" -f phrase='created:>=2026-08-19 action:team' -f include='web' -f per_page=100 "orgs/$ORG/audit-log" --jq '.[] | {timestamp:."@timestamp",action,actor,org,repo,team,request_id}'
If this returns 403/404, distinguish product availability from authorization. A GitHub Free/Team organization can use the owner UI for ordinary organization audit events, but the documented REST organization audit-log endpoint is Enterprise Cloud. Do not “fix” that by creating a broader token.
8. Map control changes to expected evidence
| Change | Expected hosted state | Documented audit family when applicable | Independent evidence |
|---|---|---|---|
| Team gains repo access | Team permission appears on repository | team.add_repository |
Access API/UI + event + ticket/approval |
| Team permission changes | Effective role changes | team.update_repository_permission |
Before/after permission + event |
| Repository ruleset created | Ruleset object exists with enforcement/conditions | repository_ruleset.create |
Ruleset GET + event + reviewed change record |
| Security configuration created | Configuration object exists | security_configuration.create |
Configuration GET + event + coverage report |
| SECURITY.md committed | Repository file/ref changes | Do not assume a dedicated policy-file audit event | Git commit + content/blob SHA + PR/review evidence |
The final row is important: Git history itself is often the primary evidence for repository-file policy changes. Governance is not “audit log or nothing.” Choose evidence that matches the resource being controlled.
9. Challenge: choose the correct evidence surface
A reviewer asks: “Who approved the wording change in SECURITY.md,
and who changed the release team's repository role two days later?”
Do not answer both with one query. The SECURITY.md question starts
with Git commit/PR/review evidence. The team-role question starts
with effective-access state plus
team.update_repository_permission audit evidence if the
organization surface makes it available. Write the two collection
plans before running any commands.
10. Verification and cleanup
git status --short
git log --oneline -5
sha256sum governance/evidence/audit-raw.json governance/evidence/audit-normalized.csv
# PowerShell alternative:
# Get-FileHash governance/evidence/audit-raw.json -Algorithm SHA256
gh repo archive "$REPO" --yes
Archiving is reversible and keeps the evidence available for review. Repository deletion is intentionally not part of the mandatory lab. If you used a disposable organization, remove only the lab-specific grants/settings you added; do not delete an organization merely for cleanup.
Knowledge check
Why does the lab save raw JSON and normalized CSV separately?
Normalization is a derived view. Keeping the raw source preserves fields and structure that a later investigation may need, while the normalized report gives a stable schema for review.
A SECURITY.md commit exists, but the organization audit query has no “security policy changed” row. Is the policy change unevidenced?
No. Git commit/blob identity and PR/review history are the appropriate primary evidence for a repository-file change. Do not assume every file edit has a dedicated audit event.
Why is widening a token not the correct response to a Free organization failing the Enterprise Cloud audit-log REST endpoint?
The failure can be a product-availability boundary rather than insufficient scope. Authentication and authorization cannot enable a feature the plan/deployment does not expose.
Which event would you expect when a team is given repository access?
GitHub documents team.add_repository for that operation. The event should be correlated with current access state and the approval/change record.
What does hashing the evidence files prove?
It proves whether the retained byte sequences changed after the hash was recorded. It does not by itself prove who produced the data or that GitHub emitted it.
Summary
You now have a reproducible evidence pipeline: create observable repository policy state, preserve raw audit input, normalize it without deleting source context, hash both forms, and map each control to the evidence surface that can actually prove it. Lesson 3 turns those mechanics into policy choices about retention, enforcement scope, and cost.
Further reading — current primary sources
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.