grafana/pkg/services/serviceaccounts/secretscan/client.go
Emil Tullstedt 10ee900beb
Errors: Remove direct dependencies on github.com/pkg/errors (#64026)
Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
2023-03-02 16:28:10 +01:00

156 lines
3.6 KiB
Go

package secretscan
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"strings"
"time"
)
const timeout = 4 * time.Second
const maxTokensPerRequest = 100
// SecretScan Client is grafana's client for checking leaked keys.
// Don't use this client directly,
// use the secretscan Service which handles token collection and checking instead.
type client struct {
httpClient *http.Client
version string
baseURL string
}
type secretscanRequest struct {
KeyHashes []string `json:"hashes"`
}
type Token struct {
Type string `json:"type"`
URL string `json:"url"`
Hash string `json:"hash"`
ReportedAt string `json:"reported_at"` //nolint
}
var (
ErrInvalidStatusCode = errors.New("invalid status code")
errSecretScanURL = errors.New("secretscan url must be https")
)
func newClient(url, version string, dev bool) (*client, error) {
if !strings.HasPrefix(url, "https://") && !dev {
return nil, errSecretScanURL
}
return &client{
version: version,
baseURL: url,
httpClient: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
Renegotiation: tls.RenegotiateFreelyAsClient,
},
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: timeout,
KeepAlive: 15 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConns: 100,
IdleConnTimeout: 30 * time.Second,
},
Timeout: time.Second * 30,
},
}, nil
}
// checkTokens checks if any leaked tokens exist.
// Returns list of leaked tokens.
func (c *client) CheckTokens(ctx context.Context, keyHashes []string) ([]Token, error) {
// decode response body
tokens := make([]Token, 0, len(keyHashes))
// batch requests to secretscan server
err := batch(len(keyHashes), maxTokensPerRequest, func(start, end int) error {
bTokens, err := c.checkTokens(ctx, keyHashes[start:end])
if err != nil {
return err
}
tokens = append(tokens, bTokens...)
return nil
})
if err != nil {
return nil, err
}
return tokens, nil
}
func (c *client) checkTokens(ctx context.Context, keyHashes []string) ([]Token, error) {
// create request body
values := secretscanRequest{KeyHashes: keyHashes}
jsonValue, err := json.Marshal(values)
if err != nil {
return nil, fmt.Errorf("%s: %w", "failed to make http request", err)
}
// Build URL
url := fmt.Sprintf("%s/tokens", c.baseURL)
// Create request for secretscan server
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
url, bytes.NewReader(jsonValue))
if err != nil {
return nil, fmt.Errorf("%s: %w", "failed to make http request", err)
}
// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "grafana-secretscan-client/"+c.version)
// make http POST request to check for leaked tokens.
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("%s: %w", "failed to do http request", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w. status code: %s", ErrInvalidStatusCode, resp.Status)
}
// decode response body
var tokens []Token
if err := json.NewDecoder(resp.Body).Decode(&tokens); err != nil {
return nil, fmt.Errorf("%s: %w", "failed to decode response body", err)
}
return tokens, nil
}
func batch(count, size int, eachFn func(start, end int) error) error {
for i := 0; i < count; {
end := i + size
if end > count {
end = count
}
if err := eachFn(i, end); err != nil {
return err
}
i = end
}
return nil
}