fix: paginate credential_audit search to stop silently dropping results past page 1 - #3812
Conversation
…ch page _search() only ever fetched one page from GitHub's Search API, so a subject with more issues (find_spray_citations, per_page=100) or merged PRs (find_merges, per_page=50) than fit on the first page had every result past that page silently dropped. GitHub's Search API returns up to 1000 results per query across multiple pages; a single unpaginated request only sees the first one. This is exactly backwards for what this tool is for: a contributor prolific enough to have filed more than 100 issues total is also the contributor most likely to have real spray citations sitting on page 2+, so the audit could under-report risk (or report NONE/LOW) precisely for the highest-volume accounts it exists to catch. Fixes it in both copies of the tool (the packaged agent_compliance.cli.credential_audit module and the standalone scripts/credential_audit.py), since they implement the same _search() helper independently. Pages through up to GitHub's 1000-result window, stopping early on a short (last) page. Added pagination tests to both test suites: verifies multi-page aggregation, that a short page stops iteration, that the 1000-result window caps iteration rather than looping unboundedly, and that an empty first page returns no items. Confirmed no regressions: existing tests in both suites still pass (13/13 in scripts/tests, 7/7 in agent-governance-python/agent-compliance/tests, run locally with pytest 9.0.3 / Python 3.12.3). Note: agent_compliance/cli/contributor_check.py's _search_issues() has the identical single-page pattern at several call sites. Left untouched to keep this PR scoped to credential_audit.py - flagging it here in case it's worth a follow-up. Signed-off-by: Karth <karthik.chundi@gmail.com>
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
|
Welcome to the Agent Governance Toolkit! Thanks for your first pull request. |
PR Review Summary
Verdict: AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims. |
|
🟡 Contributor Check: MEDIUM
Automated check by AGT Contributor Check. |
|
@microsoft-github-policy-service agree |
Imran Siddique (imran-siddique)
left a comment
There was a problem hiding this comment.
The bug is real and the fix is right. The loop terminates correctly on all three exit conditions, and
the tests cover each one, which is more than most pagination fixes get.
Worth being explicit about why the old code was silently wrong rather than merely incomplete: it
requested per_page=100 and returned data["items"] with no page parameter and no check on
whether the page was full. A subject with 100 or fewer results looked identical to a subject with 900,
and the tool reported a clean audit in both cases. For a credential-spray auditor that is the worst
possible failure direction, and your comment makes the right point that the lost pages are exactly
the later, farther-out citations a real spray produces.
Checked the arithmetic against GitHub's actual constraint. The Search API rejects requests where
page * per_page exceeds 1000, so the cap has to be on the product and not just on the count.
max_pages = 1000 // 100 = 10 gives page 10 covering results 901 to 1000 and stops before the 422.
max(1, ...) handles a caller passing per_page above 1000. Both correct.
The short-page break is the right primary terminator, and keeping the empty-page break as well is
correct rather than redundant: _api returns None on failure, and without that branch a transient
error mid-pagination would keep looping to page 10 issuing failing requests.
Sequencing, and this one matters. #3876 is open and refactors scripts/credential_audit.py down
to a 27-line compatibility shim that does sys.modules[__name__] = _implementation, deleting the
implementation from that file entirely. This PR edits both copies. So:
- If this merges first, #3876 rebases and drops your
scripts/hunk, which is a clean outcome since
your packaged-module fix is the one that survives. - If #3876 merges first, your
scripts/credential_audit.pyhunk no longer applies and needs removing.
Landing this one first is the lower-friction order, given it is 160 lines against 3,200.
Your new tests in scripts/tests/test_credential_audit.py should keep passing either way, since under
the shim import credential_audit resolves to the packaged module and patch.object still finds
_api. They would just be exercising the packaged implementation twice, which is harmless and worth a
tidy-up in whichever PR lands second.
One thing to fix: Validate PR title currently shows red on this PR, but there is a later SUCCESS
for the same check in the rollup, so that entry is stale rather than a real failure. Worth knowing
before anyone treats it as a blocker.
Nothing blocking.
Carlos Hernandez (carloshvp)
left a comment
There was a problem hiding this comment.
Reviewed head ab8f8571 on a clean merge with current main at 359a2332. The scripts suite passed 13/13, and the packaged credential-audit plus contributor-check tests passed 7/7. I then applied the new tests to current main without the implementation; each suite failed three pagination cases. The tests distinguish the fix from the old single-page behavior. DCO, CLA, and the live check rollup are green.
Both production callers use per_page=50 or 100, so the loop covers GitHub's full 1000-result window and closes the reported undercount. One non-blocking edge remains in the helper contract: floor division stops a future per_page=30 caller after 33 pages and 990 items, while GitHub accepts page 34. Ceiling division plus a non-divisor test would cover that case. No current credential-audit path uses such a value.
This PR overlaps my #3876 in scripts/credential_audit.py. Landing #3812 first gives the lower-conflict sequence: #3876 can then rebase, retain the packaged implementation from this PR, and drop its deleted standalone implementation hunk. I found no blocker in this change and approve it.
Summary
_search()in both copies of the credential-audit tool only ever fetches one page from GitHub's Search API.find_spray_citations()calls it withper_page=100andfind_merges()withper_page=50, but neither passes/increments apageparameter, so any subject with more issues (or more merged PRs into the target repo) than fit on that first page has every result past it silently dropped - no error, no warning, just an incomplete result set.GitHub's Search API returns up to 1000 results per query, paginated (see https://docs.github.com/en/rest/search#about-search). A single unpaginated request only ever sees the first page of that window.
Why this matters for what the tool is actually for
This is backwards for a credential-spray detector specifically: a contributor prolific enough to have filed more than 100 issues total is also the contributor most likely to have real spray citations sitting on page 2+. As written, the audit can silently report
NONE/LOWrisk precisely for the highest-volume accounts it exists to catch, while a low-volume account gets fully covered by chance. Same shape of gap forfind_merges()and prolific target-repo contributors, though the practical ceiling there (50+ merged PRs into one repo by one person) is a higher bar.Fix
Paginate
_search()through GitHub's full 1000-result window, stopping early when a page comes back shorter thanper_page(the natural last-page signal). Applied to both copies since they implement this helper independently:agent-governance-python/agent-compliance/src/agent_compliance/cli/credential_audit.pyscripts/credential_audit.pyTesting
Added
TestSearchPaginationto both test suites (mocking_api), covering:Nonefirst-page response returning no itemsRan both suites locally (pytest 9.0.3, Python 3.12.3):
scripts/tests/test_credential_audit.py: 13/13 passed (4 new + 9 existing, no regressions)agent-governance-python/agent-compliance/tests/test_credential_audit.py(new file) +test_contributor_check.py: 7/7 passedI don't have a real GitHub token with search-API access exercised against live data in this environment, so verification is at the
_search()/pagination-logic level via mocks, not an end-to-end run ofaudit_credentials()against a live account - flagging that honestly.Scope note
agent_compliance/cli/contributor_check.py's_search_issues()has the identical single-page pattern at several call sites (per_page=50/100, nopageloop) - same underlying gap, different tool. Left untouched here to keep this PR scoped tocredential_audit.py; happy to send a follow-up if useful.AI-assisted contribution disclosure
Per
CONTRIBUTING.md: this is an AI-assisted contribution (Claude, directed by me) - I reviewed the diff, understand the fix and can explain/defend it, and it is not an autonomous/unreviewed submission, so no additional disclosure is required, but noting it for transparency.