* Add POC analyze sub-command * Address lint errors * added http logging to most analyzers * Use custom RoundTripper with default http.Client * [chore] Embed scopes at compile time * [chore] Move subcommand check up to prevent printing metrics * Create framework of interfaces, structs, and protos * Implement Analyzer for airbrake * Add FullAccess permission constant * Implement Analyzer for asana * Implement Analyzer for bitbucket * Implement Analyzer for github * Implement Analyzer for gitlab * Implemente Analyzer for huggingface * Implement Analyzer for mailchimp * implement analyzer for mailgun * update cli cmd * Implement analyzer for openai * fix timing issue on scopes * print permissions only if restricted key * Implement Analyzer for mysql * enable loggin check * fixed the formatting issue to wrap sub-errors * implemented analyzer for opsgenie * implemented analyzer for postgres * use format string * implemented analyzer for sendgrid * simplify returning the error * implemented analyzer for postman * added handling of workspace error * Update protos to match OSS * Generate protos * Update data structures to match OSS * Update airbrake implementation * Remove asana implementation * Remove mailchimp implementation * Update openai implementation to match OSS * Remove gitlab implementation * Remove huggingface implementation * Remove bitbucket implementation * Fix permission in airbrake * Remove github implementation * Remove mailgun implementation * Cleanup compiler errors * Implement Analyzer interface for github * Add parents to github resources * Add fine_grained to github metadata * Update with changes from main * Remove unused function stubs --------- Co-authored-by: Joe Leon <[email protected]> Co-authored-by: Hon <[email protected]> Co-authored-by: Abdul Basit <[email protected]> Co-authored-by: Abdul Basit <[email protected]>
296 lines
7.6 KiB
Go
296 lines
7.6 KiB
Go
package gitlab
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/jedib0t/go-pretty/table"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/pb/analyzerpb"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
|
)
|
|
|
|
var _ analyzers.Analyzer = (*Analyzer)(nil)
|
|
|
|
type Analyzer struct {
|
|
Cfg *config.Config
|
|
}
|
|
|
|
func (Analyzer) Type() analyzerpb.AnalyzerType { return analyzerpb.AnalyzerType_GitLab }
|
|
|
|
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
|
|
_, err := AnalyzePermissions(a.Cfg, credInfo["key"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, fmt.Errorf("not implemented")
|
|
}
|
|
|
|
// consider calling /api/v4/metadata to learn about gitlab instance version and whether neterrprises is enabled
|
|
|
|
// we'll call /api/v4/personal_access_tokens and /api/v4/user and then filter down to scopes.
|
|
|
|
type AccessTokenJSON struct {
|
|
Name string `json:"name"`
|
|
Revoked bool `json:"revoked"`
|
|
CreatedAt string `json:"created_at"`
|
|
Scopes []string `json:"scopes"`
|
|
LastUsedAt string `json:"last_used_at"`
|
|
ExpiresAt string `json:"expires_at"`
|
|
}
|
|
|
|
type ProjectsJSON struct {
|
|
NameWithNamespace string `json:"name_with_namespace"`
|
|
Permissions struct {
|
|
ProjectAccess struct {
|
|
AccessLevel int `json:"access_level"`
|
|
} `json:"project_access"`
|
|
} `json:"permissions"`
|
|
}
|
|
|
|
type ErrorJSON struct {
|
|
Error string `json:"error"`
|
|
Scope string `json:"scope"`
|
|
}
|
|
|
|
type MetadataJSON struct {
|
|
Version string `json:"version"`
|
|
Enterprise bool `json:"enterprise"`
|
|
}
|
|
|
|
func getPersonalAccessToken(cfg *config.Config, key string) (AccessTokenJSON, int, error) {
|
|
var tokens AccessTokenJSON
|
|
|
|
client := analyzers.NewAnalyzeClient(cfg)
|
|
req, err := http.NewRequest("GET", "https://gitlab.com/api/v4/personal_access_tokens/self", nil)
|
|
if err != nil {
|
|
return tokens, -1, err
|
|
}
|
|
|
|
req.Header.Set("Private-Token", key)
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return tokens, resp.StatusCode, err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
if err := json.NewDecoder(resp.Body).Decode(&tokens); err != nil {
|
|
return tokens, resp.StatusCode, err
|
|
}
|
|
return tokens, resp.StatusCode, nil
|
|
}
|
|
|
|
func getAccessibleProjects(cfg *config.Config, key string) ([]ProjectsJSON, error) {
|
|
var projects []ProjectsJSON
|
|
|
|
client := analyzers.NewAnalyzeClient(cfg)
|
|
req, err := http.NewRequest("GET", "https://gitlab.com/api/v4/projects", nil)
|
|
if err != nil {
|
|
return projects, err
|
|
}
|
|
|
|
req.Header.Set("Private-Token", key)
|
|
|
|
// Add query parameters
|
|
q := req.URL.Query()
|
|
q.Add("min_access_level", "10")
|
|
req.URL.RawQuery = q.Encode()
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return projects, err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return projects, err
|
|
}
|
|
|
|
newBody := func() io.ReadCloser {
|
|
return io.NopCloser(bytes.NewReader(bodyBytes))
|
|
}
|
|
|
|
if err := json.NewDecoder(newBody()).Decode(&projects); err != nil {
|
|
var e ErrorJSON
|
|
if err := json.NewDecoder(newBody()).Decode(&e); err == nil {
|
|
return projects, fmt.Errorf("Insufficient Scope to query for projects. We need api or read_api permissions.")
|
|
}
|
|
return projects, err
|
|
}
|
|
return projects, nil
|
|
}
|
|
|
|
func getMetadata(cfg *config.Config, key string) (MetadataJSON, error) {
|
|
var metadata MetadataJSON
|
|
|
|
client := analyzers.NewAnalyzeClient(cfg)
|
|
req, err := http.NewRequest("GET", "https://gitlab.com/api/v4/metadata", nil)
|
|
if err != nil {
|
|
return metadata, err
|
|
}
|
|
|
|
req.Header.Set("Private-Token", key)
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return metadata, err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return metadata, err
|
|
}
|
|
|
|
newBody := func() io.ReadCloser {
|
|
return io.NopCloser(bytes.NewReader(bodyBytes))
|
|
}
|
|
|
|
if err := json.NewDecoder(newBody()).Decode(&metadata); err != nil {
|
|
return metadata, err
|
|
}
|
|
|
|
if metadata.Version == "" {
|
|
var e ErrorJSON
|
|
if err := json.NewDecoder(newBody()).Decode(&e); err != nil {
|
|
return metadata, err
|
|
}
|
|
return metadata, fmt.Errorf("Insufficient Scope to query for metadata. We need read_user, ai_features, api or read_api permissions.")
|
|
}
|
|
|
|
return metadata, nil
|
|
}
|
|
|
|
type SecretInfo struct {
|
|
AccessToken AccessTokenJSON
|
|
Metadata MetadataJSON
|
|
Projects []ProjectsJSON
|
|
}
|
|
|
|
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
|
|
// get personal_access_tokens accessible
|
|
token, statusCode, err := getPersonalAccessToken(cfg, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if statusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("Invalid GitLab Access Token")
|
|
}
|
|
|
|
meta, err := getMetadata(cfg, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
projects, err := getAccessibleProjects(cfg, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &SecretInfo{
|
|
AccessToken: token,
|
|
Metadata: meta,
|
|
Projects: projects,
|
|
}, nil
|
|
}
|
|
|
|
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
|
|
info, err := AnalyzePermissions(cfg, key)
|
|
if err != nil {
|
|
color.Red("[x] Error: %s", err)
|
|
return
|
|
}
|
|
|
|
// print token info
|
|
printTokenInfo(info.AccessToken)
|
|
|
|
// print gitlab instance metadata
|
|
if info.Metadata.Version != "" {
|
|
printMetadata(info.Metadata)
|
|
}
|
|
|
|
// print token permissions
|
|
printTokenPermissions(info.AccessToken)
|
|
|
|
// print repos accessible
|
|
if len(info.Projects) > 0 {
|
|
printProjects(info.Projects)
|
|
}
|
|
}
|
|
|
|
func getRemainingTime(t string) string {
|
|
targetTime, err := time.Parse("2006-01-02", t)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
// Get the current time
|
|
currentTime := time.Now()
|
|
|
|
// Calculate the duration until the target time
|
|
durationUntilTarget := targetTime.Sub(currentTime)
|
|
durationUntilTarget = durationUntilTarget.Truncate(time.Minute)
|
|
|
|
// Print the duration
|
|
return fmt.Sprintf("%v", durationUntilTarget)
|
|
}
|
|
|
|
func printTokenInfo(token AccessTokenJSON) {
|
|
color.Green("[!] Valid GitLab Access Token\n\n")
|
|
color.Green("Token Name: %s\n", token.Name)
|
|
color.Green("Created At: %s\n", token.CreatedAt)
|
|
color.Green("Last Used At: %s\n", token.LastUsedAt)
|
|
color.Green("Expires At: %s (%v remaining)\n\n", token.ExpiresAt, getRemainingTime(token.ExpiresAt))
|
|
if token.Revoked {
|
|
color.Red("Token Revoked: %v\n", token.Revoked)
|
|
}
|
|
}
|
|
|
|
func printMetadata(metadata MetadataJSON) {
|
|
color.Green("[i] GitLab Instance Metadata\n")
|
|
color.Green("Version: %s\n", metadata.Version)
|
|
color.Green("Enterprise: %v\n\n", metadata.Enterprise)
|
|
}
|
|
|
|
func printTokenPermissions(token AccessTokenJSON) {
|
|
color.Green("[i] Token Permissions\n")
|
|
t := table.NewWriter()
|
|
t.SetOutputMirror(os.Stdout)
|
|
t.AppendHeader(table.Row{"Scope", "Access" /* Add more column headers if needed */})
|
|
for _, scope := range token.Scopes {
|
|
t.AppendRow([]any{color.GreenString(scope), color.GreenString(gitlab_scopes[scope])})
|
|
}
|
|
t.SetColumnConfigs([]table.ColumnConfig{
|
|
{Number: 2, WidthMax: 100}, // Limit the width of the third column (Description) to 20 characters
|
|
})
|
|
t.Render()
|
|
}
|
|
|
|
func printProjects(projects []ProjectsJSON) {
|
|
color.Green("\n[i] Accessible Projects\n")
|
|
t := table.NewWriter()
|
|
t.SetOutputMirror(os.Stdout)
|
|
t.AppendHeader(table.Row{"Project", "Access Level" /* Add more column headers if needed */})
|
|
for _, project := range projects {
|
|
access := access_level_map[project.Permissions.ProjectAccess.AccessLevel]
|
|
if project.Permissions.ProjectAccess.AccessLevel == 50 {
|
|
access = color.GreenString(access)
|
|
} else if project.Permissions.ProjectAccess.AccessLevel >= 30 {
|
|
access = color.YellowString(access)
|
|
} else {
|
|
access = color.RedString(access)
|
|
}
|
|
t.AppendRow([]any{color.GreenString(project.NameWithNamespace), access})
|
|
}
|
|
t.Render()
|
|
}
|