-
Notifications
You must be signed in to change notification settings - Fork 2.3k
[INS-410] Added batch token detector #4824
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4b2495d
97994b3
199207d
ea378bd
4792fae
8710f1c
3ad745e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| package hashicorpbatchtoken | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "net/url" | ||
| "strings" | ||
|
|
||
| regexp "github.com/wasilibs/go-re2" | ||
|
|
||
| "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" | ||
| "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" | ||
| ) | ||
|
|
||
| type Scanner struct { | ||
| client *http.Client | ||
| detectors.DefaultMultiPartCredentialProvider | ||
| detectors.EndpointSetter | ||
| } | ||
|
|
||
| var _ detectors.Detector = (*Scanner)(nil) | ||
| var _ detectors.EndpointCustomizer = (*Scanner)(nil) | ||
|
|
||
| var ( | ||
| defaultClient = detectors.DetectorHttpClientWithNoLocalAddresses | ||
|
|
||
| // Batch tokens: hvb.<50-300 chars> | ||
| batchTokenPat = regexp.MustCompile( | ||
| `\b(hvb\.[A-Za-z0-9_.-]{50,300})\b`, | ||
| ) | ||
|
|
||
| vaultUrlPat = regexp.MustCompile(`(https?:\/\/[^\s\/]*\.hashicorp\.cloud(?::\d+)?)(?:\/[^\s]*)?`) | ||
| ) | ||
|
|
||
| func (s Scanner) Keywords() []string { | ||
| return []string{"hvb."} | ||
| } | ||
|
|
||
| func (Scanner) CloudEndpoint() string { return "" } | ||
|
|
||
| func (s Scanner) Description() string { | ||
| return "This detector detects and verifies HashiCorp Vault batch tokens" | ||
| } | ||
|
|
||
| func (s Scanner) getClient() *http.Client { | ||
| if s.client != nil { | ||
| return s.client | ||
| } | ||
| return defaultClient | ||
| } | ||
|
|
||
| func (s Scanner) FromData( | ||
| ctx context.Context, | ||
| verify bool, | ||
| data []byte, | ||
| ) (results []detectors.Result, err error) { | ||
|
|
||
| dataStr := string(data) | ||
|
|
||
| uniqueTokens := make(map[string]struct{}) | ||
| for _, match := range batchTokenPat.FindAllStringSubmatch(dataStr, -1) { | ||
| uniqueTokens[match[1]] = struct{}{} | ||
| } | ||
|
|
||
| var uniqueVaultUrls = make(map[string]struct{}) | ||
| for _, match := range vaultUrlPat.FindAllStringSubmatch(dataStr, -1) { | ||
| url := strings.TrimSpace(match[1]) | ||
| uniqueVaultUrls[url] = struct{}{} | ||
| } | ||
|
|
||
| endpoints := make([]string, 0, len(uniqueVaultUrls)) | ||
| for endpoint := range uniqueVaultUrls { | ||
| endpoints = append(endpoints, endpoint) | ||
| } | ||
|
|
||
| for _, endpoint := range s.Endpoints(endpoints...) { | ||
| for token := range uniqueTokens { | ||
| result := detectors.Result{ | ||
| DetectorType: detectorspb.DetectorType_HashiCorpVaultBatchToken, | ||
| Raw: []byte(token), | ||
| RawV2: []byte(token + endpoint), | ||
| Redacted: token[:8] + "...", | ||
| } | ||
|
|
||
| if verify { | ||
| verified, verificationResp, verificationErr := verifyVaultToken( | ||
| ctx, | ||
| s.getClient(), | ||
| endpoint, | ||
| token, | ||
| ) | ||
| result.SetVerificationError(verificationErr, token) | ||
| result.Verified = verified | ||
|
|
||
| if verificationResp != nil { | ||
| result.ExtraData = map[string]string{ | ||
| "policies": strings.Join(verificationResp.Data.Policies, ", "), | ||
| "orphan": fmt.Sprintf("%v", verificationResp.Data.Orphan), | ||
| "renewable": fmt.Sprintf("%v", verificationResp.Data.Renewable), | ||
| "type": verificationResp.Data.Type, | ||
| "entity_id": verificationResp.Data.EntityId, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| results = append(results, result) | ||
| } | ||
| } | ||
|
|
||
| return | ||
| } | ||
|
|
||
| type lookupResponse struct { | ||
| Data struct { | ||
| DisplayName string `json:"display_name"` | ||
| EntityId string `json:"entity_id"` | ||
| ExpireTime string `json:"expire_time"` | ||
| Orphan bool `json:"orphan"` | ||
| Policies []string `json:"policies"` | ||
| Renewable bool `json:"renewable"` | ||
| Type string `json:"type"` | ||
| } | ||
| } | ||
|
|
||
| func verifyVaultToken( | ||
| ctx context.Context, | ||
| client *http.Client, | ||
| baseUrl string, | ||
| token string, | ||
| ) (bool, *lookupResponse, error) { | ||
| url, err := url.JoinPath(baseUrl, "/v1/auth/token/lookup-self") | ||
| if err != nil { | ||
| return false, nil, err | ||
| } | ||
| req, err := http.NewRequestWithContext( | ||
| ctx, | ||
| http.MethodGet, | ||
| url, | ||
| http.NoBody, | ||
| ) | ||
| if err != nil { | ||
| return false, nil, err | ||
| } | ||
|
|
||
| req.Header.Set("X-Vault-Token", token) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing Vault Namespace Header Breaks VerificationHigh Severity The
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to the docs the |
||
|
|
||
| res, err := client.Do(req) | ||
| if err != nil { | ||
| return false, nil, err | ||
| } | ||
| defer func() { | ||
| _, _ = io.Copy(io.Discard, res.Body) | ||
| _ = res.Body.Close() | ||
| }() | ||
|
|
||
| switch res.StatusCode { | ||
| case http.StatusOK: | ||
| var resp lookupResponse | ||
| if err := json.NewDecoder(res.Body).Decode(&resp); err != nil { | ||
| return false, nil, err | ||
| } | ||
|
|
||
| return true, &resp, nil | ||
|
|
||
| case http.StatusForbidden, http.StatusUnauthorized: | ||
| return false, nil, nil | ||
|
|
||
| default: | ||
| return false, nil, fmt.Errorf( | ||
| "unexpected HTTP response status %d", | ||
| res.StatusCode, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| func (s Scanner) Type() detectorspb.DetectorType { | ||
| return detectorspb.DetectorType_HashiCorpVaultBatchToken | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| //go:build detectors | ||
| // +build detectors | ||
|
|
||
| package hashicorpbatchtoken | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/trufflesecurity/trufflehog/v3/pkg/common" | ||
| "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" | ||
| ) | ||
|
|
||
| func TestBatchToken_FromData(t *testing.T) { | ||
| ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) | ||
| defer cancel() | ||
|
|
||
| // Fetch test secrets | ||
| testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6") | ||
| if err != nil { | ||
| t.Fatalf("could not get test secrets: %s", err) | ||
| } | ||
|
|
||
| vaultURL := testSecrets.MustGetField("HASHICORPVAULT_CLOUD_URL") | ||
|
|
||
| // This token has maximum TTL of 32days (768h), so it should still be valid by the time this test runs | ||
| // but if the test fails due to an invalid token, this is the most likely culprit and the token may need to be regenerated. | ||
| // To regenerate the token run this command in vault web cli: | ||
| // write auth/token/create type=batch policies="test-policy" ttl="768h" no_parent=true | ||
| batchToken := testSecrets.MustGetField("HASHICORPVAULT_BATCH_TOKEN") | ||
|
|
||
| fakeToken := "hvb.fakeinvalidtokenaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| input string | ||
| verify bool | ||
| wantTokens []string | ||
| wantVerified bool | ||
| wantVerificationErr bool | ||
| }{ | ||
| { | ||
| name: "valid batch token with URL, verify", | ||
| input: fmt.Sprintf("%s\n%s", batchToken, vaultURL), | ||
| verify: true, | ||
| wantTokens: []string{ | ||
| batchToken + vaultURL, | ||
| }, | ||
| wantVerified: true, | ||
| wantVerificationErr: false, | ||
| }, | ||
| { | ||
| name: "invalid batch token with URL, verify", | ||
| input: fmt.Sprintf("%s\n%s", fakeToken, vaultURL), | ||
| verify: true, | ||
| wantTokens: []string{ | ||
| fakeToken + vaultURL, | ||
| }, | ||
| wantVerified: false, | ||
| wantVerificationErr: false, | ||
| }, | ||
| { | ||
| name: "valid batch token with URL, no verify", | ||
| input: fmt.Sprintf("%s\n%s", batchToken, vaultURL), | ||
| verify: false, | ||
| wantTokens: []string{ | ||
| batchToken + vaultURL, | ||
| }, | ||
| wantVerified: false, | ||
| wantVerificationErr: false, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| scanner := Scanner{} | ||
| scanner.UseFoundEndpoints(true) | ||
| scanner.UseCloudEndpoint(true) | ||
|
|
||
| results, err := scanner.FromData(ctx, tt.verify, []byte(tt.input)) | ||
| require.NoError(t, err) | ||
|
|
||
| if len(results) != len(tt.wantTokens) { | ||
| t.Fatalf("expected %d results, got %d", len(tt.wantTokens), len(results)) | ||
| } | ||
|
|
||
| for i, r := range results { | ||
| if string(r.RawV2) != tt.wantTokens[i] && string(r.Raw) != tt.wantTokens[i] { | ||
| t.Errorf("expected token %s, got %s", tt.wantTokens[i], string(r.Raw)) | ||
| } | ||
|
|
||
| if r.Verified != tt.wantVerified { | ||
| t.Errorf("expected verified=%v, got %v", tt.wantVerified, r.Verified) | ||
| } | ||
|
|
||
| if (r.VerificationError() != nil) != tt.wantVerificationErr { | ||
| t.Errorf("expected verification error=%v, got %v", tt.wantVerificationErr, r.VerificationError()) | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func BenchmarkFromData(b *testing.B) { | ||
| ctx := context.Background() | ||
| s := Scanner{} | ||
|
|
||
| for name, data := range detectors.MustGetBenchmarkData() { | ||
| b.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) | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| } |


Uh oh!
There was an error while loading. Please reload this page.