Files
trufflehog/pkg/analyzer/analyzers/databricks/requests.go
Kashif Khan 9a2b2681e2 Added DataBricks Analyzer (#4135)
* initial commit

* initial structure

* initial commit

* added databricks analyzer

* removed print statement

* resolved ahrav the legend comments

* resolved comments

* fixed issue
2025-05-20 10:55:44 +05:00

299 lines
7.8 KiB
Go

package databricks
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var (
// ErrUnauthorized is returned when the Databricks API answers with HTTP-401.
errUnAuthorized = errors.New("invalid/expired personal access token")
apiEndpoints = map[ResourceType]string{
CurrentUser: "/api/2.0/preview/scim/v2/Me",
TokensInfo: "/api/2.0/token-management/tokens",
TokenPermissions: "/api/2.0/permissions/authorization/tokens/permissionLevels",
Repositories: "/api/2.0/repos",
GitCredentials: "/api/2.0/git-credentials",
Jobs: "/api/2.2/jobs/list",
Clusters: "/api/2.1/clusters/list",
Groups: "/api/2.0/preview/scim/v2/Groups",
Users: "/api/2.0/preview/scim/v2/Users",
/*
TODO:
- https://docs.databricks.com/api/gcp/workspace/workspace/list (list content inside path)
- http://docs.databricks.com/api/gcp/workspace/libraries/allclusterlibrarystatuses (list cluster statuses)
*/
}
)
// doAndDecode performs an authenticated GET request against the constructed
// Databricks URL and JSON-decodes the response into the supplied result.
//
// The generic type parameter T allows the caller to decide which concrete
// struct the response should be unmarshalled into:
func doAndDecode[T any](ctx context.Context, client *http.Client, domain string, rt ResourceType, token string, out *T) error {
u := url.URL{
Scheme: "https",
Host: domain,
Path: apiEndpoints[rt],
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), http.NoBody)
if err != nil {
return fmt.Errorf("building request: %w", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("Accept", "application/json")
// Execute request and read / decode body. We stream directly into the
// decoder instead of loading the whole response into memory first.
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("performing request: %w", err)
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
return fmt.Errorf("decoding response: %w", err)
}
return nil
case http.StatusUnauthorized:
return errUnAuthorized
default:
return fmt.Errorf("unexpected status code %d for API %s", resp.StatusCode, apiEndpoints[rt])
}
}
func captureDataBricksResources(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
if err := captureRepos(ctx, client, domain, token, secretInfo); err != nil {
return err
}
if err := captureGitCreds(ctx, client, domain, token, secretInfo); err != nil {
return err
}
if err := captureJobs(ctx, client, domain, token, secretInfo); err != nil {
return err
}
if err := captureClusters(ctx, client, domain, token, secretInfo); err != nil {
return err
}
if err := captureGroups(ctx, client, domain, token, secretInfo); err != nil {
return err
}
if err := captureUsers(ctx, client, domain, token, secretInfo); err != nil {
return err
}
return nil
}
func captureUserInfo(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
var user CurrentUserInfo
if err := doAndDecode(ctx, client, domain, CurrentUser, token, &user); err != nil {
return err
}
secretInfo.UserInfo = User{
ID: user.ID,
UserName: user.UserName,
}
for _, email := range user.Emails {
if email.Primary {
secretInfo.UserInfo.PrimaryEmail = email.Value
}
}
return nil
}
func captureTokensInfo(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
var tokens Tokens
if err := doAndDecode(ctx, client, domain, TokensInfo, token, &tokens); err != nil {
return err
}
for _, t := range tokens.TokensInfo {
secretInfo.Tokens = append(secretInfo.Tokens, Token{
ID: t.ID,
Name: t.Name,
ExpiryTime: readableTime(t.ExpiryTime),
LastUsedDay: readableTime(t.LastUsedDay),
CreatedBy: t.CreatedBy,
})
}
return nil
}
func captureTokenPermissions(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
var permissions Permissions
if err := doAndDecode(ctx, client, domain, TokenPermissions, token, &permissions); err != nil {
return err
}
for _, item := range permissions.PermissionLevels {
secretInfo.TokenPermissionLevels = append(secretInfo.TokenPermissionLevels, item.PermissionLevel)
}
return nil
}
func captureRepos(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
var repos ReposResponse
if err := doAndDecode(ctx, client, domain, Repositories, token, &repos); err != nil {
return err
}
for _, repo := range repos.Repositories {
if repo.ID == "" {
repo.ID = repo.URL
}
secretInfo.Resources = append(secretInfo.Resources, DataBricksResource{
ID: repo.ID,
Name: repo.Path,
Type: Repositories.String(),
Metadata: map[string]string{
"provider": repo.Provider,
"url": repo.URL,
},
})
}
return nil
}
func captureGitCreds(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
var creds GitCreds
if err := doAndDecode(ctx, client, domain, GitCredentials, token, &creds); err != nil {
return err
}
for _, credential := range creds.Credentials {
secretInfo.Resources = append(secretInfo.Resources, DataBricksResource{
ID: credential.ID,
Name: credential.UserName,
Type: GitCredentials.String(),
Metadata: map[string]string{
"provider": credential.Provider,
},
})
}
return nil
}
func captureJobs(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
var jobs JobsResponse
if err := doAndDecode(ctx, client, domain, Jobs, token, &jobs); err != nil {
return err
}
for _, job := range jobs.Jobs {
secretInfo.Resources = append(secretInfo.Resources, DataBricksResource{
ID: job.ID,
Name: job.Name,
Type: Jobs.String(),
Metadata: map[string]string{
"description": job.Description,
},
})
}
return nil
}
func captureClusters(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
var clusters ClustersResponse
if err := doAndDecode(ctx, client, domain, Clusters, token, &clusters); err != nil {
return err
}
for _, cluster := range clusters.Clusters {
secretInfo.Resources = append(secretInfo.Resources, DataBricksResource{
ID: cluster.ID,
Name: cluster.Name,
Type: Clusters.String(),
Metadata: map[string]string{
"created by": cluster.CreatedBy,
},
})
}
return nil
}
func captureGroups(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
var groups GroupsResponse
if err := doAndDecode(ctx, client, domain, Groups, token, &groups); err != nil {
return err
}
for _, group := range groups.Resources {
secretInfo.Resources = append(secretInfo.Resources, DataBricksResource{
ID: group.ID,
Name: group.Name,
Type: Groups.String(),
})
}
return nil
}
func captureUsers(ctx context.Context, client *http.Client, domain, token string, secretInfo *SecretInfo) error {
var users UsersResponse
if err := doAndDecode(ctx, client, domain, Users, token, &users); err != nil {
return err
}
for _, user := range users.Resources {
secretInfo.Resources = append(secretInfo.Resources, DataBricksResource{
ID: user.ID,
Name: user.UserName,
Type: Users.String(),
Metadata: map[string]string{
"active": fmt.Sprintf("%t", user.Active),
},
})
}
return nil
}
func readableTime(timestamp int) string {
timestampMillis := int64(timestamp)
t := time.Unix(timestampMillis/1000, (timestampMillis%1000)*int64(time.Millisecond))
return t.Format("2006-01-02 15:04:05")
}