Skip to content

fix(launchpad): scope tracked-sensitive-files to what the cohort owns - #1984

Open
tucktuck101 wants to merge 1 commit into
launchpadfrom
audit/1965-cohort-owned-scope
Open

fix(launchpad): scope tracked-sensitive-files to what the cohort owns#1984
tucktuck101 wants to merge 1 commit into
launchpadfrom
audit/1965-cohort-owned-scope

Conversation

@tucktuck101

Copy link
Copy Markdown
Collaborator

Summary

audit failed on launchpad trunk, and so on every branch cut after 2026-08-28, over six files nobody in this cohort wrote. The check is not wrong about them — four genuinely contain private key material — but they are byte-for-byte upstream's APNs test fixtures, and this fork cannot fix them. This narrows the check by ownership rather than adding an exemption, because the check deliberately has no exemption mechanism and an earlier one caused a real miss.

Feature

N/A - single-issue PR

Related issue

Closes #1965

Issue type

Bug


Agent provenance

Field Value
Harness / provider omp
Model claude-opus-5
Session reference N/A
Initiating human @tucktuck101

Objective

A tracked-sensitive-files check that passes on launchpad trunk while still failing on any sensitive-shaped file this cohort added or modified.

Impacted components

launchpad/scripts/security_audit_classifier.py              additive: fetch_upstream_blobs, local_blob, divergence
launchpad/scripts/security_audit_tracked_files_check.py     partitions hits by ownership
launchpad/scripts/test_security_audit_tracked_files_check.py  4 new controls, existing ones taken off the network

Approach and rejected alternatives

The six files, classified by content — headers only, no key material printed:

apns-test-cert-only.pem              CERTIFICATE
apns-test-encrypted-identity.pem     ENCRYPTED PRIVATE KEY, CERTIFICATE
apns-test-identity.pem               PRIVATE KEY, CERTIFICATE
apns-test-key-only.pem               PRIVATE KEY
apns-test-mismatched-identity.pem    PRIVATE KEY, CERTIFICATE
apple-app-attestation-root.pem       CERTIFICATE

They arrived in c432a111c ("feat(mobile): push notifications MVP (block#6269)"), an ancestor of block/buzz@main, and are byte-for-byte identical here (git rev-parse on fork and upstream returns the same OIDs). block/buzz is public, so they have always been public — consistent with deliberately generated APNs test fixtures.

Rejected: an allowlist or path exemption. This was my first instinct and it is wrong. _matches_sensitive_shape has no exemption mechanism on purpose: a *.example suffix exemption was removed after it let a tracked seed/authorized_keys.example through, which is exactly the shape #68 documents a real past incident for. Re-adding the mechanism for these six would undo a control that was hardened deliberately.

Rejected: match on content instead of filename. Would not help — four of the six really do contain PRIVATE KEY blocks, so a content rule still fires.

Rejected: untrack them, or generate them at test time. Both edit upstream-owned test code, which this fork does not do.

Rejected: raise it upstream only. Arguably correct owner, and worth doing separately, but it leaves trunk red indefinitely.

Chosen: scope by ownership. A fork that operates the upstream product is accountable for what it wrote, not for what it inherited unchanged. divergence() returns fork-added, inherited-modified, inherited-identical or indeterminate, and only inherited-identical licenses a skip.

Identity, not origin — this is the part that matters. classify() already answered "did this path exist upstream", and that is not sufficient: an inherited filename is precisely where cohort-added key material could hide. divergence() compares blob OIDs, so a path present upstream but modified here still fails. The OID comes from the same single git fetch the classifier already ran — git ls-tree without --name-only is one column wider and costs no extra network.

fetch_upstream_paths and classify are untouched, so the change is additive. Incidentally this is the classifier's first caller: it was written for #62 and never wired to anything.

Unreachable upstream reports INDETERMINATE, naming the paths. Neither PASS nor FAIL is honest when ownership cannot be established, and INDETERMINATE is visibly not-green so it cannot be read as clean. Upstream-identical hits are named in the PASS detail — a skip nobody can see is indistinguishable from a check that stopped looking.

