Skip to main content
INS // Insights

Export Access Review Evidence for Audit

Updated September 2026 · 10 min read
Export Access Review Evidence for Audit

Auditors who ask to export access review evidence for audit do not want a zip of screenshots from last quarter's campaign UI. They want the decision on each grant, the ticket that revoked what was denied, and a file they can reload in the GRC they already run. Rutagon builds that join as a re-runnable export. The campaign is not the evidence. The join is.

Mid-market SaaS teams often close a recertification campaign and call it done. The GRC shows green. The identity provider still has the group. The SaaS tenant still has the admin seat. A screenshot of "completed" does not prove a revoke happened. We export the campaign row, the mutate result, and the ticket that carried the residue when the API failed.

This is scoped work. We do not replace your GRC. We fill the gap between a reviewer click and a system that actually changed. Automated access reviews still produce the campaign. This article is the packet that leaves it.

Export Access Review Evidence for Audit

Export access review evidence for audit starts with a campaign run ID you already have. We pull every review item: subject, system, entitlement, reviewer, decision, and decided time. We pull every revoke job and every ticket opened when write-back could not finish. We join on a stable grant key, not on display names. The export is a file with one row per grant in that run, plus the linked ticket IDs.

Re-runnable means the same job, same inputs, and same join keys produce the same packet or a diff you can explain. A human pasting cells into a tracker is not re-runnable. A scheduled job that reads the campaign store and the ticket project is.

Reviewers still decide in the campaign you already operate. We do not invent a second review UI unless the GRC cannot hold the custom fields the join needs. The engineering is the join, the mutate, and the export contract. Grant keys name the person or workload, the system, and the entitlement. "Billing Admin" is a label. emp-4412|okta|app:billing|role:admin is a key.

Campaign Decisions Joined to Revoke Tickets

A decision without a revoke is a wish. Approve writes nothing to the downstream system. Deny must produce a mutate or a ticket. We treat "deny plus no ticket plus still entitled" as an open finding, not a closed campaign item.

run_id: recert-saas-cycle
grant_key: emp-4412|okta|app:billing|role:admin
decision: deny
decided_at: campaign-timestamp
revoke:
  ticket: REV-8841
  mutate: failed
  retry: 3
  residue: open

When mutate succeeds, the ticket can still exist as the audit trail. When mutate fails, the ticket is the work. The export includes both states. A green campaign that dropped failed mutates is a lie we refuse to ship.

Ticket fields we require are owner, system, entitlement, opened time, closed time, and close reason. A chat thread is not a ticket. A comment on the campaign item is not a ticket unless your GRC is the system of record and it has those fields. We will not serialize a thumbs-up emoji as a revoke.

Custom apps without SCIM still join. If we can export effective access and we can open a ticket with a revoke instruction, the row exists. If we cannot mutate, the residue stays visible. That is the same honesty we use for evidence freshness tracking: stale or failed extracts are findings, not silent skips. Multi-system grants get one row per entitlement. A deny of "too much access" with no grant key cannot join.

A Re-runnable Export, Not Screenshots

Screenshots fail sampling for three reasons. They do not recompute. They do not join. They do not survive a UI redesign. An auditor who picks employee 4412 in the next cycle should get the same grant keys from the job, not a new intern hunting for the same modal.

The export we ship is a versioned schema. Columns are documented. A rerun with the same run ID overwrites nothing. It writes a new artifact with a content hash. If the hash matches, the packet is unchanged. If it does not, the diff is the story: a late ticket close, a retry that finally mutated, a reviewer who changed a decision. That diff is evidence too.

We keep the raw pulls. Campaign API dump. Ticket project export. Optional identity-provider group membership snapshot at run time. The join is derived. If someone challenges a row, we can show the two sources and the key. We do not hand-edit the joined file. Corrections go back to the source systems and the job runs again.

File format is whatever the GRC ingest already accepts: CSV, JSON, or the vendor's evidence object. We do not ask you to switch GRC products. We map our schema onto the custom test or evidence control you already named. Security automation is the build surface: connectors, join, mutate, export. Population coverage lives in the same file. A skip with no reason is a finding.

What Lands in the GRC You Already Run

Your GRC already has a control for user access reviews. What it usually lacks is proof that deny became revoke. We attach the export to that control as a recurring evidence source, not as a one-off upload from a laptop.

The GRC row should answer which campaign, which population, who decided, what was denied, which ticket closed the deny, and when the entitlement disappeared. If the GRC can store attachments only, we store the file and a pointer. If it can store structured custom fields, we write those fields. Either way the file remains the source of truth for a re-run.

Sampling then becomes mechanical. Pick a denied row. Open the ticket. Confirm the identity provider or SaaS no longer shows the grant, or confirm the residue is still open with an owner. We do not claim a certification. We claim a packet an auditor can re-open.

Exceptions are first-class rows. Keep-with-expiry is a decision with a dated reopen. Open-ended "business need" without an owner is not an exception we will serialize as a pass.

AWS Last-Accessed Data as an Input, Not the Export

Cloud IAM is often in the same campaign as SaaS. Last-accessed information from IAM Access Advisor can show that a permission set was unused, or that it was used after a deny. We pull that as a column on the grant row when the identity is an IAM user or role you already review.

Last accessed is not permission. Unused is not automatically denied. Used after deny is a finding. The export still centers on the campaign decision and the revoke ticket. Standing admin after a deny is the finding, same residue pattern as a SaaS admin seat.

What We Refuse to Call Closed

A campaign marked complete while deny rows have empty ticket IDs is not closed. A ticket marked done while the SaaS export still shows the role is not closed. A screenshot album with no grant keys is not closed. Connector failure is not a pass: unevaluable subjects are rows, not green cells. Shared "GRC admin" as the only actor makes sampling useless. Decision time, mutate time, and ticket close time belong on the row.

Frequently Asked Questions

Can we upload campaign CSV and skip the ticket join?

You can upload anything. It will not answer the revoke question. A campaign CSV without ticket IDs and mutate results is a list of opinions. We join because auditors sample denials, not vibes.

What if our GRC only accepts PDF screenshots?

Then we still produce the re-runnable file and attach a generated PDF that is a rendering of that file, not a camera photo of a UI. The file remains what we re-run. The PDF is a view. If the GRC later grows a structured ingest, we point it at the same job.

Do you replace our quarterly access review campaign?

No. We export it and we close the revoke loop. The campaign, the reviewers, and the GRC product stay yours. We build the join, the mutate, and the evidence contract those tools do not cover.

How do failed API revokes show up in the export?

As residue: decision deny, mutate failed, ticket open, last error, retry count. The row stays non-green until the entitlement is gone or a named owner accepts a dated exception. Hiding failed mutates is how green campaigns lie.

Ask us what your GRC platform isn't covering. Call 907-841-8407, email contact@rutagon.com, or use rutagon.com/contact.