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.com using HTTPS and specify the Accept: application/vnd.heroku+json; version=3 Accept 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:

  1. Token pattern. It was a bare UUID, but the Heroku API Key rule matches HRKU- followed by a UUID, and that rule lists a bare UUID as an explicit negativeExample. 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.
  2. Accept header. Without the versioned Accept header, Heroku answers 406 Not Acceptable (not_acceptable: "request failed, set Accept: application/vnd.heroku+json; version=3 header and try again"). 406 is unmapped, so every token — live or dead — would have come back unknown. 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

How to set up and validate locally

  1. 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
  2. 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
  3. Optional, with a real credential — a revoked token should come back inactive and a live one active:

    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.

Edited by Ahmed Hemdan

Merge request reports

Loading
Loading