* 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.
251 lines
6.8 KiB
Go
251 lines
6.8 KiB
Go
//go:build detectors
|
|
// +build detectors
|
|
|
|
package github
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/kylelemons/godebug/pretty"
|
|
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
|
|
)
|
|
|
|
func TestGitHub_FromChunk(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
|
defer cancel()
|
|
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
|
|
if err != nil {
|
|
t.Fatalf("could not get test secrets from GCP: %s", err)
|
|
}
|
|
unverifiedGhp := testSecrets.MustGetField("GITHUB_UNVERIFIED_GHP")
|
|
unverifiedGhpLong := testSecrets.MustGetField("GITHUB_UNVERIFIED_GHP_LONG")
|
|
unverifiedGho := testSecrets.MustGetField("GITHUB_UNVERIFIED_GHO")
|
|
unverifiedGhu := testSecrets.MustGetField("GITHUB_UNVERIFIED_GHU")
|
|
unverifiedGhs := testSecrets.MustGetField("GITHUB_UNVERIFIED_GHS")
|
|
unverifiedGhr := testSecrets.MustGetField("GITHUB_UNVERIFIED_GHR")
|
|
verifiedGhp := testSecrets.MustGetField("GITHUB_VERIFIED_GHP")
|
|
|
|
type args struct {
|
|
ctx context.Context
|
|
data []byte
|
|
verify bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
s Scanner
|
|
args args
|
|
want []detectors.Result
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "found, verified ghp",
|
|
s: Scanner{},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
data: []byte(fmt.Sprintf("You can find a github secret %s within", verifiedGhp)),
|
|
verify: true,
|
|
},
|
|
want: []detectors.Result{
|
|
{
|
|
DetectorType: detector_typepb.DetectorType_Github,
|
|
Verified: true,
|
|
ExtraData: map[string]string{
|
|
"account_type": "User",
|
|
// "company": "", // not present in test verifiedGhp
|
|
// "name": "", // not present in test verifiedGhp
|
|
"rotation_guide": "https://howtorotate.com/docs/tutorials/github/",
|
|
"scopes": "notifications",
|
|
// "site_admin": "false", // not present in test verifiedGhp
|
|
"token_type": "Personal Access Token (classic)",
|
|
"url": "https://github.com/truffle-sandbox",
|
|
"username": "truffle-sandbox",
|
|
"version": "2",
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "found, unverified ghp",
|
|
s: Scanner{},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
data: []byte(fmt.Sprintf("You can find a github secret %s within", unverifiedGhp)),
|
|
verify: true,
|
|
},
|
|
want: []detectors.Result{
|
|
{
|
|
DetectorType: detector_typepb.DetectorType_Github,
|
|
Verified: false,
|
|
ExtraData: map[string]string{
|
|
"rotation_guide": "https://howtorotate.com/docs/tutorials/github/",
|
|
"token_type": "Personal Access Token (classic)",
|
|
"version": "2",
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "found, unverified gho",
|
|
s: Scanner{},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
data: []byte(fmt.Sprintf("You can find a github secret %s within", unverifiedGho)),
|
|
verify: true,
|
|
},
|
|
want: []detectors.Result{
|
|
{
|
|
DetectorType: detector_typepb.DetectorType_Github,
|
|
Verified: false,
|
|
ExtraData: map[string]string{
|
|
"rotation_guide": "https://howtorotate.com/docs/tutorials/github/",
|
|
"token_type": "OAuth Access Token",
|
|
"version": "2",
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "found, unverified ghu",
|
|
s: Scanner{},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
data: []byte(fmt.Sprintf("You can find a github secret %s within", unverifiedGhu)),
|
|
verify: true,
|
|
},
|
|
want: []detectors.Result{
|
|
{
|
|
DetectorType: detector_typepb.DetectorType_Github,
|
|
Verified: false,
|
|
ExtraData: map[string]string{
|
|
"rotation_guide": "https://howtorotate.com/docs/tutorials/github/",
|
|
"token_type": "GitHub App User-to-Server Token",
|
|
"version": "2",
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "found, unverified ghs",
|
|
s: Scanner{},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
data: []byte(fmt.Sprintf("You can find a github secret %s within", unverifiedGhs)),
|
|
verify: true,
|
|
},
|
|
want: []detectors.Result{
|
|
{
|
|
DetectorType: detector_typepb.DetectorType_Github,
|
|
Verified: false,
|
|
ExtraData: map[string]string{
|
|
"rotation_guide": "https://howtorotate.com/docs/tutorials/github/",
|
|
"token_type": "GitHub App Server-to-Server (installation) Token",
|
|
"version": "2",
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "found, unverified ghr",
|
|
s: Scanner{},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
data: []byte(fmt.Sprintf("You can find a github secret %s within", unverifiedGhr)),
|
|
verify: true,
|
|
},
|
|
want: []detectors.Result{
|
|
{
|
|
DetectorType: detector_typepb.DetectorType_Github,
|
|
Verified: false,
|
|
ExtraData: map[string]string{
|
|
"rotation_guide": "https://howtorotate.com/docs/tutorials/github/",
|
|
"token_type": "GitHub App Refresh Token",
|
|
"version": "2",
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "found, unverified ghp future length 255",
|
|
s: Scanner{},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
data: []byte(fmt.Sprintf("You can find a github secret %s within", unverifiedGhpLong)),
|
|
verify: true,
|
|
},
|
|
want: []detectors.Result{
|
|
{
|
|
DetectorType: detector_typepb.DetectorType_Github,
|
|
Verified: false,
|
|
ExtraData: map[string]string{
|
|
"rotation_guide": "https://howtorotate.com/docs/tutorials/github/",
|
|
"token_type": "Personal Access Token (classic)",
|
|
"version": "2",
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "not found",
|
|
s: Scanner{},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
data: []byte("https://raw.github.com/k/d890e8640f20fba3215ba7be8e0ff145aeb8c17c/include/base64.js"),
|
|
verify: true,
|
|
},
|
|
want: nil,
|
|
wantErr: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
s := Scanner{}
|
|
s.UseCloudEndpoint(true)
|
|
s.SetCloudEndpoint(s.CloudEndpoint())
|
|
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("GitHub.FromData() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
for i := range got {
|
|
if len(got[i].Raw) == 0 {
|
|
t.Fatal("no raw secret present")
|
|
}
|
|
got[i].Raw = nil
|
|
got[i].SecretParts = nil
|
|
}
|
|
if diff := pretty.Compare(got, tt.want); diff != "" {
|
|
t.Errorf("GitHub.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkFromData(benchmark *testing.B) {
|
|
ctx := context.Background()
|
|
s := Scanner{}
|
|
for name, data := range detectors.MustGetBenchmarkData() {
|
|
benchmark.Run(name, func(b *testing.B) {
|
|
b.ResetTimer()
|
|
for n := 0; n < b.N; n++ {
|
|
_, err := s.FromData(ctx, false, data)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|