Verification

Command run:

python3 -m unittest discover -s launchpad/scripts -p "test_security_audit*.py"
# the changed check, against this repository
python3 -c "from security_audit_tracked_files_check import run; print(run(Path('.')))"
# negative control: a cohort-owned key, force-added because *.pem is gitignored,
# committed, checked, then reset away

Raw output:

--- all security-audit controls
........................................................................................
Ran 88 tests in 3.634s
OK

--- the changed check against this repository
status : pass
detail : no cohort-owned tracked file matches a sensitive shape (5718 tracked files checked);
         6 sensitive-shaped file(s) are byte-for-byte upstream's and not cohort-owned:
         crates/buzz-push-gateway/tests/fixtures/apns-test-cert-only.pem;
         crates/buzz-push-gateway/tests/fixtures/apns-test-encrypted-identity.pem;
         crates/buzz-push-gateway/tests/fixtures/apns-test-identity.pem;
         crates/buzz-push-gateway/tests/fixtures/apns-test-key-only.pem;
         crates/buzz-push-gateway/tests/fixtures/apns-test-mismatched-identity.pem;
         crates/buzz-push-gateway/tests/fixtures/apple-app-attestation-root.pem

--- negative control: cohort-owned key committed to THIS repo, then reverted
status : fail
detail : 1 cohort-owned tracked file(s) match a sensitive shape: launchpad/deploy/negative-control.pem

The negative control is the acceptance criterion demonstrated rather than asserted: with the six upstream fixtures present and correctly excluded, one cohort-added key still fails the run.

  • Tests or checks were run and the raw output is pasted above
  • The diff is confined to the scope of the linked issue
  • No secrets, keys, tokens or hostnames were added to tracked files

Not verified

  • The full security_audit.py harness was not run to completion locally. It downloads and checksum-verifies a pinned gitleaks binary and exceeded a 600s timeout on this machine. Only the check I changed was run against the repository, plus all 88 unit controls. CI on this PR is the first end-to-end run.
  • The CI path through fetch_upstream_blobs is unproven. The workflow already uses fetch-depth: 0 because the classifier fetches block/buzz, but no job has ever exercised that fetch — this is the classifier's first caller, so its behaviour on a runner is untested.
  • Whether those four private keys are genuinely throwaway fixtures is upstream's claim, not a verified fact. They are named apns-test-*, live in a test fixtures directory, and are public in upstream — but nothing here proves they are unused elsewhere.
  • Rename/move is not tracked. A file moved upstream to a new path reads as fork-added at its old path and would fail. No such case exists today.
  • git ls-tree parsing assumes the standard <mode> <type> <oid>\t<path> shape. Malformed entries are skipped rather than raising, which is a silent-ish failure mode.

Authority

N/A - no approval or merge performed by an agent; this PR is submitted for human review.

Deferred blockers

none

Security implications

This narrows a security control, so it deserves the scrutiny. What it gives up: a sensitive-shaped file that is byte-for-byte identical to upstream's no longer fails. What it keeps: everything the cohort added (fork-added), everything the cohort changed (inherited-modified), and everything whose ownership cannot be established (indeterminate, which is not-green). The negative control above proves a cohort-added key still fails with the upstream fixtures present.

The alternative was an exemption list, which would have been a permanent hole matched by name rather than by content identity — strictly weaker than this, and against the documented intent of the function it would have modified.

One genuine new dependency: correctness now rests on git fetch of a public upstream reaching the network. Failure is handled as INDETERMINATE rather than PASS, so a network outage cannot silently turn the check green.

