To check an AI-generated attack path, pin the repository revision and turn the narrative into a chain of testable claims: who can enter, what they control, which code receives it, which controls run, and what harmful result follows. Open each cited line at that revision, then follow the calls both forward to the effect and backward to the caller. Look deliberately for a control that would defeat the claim. Record any missing deployment or runtime fact instead of letting a plausible story become a confirmed vulnerability. A citation proves what a line says; it does not prove the line runs in production or that an attacker succeeded.
The answer changes when the assessment includes authorized runtime evidence. A safely observed denial or disclosure can establish more than source inspection alone, but it must be labeled with its environment, accounts, date and exact action. This guide is for checking source-supported claims before handing them to a reviewer, not for attempting a live exploit.
A citation is an address, not a verdict
An AI report can cite a real call to get_invoice(invoice_id) and still be wrong about access control. The caller may be restricted by middleware, the storage query may bind the current account, the feature may be disabled, or the cited helper may be dead code. The opposite error is possible too: a model may see an ownership check in one route and assume a direct export route shares it. OWASP's API1:2023 guidance asks whether the caller is authorized for the specific object, not merely whether the caller can enter the API function. Its Authorization Cheat Sheet calls for permissions to be checked on every request.
Use a permalink or equivalent repository reference bound to a commit, not a moving branch URL. GitHub's permalink guidance explains how a commit ID and selected lines preserve the version being discussed. Store a file hash or immutable artifact ID as well when your source system does not offer stable line links. A line number without the revision is easy to misread after a refactor.
Audit three citations with three different defects
Synthetic source packet; all paths, revisions and lines below are invented and unexecuted. Harbor Ledger's report claims that an ordinary user can read a foreign invoice through the direct download route. It supplies:
A. api/download.py:44–47 at abc1234 — caller supplies invoice_id
B. stores/invoice.py:90–94 at main — query uses invoice_id alone
C. api/export.py:20–24 at abc1234 — PDF returned to caller
Citation A resolves at the pinned revision:
44 def download(request, context):
45 invoice_id = request.path["invoice_id"]
46 row = invoice_store.for_account(context.account).get(invoice_id)
47 return pdf_response(row.pdf_key)
It proves caller control of the ID and a call to an account-scoped receiver. It does not prove the receiver enforces its scope. Preserve A as valid evidence, including the receiver construction the model omitted.
Citation B uses a moving branch. At abc1234, the old line range now contains unrelated formatting code. A search at the pinned revision finds the actual store implementation at lines130–133:
130 def get(self, invoice_id):
131 return db.query_one(
132 "SELECT pdf_key FROM invoices WHERE id = ? AND account_id = ?",
133 [invoice_id, self.verified_account])
This is explanatory bound-query notation; confirm the real driver's semantics in an actual review. The repair is not “replace B's line number and keep the conclusion.” Record B as stale, add the newly inspected range as B2, and reconsider the claim. The new evidence contradicts the alleged unscoped read if verified_account has trusted provenance.
Citation C exists, but belongs to a different export entry point. It cannot bridge the download handler to a response unless the call graph connects them. In this packet, it does not. The direct response is already at A47. C may start a separate export investigation; it is not evidence for the claimed download edge.
| Citation | Address valid? | Supports the claimed edge? | Action |
|---|---|---|---|
| A | Yes, pinned | ID input and actual scoped call | Retain; read constructor provenance |
| B | No, moving/stale range | No | Preserve failed citation; create B2 through new inspection |
| B2 | Yes, pinned | Account predicate at actual read | Record as counterevidence |
| C | Yes, pinned | Wrong entry point | Remove from this chain; preserve separate lead |
Repair provenance without laundering the original claim
The revised chain is download route → trusted account context → scoped store → PDF response. Its missing fact is now the origin of context.account, not whether the store has a predicate. Open route registration and the store constructor. If they use current server-derived account authority, the exact cross-account source claim is rejected. If it accepts a caller-selected account without checking the caller’s authority for that account, the predicate is real but insufficient, and the claim changes to tenant-context spoofing. A caller-selected account is legitimate when the server first authorizes that selection; its origin alone does not decide the issue.
A completed citation-repair record reads:
Original B could not be reproduced at
abc1234; no conclusion relied on it. B2 was retrieved at that revision and shows the account predicate. C is a valid export citation but disconnected from the direct-download path. A plus B2 and the verified context constructor defeat the direct-download claim in the synthetic source packet. Export and production mapping remain separate work.
This record allows another reviewer to see what changed. Silently editing the original report would make a hallucinated or stale citation look as though it had always supported the final conclusion.
Know where line verification ends
A permalink binds text to a revision; GitHub documents commit-bound file links. A content hash can additionally identify the exact acquired file. Neither proves a call executes, a query adapter binds values, or a production deployment uses the revision. Those are separate edges with separate evidence.
When source has moved, retrieve the original snapshot if available. If it is unavailable, mark the original claim unverified and start a new review at the new revision. Do not pretend a similar function today proves what existed yesterday. If retrieval is truncated, cite only the received range and name the unread control boundary.
The false-positive evidence guide explains the broader distinction between a lead and a supported finding. This article's practical artifact is the citation audit: valid text, stale address, disconnected edge and recorded repair. None of the code above represents a live exploit or a completed independent review.
Build an edge ledger when the narrative crosses files
Harbor Ledger's corrected citations become easier to audit if each transition has its own row. This is especially useful when a report mixes source snippets from a handler, helper, middleware package, and response adapter. The question is not simply whether all four files exist. It is whether the cited evidence connects them in the claimed order for the same request and object.
| Edge in the synthetic claim | What would establish it | What would leave it open |
|---|---|---|
External request reaches download |
Route registration and applicable middleware | Handler exists but registration is missing |
| Requested invoice ID reaches the store | Handler argument and receiver construction | Model summary substitutes another identifier |
| Store uses authorized account context | Constructor and current membership decision | Account origin is unread or only asserted |
| Selected row supplies the PDF key | Query projection and response argument | A different row or fallback supplies the key |
| Response exposes that PDF to this caller | Delivery implementation and return path | Adapter behavior is unavailable |
A valid row can rely on several source ranges. Do not compress it to one decorative citation if the important control is in another file. Conversely, do not attach an entire directory as evidence when a few precise ranges establish the transition. The ledger should let a reviewer open the decisive material without repeating the entire search.
Follow the identity of values as carefully as function names. A local variable called invoice_id might be reassigned; a store result might be replaced by a cache entry; a response helper might receive a key derived from the original request instead of the authorized row. Those are questions to inspect, not claims that the fictional application has those defects. The exact chain decides which checks protect the actual effect.
Handle context that does not fit in the cited range
A four-line snippet can accurately show an account predicate while omitting a condition around it. Read enough surrounding code to establish the branch, early returns, and error handling relevant to the allegation. If the query runs only after a successful guard, record that dependency. If a fallback performs a different lookup after the query returns no row, include the fallback before deciding the account boundary is settled.
For the synthetic packet, imagine a hypothetical fallback that converts “not found” into a legacy export lookup. That would require a new edge in the ledger. The scoped query remains real counterevidence for its own branch, but it no longer settles every outcome of the handler. The reviewer should state precisely which branch is guarded and which remains to be inspected, instead of dismissing or confirming the whole report from the first query found.
Generated registration introduces another limit. A source file may declare a route without showing how the build mounts it. Record the available declaration and the missing generated map separately. If the map can be acquired as data, inspect its revision relationship. If it cannot, the source observation can still justify a local correction, but the reviewer should not claim to have proved production reachability.
Make a repaired citation useful after the next refactor
Keep the old and new evidence identifiers in the review record: B failed to resolve; B2 was newly inspected; B2 changed the conclusion. A later reviewer then knows that B2 was not merely a relocated quote from B. This preserves the intellectual history without requiring every developer ticket to carry the full failed report.
When a patch moves the function, establish a new evidence set at the patch revision. The old line number is still useful for explaining the original claim, but it is not the acceptance evidence for the fix. Compare the relevant actor, value, control, and effect across revisions. A renamed helper can preserve an unsafe path; an unchanged helper can become protected by a changed mandatory caller.
The final handoff should therefore identify both the source location and the reason it matters. “invoice.py lines130–133 bind the selected invoice to the server-authorized account” is more useful than “see attached source.” If constructor provenance is still missing, say so in the same sentence. That makes the citation an inspectable part of the argument instead of a visual signal of credibility.
For the next AI claim, audit each citation for revision, range and claim support before reading its severity label. If the claim is still only a lead, use the finding triage gate. If your team uses separate model passes, continue with why separate calls do not guarantee separate judgment.