trust & data

Security & data handling

TriageRook is built for developers who read the permission screen before they click Authorize. This page is the honest, code-backed version of what that screen means: what we can touch, what we keep, and what we throw away.

We authenticate with a GitHub App, not OAuth scopes

Sign-in is backed by the TriageRook Security GitHub App. A GitHub App does not request the classic OAuth scopes you may have seen elsewhere (no public_repo, no repo). Instead, the App declares a fixed set of fine-grained permissions, and those are the only things it can ever do:

PermissionAccessWhy
Repository contentsRead & writeRead is how every scan fetches your file tree and file contents. Write exists for one opt-in action only: opening a fix pull request when you click “Fix” on a finding. Nothing is written unless you ask for that PR.
Pull requestsWriteLets the same opt-in “Fix” flow open a PR with the suggested change. Never used during a scan.
Email addressReadIdentifies your account at sign-in. Your stable GitHub numeric user id (not your username) is the key we store scans under.
MetadataReadMandatory baseline for any GitHub App: repo names, default branch, visibility. Visibility is what lets us refuse private repos at the door.

About contents: write

Contents writeis the one permission a skeptical reader should question, so we will be explicit: it is used by exactly one feature — the Fix PR button, which opens a pull request with a suggested remediation when you click it. A scan never writes to your repository. If you never use Fix, that permission is never exercised.

read:org is optional — and we never fake the answer

One repo-posture signal checks whether an organization enforces two-factor authentication. Reading that requires organization-level access (read:org). It is not part of the default install, and the IAM risk scanner does not use it at all.

When we cannot read it, the mfa-org signal is marked unknownrather than “failing.” An unknown signal is excluded from both the numerator and the denominator of your posture score — its points are withheld, never silently awarded and never counted against you. You are told the signal could not be evaluated instead of being shown a confident number we did not earn.

Public repositories only

TriageRook only scans public repositories. This is enforced in code, not just promised in copy: the authenticated scan endpoint resolves the repository's visibility and refuses with a 403before any file tree or blob is fetched if the repo is private. The check runs at the API boundary, so a private repo's contents are never requested in the first place.

What each scan endpoint stores

There are two ways to scan, and they persist very different amounts of data.

Anonymous scan — stores nothing

Paste a public repo URL with no account (/scan-public/<owner>/<repo>). The result is computed and returned to your browser, and that is the end of it. We do not write the scan, the findings, or your code to any database.

Two things are recorded, neither of them your data: per-hour rate-limit counters (keyed by source IP and by repo, to stop abuse), and a single structured log line per attempt containing the owner, repo, duration, and outcome. That log line deliberately contains no IP address and no other personal data.

Signed-in scan — saved to your history

When you sign in and scan, the full result is saved to your scan history (a Supabase scans table) so you can revisit it, diff it against a later scan, and export SARIF. The stored row is keyed to your GitHub numeric user id and holds the repo owner/name, the result document, the prioritized findings, and the risk / posture / IAM / supply-chain summaries.

Every secret inside that stored result is already masked (see below). The raw secret value is never part of what gets written.

Secrets are masked before anything is stored

When a secret pattern matches, the matched value is masked at the moment of detection — before it is attached to a finding, before the finding is returned, and before any persistence. Short values (8 characters or fewer) become all bullets; longer values keep the first and last four characters with the middle replaced by bullets, and the line is truncated to 200 characters. The masked line is the only form that leaves memory.

Example

AKIAIOSFODNN7EXAMPLE → AKIA•••••••••••MPLE

Optional secret livenessvalidation (does this key still work?) is off by default. It only runs on signed-in scans and only when a deployment-level flag is enabled; anonymous scans never trigger it. When it does run, the raw value is held in memory for the duration of that scan to make the check and is then dropped — it is never persisted and never included in the response body.

Your code is read, analyzed, and discarded

Source code is fetched per scan through the GitHub API, analyzed entirely in memory, and never written to storage. There is no clone left on a disk, no copy of your files in a database. When the scan function returns, the file contents are gone with it.

No execution, no agent

TriageRook never executes your code. Every detector is static analysis — pattern matching, entropy, AST walks, manifest parsing — run against file text. There is no sandbox running your build, no long-lived agent sitting inside your repo, and no background process after the scan finishes.

Your GitHub token never reaches the browser

The GitHub access token lives only on the encrypted session cookie and is read server-side. It is deliberately kept off the session object that /api/auth/session exposes to any same-origin script, so even a successful XSS cannot read it from the session JSON. Server code reads it from the encrypted cookie when it needs to call GitHub; it is never placed in a response body sent to the client.

Think a claim here overstates what the code does? That is a bug we want to fix. Open an issue.