Escalations

  • This PR replaces fix(launchpad): scope tracked-sensitive-files to what the cohort owns #1983. That branch's first commit used a real -----BEGIN PRIVATE KEY----- header as fixture content, which gitleaks-secret-scan correctly flagged. The PR-scoped scan reads the commits a branch adds, not its tip, so removing the header in a follow-up commit did not clear it — and force-pushing is barred. The work is carried here as one clean commit. fix(launchpad): scope tracked-sensitive-files to what the cohort owns #1983 is closed as superseded, not abandoned.

  • Upstream arguably should not ship private-key-shaped test fixtures, even generated ones, and this PR does not tell them so. Worth a separate upstream report; I have not filed one because that is a decision about how this cohort talks to block/buzz, not a code fix.

  • I recommended the allowlist approach first and it was wrong. The check's own comments document why exemptions were removed, and I proposed re-adding the mechanism before reading them. Recording it because the reasoning matters more than the outcome: a security control's history is part of its specification.

  • Four of the six files really do contain private keys. If anyone believes one of them could be a real credential rather than a fixture, this stops being a CI problem and becomes an upstream security report, and this PR should be held.

`audit` failed on `launchpad` trunk, and therefore on every branch cut after
2026-08-28, over six files nobody here wrote.

The check is not wrong about them. Four of the six genuinely contain private key
material:

    apns-test-cert-only.pem              CERTIFICATE
    apns-test-encrypted-identity.pem     ENCRYPTED PRIVATE KEY, CERTIFICATE
    apns-test-identity.pem               PRIVATE KEY, CERTIFICATE
    apns-test-key-only.pem               PRIVATE KEY
    apns-test-mismatched-identity.pem    PRIVATE KEY, CERTIFICATE
    apple-app-attestation-root.pem       CERTIFICATE

They arrived in `c432a111c` ("feat(mobile): push notifications MVP (block#6269)"),
which is an ancestor of `block/buzz@main`, and they are byte-for-byte identical
here. They are APNs test fixtures in upstream's public repository. This fork
operates the upstream product rather than developing it: it cannot fix them, and
it does not move or rename upstream paths.

So this narrows by OWNERSHIP, not by exemption. An allowlist would be a standing
hole, and this check deliberately has none — a suffix exemption was removed after
one let a tracked `seed/authorized_keys.example` through, which is the shape #68
documents a real past incident for. Adding the mechanism back for these six would
undo that.

IDENTITY, NOT ORIGIN. `security_audit_classifier.classify` already answered "did
this path exist upstream", and that is not enough: an inherited filename is
exactly where cohort-added key material could hide. `divergence()` compares blob
OIDs and returns one of `fork-added`, `inherited-modified`, `inherited-identical`
or `indeterminate`. Only `inherited-identical` licenses a skip; the other three
are cohort-accountable or unknown.

The OID comes from the same single `git fetch` the classifier already performed —
`git ls-tree` without `--name-only` is one column wider and costs no extra
network. `fetch_upstream_paths` and `classify` are untouched, so this is additive.
It is also the classifier's first caller: it was built for #62 and never wired.

Unreachable upstream reports INDETERMINATE, naming the paths — neither PASS nor
FAIL is honest when ownership cannot be established, and INDETERMINATE is visibly
not-green so it cannot be mistaken for clean. Upstream-identical hits are named
in the PASS detail: a skip nobody can see is indistinguishable from a check that
stopped looking.

The pre-existing controls now patch the upstream fetch rather than reaching the
network to decide something their own fixtures already know. Four controls added:
upstream-identical does not fail and is named; a cohort-added key still fails
alongside upstream fixtures; an inherited path modified here still fails;
unreachable upstream is INDETERMINATE, not PASS.

Verified against this repository:

  status : pass
  detail : no cohort-owned tracked file matches a sensitive shape (5718 tracked
           files checked); 6 sensitive-shaped file(s) are byte-for-byte
           upstream's and not cohort-owned: crates/buzz-push-gateway/tests/
           fixtures/apns-test-cert-only.pem; ... apple-app-attestation-root.pem

