* detectors/github/v2: differentiate token type in ExtraData FromData matched all six GitHub token prefixes (ghp_, github_pat_, gho_, ghu_, ghs_, ghr_) with a single regex, but every match was tagged with the same DetectorType_Github and the same hardcoded PAT description. The matched prefix was discarded before it could reach any downstream consumer. Add token_type and remediation to ExtraData, following the same convention already used by the launchdarkly, slack, and larksuite detectors, so consumers can tell a leaked OAuth/GitHub App token apart from a classic or fine-grained PAT and point users at the correct GitHub settings page to revoke it. * detectors/github/v2: drop remediation field, add token_type test coverage Per review feedback on #5223: remove the remediation field from ExtraData (maintainers are expanding howtorotate.com docs instead, and want to keep the additive ExtraData surface minimal for downstream consumers). This also moots the ghr_ remediation-URL bug Bugbot flagged, since that field no longer exists. Add table-driven tests for token_type mapping plus a drift guard ensuring every keyPat prefix has a tokenTypesByPrefix entry. * detectors/github/v2: fix integration test expectations for token_type Bugbot caught this on the merge-into-branch commit: github_integration_test.go does an exact ExtraData comparison via pretty.Compare and didn't account for the new token_type field, so every case would fail under the detectors build tag. Add the expected token_type per case. * detectors/github/v2: derive drift guard from keyPat's own regex source TestGithubTokenType_KeyPatPrefixesCovered compared tokenTypesByPrefix against a second hand-maintained prefix list, so a new prefix added to keyPat and forgotten in the map could also be forgotten in that list, leaving the "drift guard" green with nothing to catch. Replace it with TestGithubTokenType_MappingMatchesKeyPatExactly, which parses the prefix alternation out of keyPat.String() directly, and TestGithubTokenType_EveryKeyPatPrefixResolves, which builds a token per derived prefix and confirms it resolves to a real type end to end. * detectors/github/v2: add row-level validation for tokenTypesByPrefix The keyPat drift guards check that map keys line up with the regex; they say nothing about whether an individual row is well-formed. A malformed row with a correct key (blank value, or a value copy-pasted from another prefix) would slip through both. Add TestTokenTypesByPrefix_RowsAreWellFormed to check each row in isolation: prefix key ends in "_", value is non-blank, value isn't the reserved "Unknown GitHub token" fallback, and no two prefixes share a value. Verified it fails on an injected duplicate-value row before reverting the injection. * detectors/github/v2: make row-blank-field check reflect-based, forward-looking TestTokenTypesByPrefix_RowsAreWellFormed only checked the current string value for blankness. That check is presence-only in the sense the removed TestGithubTokenType_KeyPatPrefixesCovered was: neither would have caught a row whose value type is a struct with a field left at its zero value (the shape tokenTypesByPrefix had before remediation was dropped, and could have again). Replace the direct blank check with assertNoBlankFields, which recurses into struct fields via reflection, so a future second field on a row is covered automatically instead of needing a matching manual check. Verified against a probe using the pre-removal {TokenType, Remediation} struct shape with one field left blank before writing this commit.
125 lines
4.0 KiB
Go
125 lines
4.0 KiB
Go
package github
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
regexp "github.com/wasilibs/go-re2"
|
|
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
|
v1 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/github/v1"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
|
|
)
|
|
|
|
type Scanner struct {
|
|
v1.Scanner
|
|
}
|
|
|
|
// Ensure the Scanner satisfies the interfaces at compile time.
|
|
var _ detectors.Detector = (*Scanner)(nil)
|
|
var _ detectors.Versioner = (*Scanner)(nil)
|
|
var _ detectors.EndpointCustomizer = (*Scanner)(nil)
|
|
var _ detectors.CloudProvider = (*Scanner)(nil)
|
|
|
|
func (s Scanner) Version() int {
|
|
return 2
|
|
}
|
|
func (Scanner) CloudEndpoint() string { return "https://api.github.com" }
|
|
|
|
var (
|
|
// Oauth token
|
|
// https://developer.github.com/v3/#oauth2-token-sent-in-a-header
|
|
// Token type list:
|
|
// https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/
|
|
// https://github.blog/changelog/2022-10-18-introducing-fine-grained-personal-access-tokens/
|
|
keyPat = regexp.MustCompile(`\b((?:ghp|gho|ghu|ghs|ghr|github_pat)_[a-zA-Z0-9_]{36,255})\b`)
|
|
|
|
// TODO: Oauth2 client_id and client_secret
|
|
// https://developer.github.com/v3/#oauth2-keysecret
|
|
|
|
// tokenTypesByPrefix maps each GitHub token prefix to a human-readable type.
|
|
// Each prefix identifies a materially different credential (PAT, OAuth
|
|
// grant, or GitHub App token) that is revoked/rotated through a different
|
|
// GitHub settings page.
|
|
// https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/
|
|
tokenTypesByPrefix = map[string]string{
|
|
"ghp_": "Personal Access Token (classic)",
|
|
"github_pat_": "Personal Access Token (fine-grained)",
|
|
"gho_": "OAuth Access Token",
|
|
"ghu_": "GitHub App User-to-Server Token",
|
|
"ghs_": "GitHub App Server-to-Server (installation) Token",
|
|
"ghr_": "GitHub App Refresh Token",
|
|
}
|
|
)
|
|
|
|
// githubTokenType returns the human-readable token type for a matched token,
|
|
// keyed off its prefix. Falls back to a generic label if no known prefix
|
|
// matches (should not happen given keyPat, but keeps this safe).
|
|
func githubTokenType(token string) string {
|
|
for prefix, tokenType := range tokenTypesByPrefix {
|
|
if strings.HasPrefix(token, prefix) {
|
|
return tokenType
|
|
}
|
|
}
|
|
return "Unknown GitHub token"
|
|
}
|
|
|
|
// Keywords are used for efficiently pre-filtering chunks.
|
|
// Use identifiers in the secret preferably, or the provider name.
|
|
func (s Scanner) Keywords() []string {
|
|
return []string{"ghp_", "gho_", "ghu_", "ghs_", "ghr_", "github_pat_"}
|
|
}
|
|
|
|
// FromData will find and optionally verify GitHub secrets in a given set of bytes.
|
|
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
|
|
dataStr := string(data)
|
|
|
|
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
|
|
|
|
for _, match := range matches {
|
|
// First match is entire regex, second is the first group.
|
|
|
|
token := match[1]
|
|
|
|
s1 := detectors.Result{
|
|
DetectorType: detector_typepb.DetectorType_Github,
|
|
Raw: []byte(token),
|
|
ExtraData: map[string]string{
|
|
"rotation_guide": "https://howtorotate.com/docs/tutorials/github/",
|
|
"version": fmt.Sprintf("%d", s.Version()),
|
|
"token_type": githubTokenType(token),
|
|
},
|
|
SecretParts: map[string]string{"key": token},
|
|
}
|
|
|
|
if verify {
|
|
client := common.SaneHttpClient()
|
|
|
|
isVerified, userResponse, headers, err := s.VerifyGithub(ctx, client, token)
|
|
s1.Verified = isVerified
|
|
s1.SetVerificationError(err, token)
|
|
|
|
if userResponse != nil {
|
|
v1.SetUserResponse(userResponse, &s1)
|
|
}
|
|
if headers != nil {
|
|
v1.SetHeaderInfo(headers, &s1)
|
|
}
|
|
}
|
|
|
|
results = append(results, s1)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (s Scanner) Type() detector_typepb.DetectorType {
|
|
return detector_typepb.DetectorType_Github
|
|
}
|
|
|
|
func (s Scanner) Description() string {
|
|
return "GitHub is a platform for version control and collaboration. Personal access tokens (PATs) can be used to access and modify repositories and other resources."
|
|
}
|