Add Heroku API key validity check
What does this MR do and why?
This adds a validity check for Heroku API keys (starting with prefix HRKU-).
A user can then tell whether the exposed credential is still live rather than only that it was leaked.
Implementation
This follows ADR-006 and uses the registry directly to map the token straight to the verifier class/client, i.e. HerokuClient.
The verifier calls GET https://api.heroku.com/account with the token as a Bearer credential and Accept: application/vnd.heroku+json; version=3. Heroku's Platform API reference requires that header on every request:
Clients must address requests to
api.heroku.comusing HTTPS and specify theAccept: application/vnd.heroku+json; version=3Accept header.
We map the responses to the following statuses/outcomes:
| Response | Outcome |
|---|---|
200 |
active |
401 |
inactive |
429 |
RateLimitError |
500, 503 |
NetworkError |
anything else, including 403 |
unknown |
Please note that aside from 401, no other status falls to inactive. 403 — a valid key without access to the account resource — is therefore never read as revoked.
Reporting a live token as dead is worse than reporting nothing, so unmapped statuses are always unknown.
Generated, not hand-written
The client and its spec are the output of the Challenge #6 (closed) AI-assisted workflow rather than hand-written code. An LLM reads the vendor's API documentation and emits a vendor spec — plain data: endpoint, auth style, token pattern, status map — a human reviews that data, and a deterministic generator turns it into the verifier and its spec. Keeping the model's output as data instead of code is what makes the review tractable.
The generator itself is not on master yet (it is part of the Challenge #6 (closed) PoC), so only its
output lands here.
Two corrections were made to the reviewed vendor spec before generating, both found by checking it against the authoritative rule and the vendor's API docs:
- Token pattern. It was a bare UUID, but the
Heroku API Keyrule matchesHRKU-followed by a UUID, and that rule lists a bare UUID as an explicitnegativeExample. A bare-UUID format check would have rejected every token we actually detect, so the check could never have fired. The likely origin of the mistake is Heroku's own documentation, whose example shows a token as a bare UUID (Authorization: Bearer 01234567-89ab-cdef-0123-456789abcdef) —HRKU-is the newer format. - Accept header. Without the versioned
Acceptheader, Heroku answers406 Not Acceptable(not_acceptable: "request failed, setAccept: application/vnd.heroku+json; version=3header and try again").406is unmapped, so every token — live or dead — would have come backunknown. The vendor spec now carries the header explicitly.
Stack Position
We have 8 MRs for all new validity checks generated during Sec Challenge #6, with the base targeting master.
Each MR in the stack adds exactly one validity check: the client class, its spec, the registry entry, and the rate limit rule.
Stack (in review order)
| # | Check | Token type |
|---|---|---|
| 1 | GitHub PAT | Github Personal Access Token |
| 2 | OpenAI project key | OpenAiProjectKey |
| 3 | Anthropic API key | anthropic_key |
| 4 | Slack access tokens | Slack token |
| 5 | Stripe live secret key | StripeLiveSecretKey |
| 6 | Datadog API key | DataDogAPIKey |
| 7 | SendGrid API token | Sendgrid API token |
| 8 | Heroku API key | Heroku API Key |
Notes
- Token type and pattern were verified against the authoritative rule in
secret-detection-rules. - Rate limit: 75 checks per minute per project, matching the ones Heroku adds back to an account's pool.
- Unmapped statuses are covered by the shared example
a partner token client, which this stack adds once and every verifier reuses. - Verifier code originates from the Sec Challenge #6 PoC branch
ghavenga-summit-ch6-validity-checks.
How to set up and validate locally
-
Run the specs:
bundle exec rspec ee/spec/lib/security/secret_detection/partner_tokens/heroku_client_spec.rb \ ee/spec/services/security/secret_detection/partner_tokens/registry_spec.rb -
Confirm the registry resolves the token type and that the rate limit rule exists:
Security::SecretDetection::PartnerTokens::Registry.client_for('Heroku API Key') # => #<Security::SecretDetection::PartnerTokens::HerokuClient> Gitlab::ApplicationRateLimiter.period_for(:partner_heroku_api) # => 60 seconds -
Optional, with a real credential — a revoked token should come back
inactiveand a live oneactive:Security::SecretDetection::PartnerTokens::Registry .client_for('Heroku API Key').verify_token(ENV['TOKEN']).status
MR acceptance checklist
I have evaluated this MR against the MR acceptance checklist.