d237098d83
GitHub PATs and Stripe secret keys have no scriptable rotation API (GitHub's create-token API was removed in 2020; Stripe has no create-key endpoint), so they belong in the guided change-password layer, not as Rotators. But they arrive from the env/file scanner as generic Source="env" tokens with no host, so the worklist showed them as "manual — no web page". Recognise them by their well-known PUBLIC value prefixes (ghp_/gho_/ghs_/ github_pat_/…, sk_live_/sk_test_/rk_live_/…) at scan time and record a non-secret Meta["service"] hint — a fixed service NAME, never the secret bytes. discover.ServiceForSecret does the detection; env.go attaches it for generic tokens, file.go before Store wipes the buffer. links.HostFor consults the hint and maps github→github.com / stripe→stripe.com, so the curated change-password URLs (already in the table) now light up for these credentials. Leak-safe (service name is derived from a public prefix, not the secret) and verified by the existing assertNoLeak checks. go build/vet clean; full suite 179 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
46 lines
2.0 KiB
Go
46 lines
2.0 KiB
Go
package discover
|
|
|
|
import "bytes"
|
|
|
|
// MetaService is the Meta key under which a scanner records the issuing service of
|
|
// a credential it recognised by value-prefix (e.g. "github", "stripe"). It is a
|
|
// NON-SECRET hint: the value is a fixed service name, never the secret material.
|
|
// The links/worklist layer turns it into a guided change-password URL for
|
|
// credentials that have no scriptable rotation API (GitHub PATs, Stripe keys).
|
|
const MetaService = "service"
|
|
|
|
// servicePrefixes maps a credential's well-known, PUBLIC value prefix to the
|
|
// service that issued it. Each prefix is a documented format marker — not the
|
|
// secret itself — so matching one and recording the service name leaks nothing.
|
|
var servicePrefixes = []struct {
|
|
prefix []byte
|
|
service string
|
|
}{
|
|
{[]byte("github_pat_"), "github"}, // fine-grained PAT
|
|
{[]byte("ghp_"), "github"}, // classic personal access token
|
|
{[]byte("gho_"), "github"}, // OAuth access token
|
|
{[]byte("ghu_"), "github"}, // user-to-server token
|
|
{[]byte("ghs_"), "github"}, // server-to-server token
|
|
{[]byte("ghr_"), "github"}, // refresh token
|
|
{[]byte("sk_live_"), "stripe"}, // secret key (live)
|
|
{[]byte("sk_test_"), "stripe"}, // secret key (test)
|
|
{[]byte("rk_live_"), "stripe"}, // restricted key (live)
|
|
{[]byte("rk_test_"), "stripe"}, // restricted key (test)
|
|
}
|
|
|
|
// ServiceForSecret recognises a credential by its well-known value prefix and
|
|
// returns the issuing service ("github", "stripe", …) or "" if unrecognised. It
|
|
// inspects only the leading bytes and returns a fixed service NAME, so no secret
|
|
// material escapes. Callers attach the result as Meta[MetaService] so the
|
|
// links/worklist layer can offer a guided rotation page for credentials that
|
|
// cannot be rotated through a provider API.
|
|
func ServiceForSecret(b []byte) string {
|
|
head := bytes.TrimLeft(b, " \t\r\n")
|
|
for _, sp := range servicePrefixes {
|
|
if bytes.HasPrefix(head, sp.prefix) {
|
|
return sp.service
|
|
}
|
|
}
|
|
return ""
|
|
}
|