Tests: 88 security-audit tests pass (14 in this check's own suite, 4 new).

Closes #1965

Fixture content is deliberately not key-shaped: the check matches on path shape
and never reads content, and an earlier revision that used a real PEM header
made `gitleaks-secret-scan` fail on two of its own lines. A control for a
secret-detection suite must not itself be the thing it detects. That revision
lived on `audit/1965-upstream-owned-fixtures`; because the PR-scoped gitleaks
scan reads the commits a branch adds rather than its tip, and force-pushing is
barred, this branch carries the work as one clean commit instead.

Signed-off-by: tucktuck101 <jeffreytaylorrobertson@gmail.com>
@tucktuck101 tucktuck101 added bug Something isn't working area:ci GitHub Actions, checks, automation by:agent Filed or authored by an AI agent, not a human labels Aug 31, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed commit 3a7e7b6c5958c7a45a8ef5c6b14f6fcf01cdfd73 against merge base 1ed55e980b0043f92d9c652e6a39a8e49345389c.

Incomplete

This review is INCOMPLETE and must not be read as a full pass:

  • no dimension was actually reviewed: the pipeline ran the 'default_reviewer' stub reviewer, which reports every dimension clean without reading it (a real dimension reviewer is #116)

Containment

No containment findings.

Fetched and empty: pr_issue_comments, pr_review_bodies, pr_review_comments.

Automated containment covers the delimiter boundary and unambiguous injection tells only. It does not cover injection phrased as ordinary, unremarkable prose. The absence of a containment finding is not evidence that this pull request contains no injection attempt.

@tucktuck101

Copy link
Copy Markdown
Collaborator Author

Authority — recorded, and partially declined

Instruction given this session by @tucktuck101, quoted verbatim, transcription noise intact:

because no one else is around to do pr reviews right now you have my permission to use adminbypass on the blocking prs. record this decision in the pr as well as acknowledgement that i take responsibility for any fixes that this requires as a result.

Acknowledgement, as requested: @tucktuck101 states he takes responsibility for any fixes required as a result of merging these pull requests without a second reviewer.

What I did not do, and why. I have not used gh pr merge --admin or any other bypass of branch protection. launchpad/AGENTS.md §5 makes that non-negotiable regardless of who grants it:

No gh pr merge --admin, and no other bypass of branch protection. Your token holds admin and enforce_admins is off, so you can. That is not permission.
[…]
A blocked merge is an answer. Fix the change or escalate to a human. Reaching for a stronger credential is the failure mode this whole record exists to prevent — on 2026-08-28, 132 pull requests were merged with --admin past 77 changes-requested reviews and unresolved CI, and that is the event ADR-0052 cites as its own motivation.

ADR-0052 part G says the same: "Delegated authority never bypasses CI or any rule GitHub enforces." The delegated-authority framework exists precisely so a human's instruction lets an agent act inside the gates; it explicitly does not extend to acting around them, and it anticipates the capability being available.

The actual gate, for the record. required_approving_review_count: 1, required_status_checks: null, enforce_admins: false. So the only platform requirement is one approving review, and GitHub does not permit the author of a pull request to approve it — these were opened under @tucktuck101's identity, which is also the identity a delegated approval would use. That is why delegated authority cannot clear this particular block: it is a same-identity problem, not a permission problem.

What is available instead, in preference order:

  1. Any second cohort reviewer approves. One click, gates satisfied legitimately.
  2. @tucktuck101 merges directly. He holds admin; using it himself is his call to make and is not an agent routing around a control. That is the honest version of the instruction above.
  3. --auto merge, which §5 explicitly prefers — the platform merges the moment the gate goes green, with no bypass.

Nothing here is a judgement about the change. It is only about who may open the gate and how.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:ci GitHub Actions, checks, automation bug Something isn't working by:agent Filed or authored by an AI agent, not a human

Projects

None yet

Development

Successfully merging this pull request may close these issues.

audit check fails on launchpad trunk: six tracked .pem fixtures from c432a111c break every branch cut after 2026-08-28

1 participant