* initial commit * initial structure * initial commit * added databricks analyzer * removed print statement * resolved ahrav the legend comments * resolved comments * fixed issue
299 lines
7.8 KiB
Go
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")
|
|
}
|