Add SendGrid API token validity check

What does this MR do and why?

This adds a validity check for SendGrid API tokens (starting with prefix SG.).

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. SendgridClient.

The verifier calls GET https://api.sendgrid.com/v3/scopes (reference) with the token as a Bearer credential.

That endpoint lists the scopes the key has, so it answers without sending mail or changing anything.

We map the responses to the following statuses/outcomes:

Response Outcome
200 active
401 inactive
429 RateLimitError
500, 502, 503 NetworkError
anything else, including 403 unknown

Please note that aside from 401, no other status falls to inactive.

Reporting a live token as dead is worse than reporting nothing, so unmapped statuses are always unknown.

Why 403 is not inactive

SendGrid's response-code reference documents 401 as "Requires authentication" – that is the code meaning the credential is no good, and the one we map to inactive.

The two documented causes of 403 both describe an account that is authenticated and working: "From address doesn't match Verified Sender Identity" and "You are temporarily blocked from sending emails due to repeated bad requests." A misconfigured sender or a temporary sending block is not a dead key, so 403 falls through to 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:

  1. 403 was mapped to inactive. Checking that against SendGrid's documented response codes showed it was wrong for the reason above, so 403 is now unmapped and resolves to unknown.
  2. The token pattern used to spell out the 22.43 segment shape. It now mirrors the detection rule exactly, dot-anywhere included. The narrower version rejected any detected string that was not in that shape, and a format rejection returns unknown without recording a metric or a log line – so those findings would never have been attempted and nothing would have shown it. Mirroring the rule means everything the scanner detects is actually checked.

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/sendgrid_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('Sendgrid API token')
    # => #<Security::SecretDetection::PartnerTokens::SendgridClient>
    Gitlab::ApplicationRateLimiter.period_for(:partner_sendgrid_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('Sendgrid API token').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