The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/serviceaccounts/secretscan/service.go

161 lines
5.0 KiB

package secretscan
import (
"context"
"fmt"
"time"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/setting"
)
const defaultURL = "https://secret-scanning.grafana.net"
type Checker interface {
CheckTokens(ctx context.Context) error
}
type CheckerClient interface {
CheckTokens(ctx context.Context, keyHashes []string) ([]Token, error)
}
type WebHookClient interface {
Notify(ctx context.Context, token *Token, tokenName string, revoked bool) error
}
type SATokenRetriever interface {
ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error)
RevokeServiceAccountToken(ctx context.Context, orgID, serviceAccountID, tokenID int64) error
}
// Secret Scan Service is grafana's service for checking leaked keys.
type Service struct {
store SATokenRetriever
client CheckerClient
webHookClient WebHookClient
logger log.Logger
webHookNotify bool
revoke bool // whether to revoke leaked tokens
}
func NewService(store SATokenRetriever, cfg *setting.Cfg) (*Service, error) {
secretscanBaseURL := cfg.SectionWithEnvOverrides("secretscan").Key("base_url").MustString(defaultURL)
// URL to send outgoing webhook when a token is leaked.
oncallURL := cfg.SectionWithEnvOverrides("secretscan").Key("oncall_url").MustString("")
revoke := cfg.SectionWithEnvOverrides("secretscan").Key("revoke").MustBool(true)
client, err := newClient(secretscanBaseURL, cfg.BuildVersion, cfg.Env == setting.Dev)
if err != nil {
return nil, fmt.Errorf("failed to create secretscan client: %w", err)
}
var webHookClient WebHookClient
if oncallURL != "" {
var errWebhook error
webHookClient, errWebhook = newWebHookClient(oncallURL, cfg.BuildVersion, cfg.Env == setting.Dev)
if errWebhook != nil {
return nil, fmt.Errorf("failed to create secretscan webhook client: %w", errWebhook)
}
}
return &Service{
store: store,
client: client,
webHookClient: webHookClient,
logger: log.New("secretscan"),
webHookNotify: oncallURL != "",
revoke: revoke,
}, nil
}
func (s *Service) RetrieveActiveTokens(ctx context.Context) ([]apikey.APIKey, error) {
saTokens, err := s.store.ListTokens(ctx, &serviceaccounts.GetSATokensQuery{})
if err != nil {
return nil, fmt.Errorf("failed to retrieve service account tokens: %w", err)
}
return saTokens, nil
}
// hasExpired returns true if the token has expired.
// Duplicate to SA API. Remerge.
func hasExpired(expiration *int64) bool {
if expiration == nil {
return false
}
v := time.Unix(*expiration, 0)
return (v).Before(time.Now())
}
// CheckTokens checks for leaked tokens.
func (s *Service) CheckTokens(ctx context.Context) error {
// Retrieve all active tokens from the database.
tokens, err := s.RetrieveActiveTokens(ctx)
if err != nil {
return fmt.Errorf("failed to retrieve tokens for checking: %w", err)
}
hashes, hashMap := s.filterCheckableTokens(tokens)
if len(hashes) == 0 {
s.logger.Debug("No active tokens to check")
return nil
}
// Check if any leaked tokens exist.
secretscanTokens, err := s.client.CheckTokens(ctx, hashes)
if err != nil {
return fmt.Errorf("failed to check tokens: %w", err)
}
// Revoke leaked tokens.
// Could be done in bulk but we don't expect more than 1 or 2 tokens to be leaked per check.
for _, secretscanToken := range secretscanTokens {
secretscanToken := secretscanToken
leakedToken := hashMap[secretscanToken.Hash]
if s.revoke {
if err := s.store.RevokeServiceAccountToken(
ctx, leakedToken.OrgID, *leakedToken.ServiceAccountId, leakedToken.ID); err != nil {
s.logger.Error("Failed to delete leaked token. Revoke manually.",
"error", err, "url", secretscanToken.URL, "reported_at", secretscanToken.ReportedAt,
"token_id", leakedToken.ID, "token", leakedToken.Name, "org", leakedToken.OrgID,
"serviceAccount", *leakedToken.ServiceAccountId)
}
}
if s.webHookNotify {
if err := s.webHookClient.Notify(ctx, &secretscanToken, leakedToken.Name, s.revoke); err != nil {
s.logger.Warn("Failed to call token leak webhook", "error", err)
}
}
s.logger.Warn("Found leaked token",
"url", secretscanToken.URL, "reported_at", secretscanToken.ReportedAt,
"token_id", leakedToken.ID, "token", leakedToken.Name, "org", leakedToken.OrgID,
"serviceAccount", *leakedToken.ServiceAccountId, "revoked", s.revoke)
}
return nil
}
// filterCheckableTokens returns a list of tokens that can be checked and a map of tokens to their hashes.
func (*Service) filterCheckableTokens(tokens []apikey.APIKey) ([]string, map[string]apikey.APIKey) {
hashes := make([]string, 0, len(tokens))
hashMap := make(map[string]apikey.APIKey)
for _, token := range tokens {
if hasExpired(token.Expires) || (token.IsRevoked != nil && *token.IsRevoked) {
continue
}
hashes = append(hashes, token.Key)
hashMap[token.Key] = token
}
return hashes, hashMap
}