mirror of
https://github.com/trufflesecurity/trufflehog.git
synced 2024-11-14 17:07:31 +00:00
[analyze] Add Analyzer interface for Gitlab (#3232)
* implement analyzer interface for gitlab * generated permissions, added unit test for gitlab analyzer * revert deletion of scopes.go * appending domain in resource names * [chore] moved expected output of test in json file to neat the code. * updated the test for gitlab analyzer to make more unique FullyQualifiedName, Ids are added for resources. * remove unnecessary metadata field and fix github -> gitlab * extract user id from access token json, make user as resource * link analyzer with gitlab v2 detector * fixed code breaking changes due to analyzer protobuf removal. --------- Co-authored-by: Abdul Basit <abasit@folio3.com>
This commit is contained in:
parent
f4670aaab1
commit
9b2cef55c0
11 changed files with 303 additions and 3 deletions
1
pkg/analyzer/analyzers/gitlab/expected_output.json
Normal file
1
pkg/analyzer/analyzers/gitlab/expected_output.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"AnalyzerType":5,"Bindings":[{"Resource":{"Name":"gitlab.com/user/22466472","FullyQualifiedName":"gitlab.com/user/22466472","Type":"user","Metadata":{"token_created_at":"2024-08-15T06:33:00.337Z","token_expires_at":"2025-08-15","token_id":10470457,"token_name":"test-project-token","token_revoked":false},"Parent":null},"Permission":{"Value":"read_api","Parent":null}},{"Resource":{"Name":"gitlab.com/user/22466472","FullyQualifiedName":"gitlab.com/user/22466472","Type":"user","Metadata":{"token_created_at":"2024-08-15T06:33:00.337Z","token_expires_at":"2025-08-15","token_id":10470457,"token_name":"test-project-token","token_revoked":false},"Parent":null},"Permission":{"Value":"read_repository","Parent":null}},{"Resource":{"Name":"truffletester / trufflehog","FullyQualifiedName":"gitlab.com/project/60871295","Type":"project","Metadata":null,"Parent":null},"Permission":{"Value":"Developer","Parent":null}}],"UnboundedResources":null,"Metadata":{"enterprise":true,"version":"17.6.0-pre"}}
|
|
@ -1,8 +1,11 @@
|
|||
//go:generate generate_permissions permissions.yaml permissions.go gitlab
|
||||
|
||||
package gitlab
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
@ -25,27 +28,93 @@ type Analyzer struct {
|
|||
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeGitLab }
|
||||
|
||||
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
|
||||
_, err := AnalyzePermissions(a.Cfg, credInfo["key"])
|
||||
key, ok := credInfo["key"]
|
||||
if !ok {
|
||||
return nil, errors.New("key not found in credentialInfo")
|
||||
}
|
||||
|
||||
info, err := AnalyzePermissions(a.Cfg, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
return secretInfoToAnalyzerResult(info), nil
|
||||
}
|
||||
|
||||
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
|
||||
result := analyzers.AnalyzerResult{
|
||||
AnalyzerType: analyzers.AnalyzerTypeGitLab,
|
||||
Metadata: map[string]any{
|
||||
"version": info.Metadata.Version,
|
||||
"enterprise": info.Metadata.Enterprise,
|
||||
},
|
||||
Bindings: []analyzers.Binding{},
|
||||
}
|
||||
|
||||
// Add user and it's permissions to bindings
|
||||
userFullyQualifiedName := fmt.Sprintf("gitlab.com/user/%d", info.AccessToken.UserID)
|
||||
userResource := analyzers.Resource{
|
||||
Name: userFullyQualifiedName,
|
||||
FullyQualifiedName: userFullyQualifiedName,
|
||||
Type: "user",
|
||||
Metadata: map[string]any{
|
||||
"token_name": info.AccessToken.Name,
|
||||
"token_id": info.AccessToken.ID,
|
||||
"token_created_at": info.AccessToken.CreatedAt,
|
||||
"token_revoked": info.AccessToken.Revoked,
|
||||
"token_expires_at": info.AccessToken.ExpiresAt,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scope := range info.AccessToken.Scopes {
|
||||
result.Bindings = append(result.Bindings, analyzers.Binding{
|
||||
Resource: userResource,
|
||||
Permission: analyzers.Permission{
|
||||
Value: scope,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// append project and it's permissions to bindings
|
||||
for _, project := range info.Projects {
|
||||
projectResource := analyzers.Resource{
|
||||
Name: project.NameWithNamespace,
|
||||
FullyQualifiedName: fmt.Sprintf("gitlab.com/project/%d", project.ID),
|
||||
Type: "project",
|
||||
}
|
||||
|
||||
accessLevel, ok := access_level_map[project.Permissions.ProjectAccess.AccessLevel]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
result.Bindings = append(result.Bindings, analyzers.Binding{
|
||||
Resource: projectResource,
|
||||
Permission: analyzers.Permission{
|
||||
Value: accessLevel,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
// 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.
|
||||
// we'll call /api/v4/personal_access_tokens and then filter down to scopes.
|
||||
|
||||
type AccessTokenJSON struct {
|
||||
ID int `json:"id"`
|
||||
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"`
|
||||
UserID int `json:"user_id"`
|
||||
}
|
||||
|
||||
type ProjectsJSON struct {
|
||||
ID int `json:"id"`
|
||||
NameWithNamespace string `json:"name_with_namespace"`
|
||||
Permissions struct {
|
||||
ProjectAccess struct {
|
||||
|
@ -248,6 +317,7 @@ func printTokenInfo(token AccessTokenJSON) {
|
|||
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("User ID: %d\n", token.UserID)
|
||||
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)
|
||||
|
|
78
pkg/analyzer/analyzers/gitlab/gitlab_test.go
Normal file
78
pkg/analyzer/analyzers/gitlab/gitlab_test.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package gitlab
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||
)
|
||||
|
||||
//go:embed expected_output.json
|
||||
var expectedOutput []byte
|
||||
|
||||
func TestAnalyzer_Analyze(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
|
||||
if err != nil {
|
||||
t.Fatalf("could not get test secrets from GCP: %s", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
want string // JSON string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid gitlab access token",
|
||||
key: testSecrets.MustGetField("GITLABV2"),
|
||||
want: string(expectedOutput),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := Analyzer{}
|
||||
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Marshal the actual result to JSON
|
||||
gotJSON, err := json.Marshal(got)
|
||||
if err != nil {
|
||||
t.Fatalf("could not marshal got to JSON: %s", err)
|
||||
}
|
||||
|
||||
// Parse the expected JSON string
|
||||
var wantObj analyzers.AnalyzerResult
|
||||
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
|
||||
t.Fatalf("could not unmarshal want JSON string: %s", err)
|
||||
}
|
||||
|
||||
// Marshal the expected result to JSON (to normalize)
|
||||
wantJSON, err := json.Marshal(wantObj)
|
||||
if err != nil {
|
||||
t.Fatalf("could not marshal want to JSON: %s", err)
|
||||
}
|
||||
|
||||
// Compare the JSON strings
|
||||
if string(gotJSON) != string(wantJSON) {
|
||||
// Pretty-print both JSON strings for easier comparison
|
||||
var gotIndented []byte
|
||||
gotIndented, err = json.MarshalIndent(got, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("could not marshal got to indented JSON: %s", err)
|
||||
}
|
||||
t.Errorf("Analyzer.Analyze() = \n%s", gotIndented)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
126
pkg/analyzer/analyzers/gitlab/permissions.go
Normal file
126
pkg/analyzer/analyzers/gitlab/permissions.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
// Code generated by go generate; DO NOT EDIT.
|
||||
package gitlab
|
||||
|
||||
import "errors"
|
||||
|
||||
type Permission int
|
||||
|
||||
const (
|
||||
Invalid Permission = iota
|
||||
Api Permission = iota
|
||||
ReadUser Permission = iota
|
||||
ReadApi Permission = iota
|
||||
ReadRepository Permission = iota
|
||||
WriteRepository Permission = iota
|
||||
ReadRegistry Permission = iota
|
||||
WriteRegistry Permission = iota
|
||||
Sudo Permission = iota
|
||||
AdminMode Permission = iota
|
||||
CreateRunner Permission = iota
|
||||
ManageRunner Permission = iota
|
||||
AiFeatures Permission = iota
|
||||
K8sProxy Permission = iota
|
||||
ReadServicePing Permission = iota
|
||||
)
|
||||
|
||||
var (
|
||||
PermissionStrings = map[Permission]string{
|
||||
Api: "api",
|
||||
ReadUser: "read_user",
|
||||
ReadApi: "read_api",
|
||||
ReadRepository: "read_repository",
|
||||
WriteRepository: "write_repository",
|
||||
ReadRegistry: "read_registry",
|
||||
WriteRegistry: "write_registry",
|
||||
Sudo: "sudo",
|
||||
AdminMode: "admin_mode",
|
||||
CreateRunner: "create_runner",
|
||||
ManageRunner: "manage_runner",
|
||||
AiFeatures: "ai_features",
|
||||
K8sProxy: "k8s_proxy",
|
||||
ReadServicePing: "read_service_ping",
|
||||
}
|
||||
|
||||
StringToPermission = map[string]Permission{
|
||||
"api": Api,
|
||||
"read_user": ReadUser,
|
||||
"read_api": ReadApi,
|
||||
"read_repository": ReadRepository,
|
||||
"write_repository": WriteRepository,
|
||||
"read_registry": ReadRegistry,
|
||||
"write_registry": WriteRegistry,
|
||||
"sudo": Sudo,
|
||||
"admin_mode": AdminMode,
|
||||
"create_runner": CreateRunner,
|
||||
"manage_runner": ManageRunner,
|
||||
"ai_features": AiFeatures,
|
||||
"k8s_proxy": K8sProxy,
|
||||
"read_service_ping": ReadServicePing,
|
||||
}
|
||||
|
||||
PermissionIDs = map[Permission]int{
|
||||
Api: 1,
|
||||
ReadUser: 2,
|
||||
ReadApi: 3,
|
||||
ReadRepository: 4,
|
||||
WriteRepository: 5,
|
||||
ReadRegistry: 6,
|
||||
WriteRegistry: 7,
|
||||
Sudo: 8,
|
||||
AdminMode: 9,
|
||||
CreateRunner: 10,
|
||||
ManageRunner: 11,
|
||||
AiFeatures: 12,
|
||||
K8sProxy: 13,
|
||||
ReadServicePing: 14,
|
||||
}
|
||||
|
||||
IdToPermission = map[int]Permission{
|
||||
1: Api,
|
||||
2: ReadUser,
|
||||
3: ReadApi,
|
||||
4: ReadRepository,
|
||||
5: WriteRepository,
|
||||
6: ReadRegistry,
|
||||
7: WriteRegistry,
|
||||
8: Sudo,
|
||||
9: AdminMode,
|
||||
10: CreateRunner,
|
||||
11: ManageRunner,
|
||||
12: AiFeatures,
|
||||
13: K8sProxy,
|
||||
14: ReadServicePing,
|
||||
}
|
||||
)
|
||||
|
||||
// ToString converts a Permission enum to its string representation
|
||||
func (p Permission) ToString() (string, error) {
|
||||
if str, ok := PermissionStrings[p]; ok {
|
||||
return str, nil
|
||||
}
|
||||
return "", errors.New("invalid permission")
|
||||
}
|
||||
|
||||
// ToID converts a Permission enum to its ID
|
||||
func (p Permission) ToID() (int, error) {
|
||||
if id, ok := PermissionIDs[p]; ok {
|
||||
return id, nil
|
||||
}
|
||||
return 0, errors.New("invalid permission")
|
||||
}
|
||||
|
||||
// PermissionFromString converts a string representation to its Permission enum
|
||||
func PermissionFromString(s string) (Permission, error) {
|
||||
if p, ok := StringToPermission[s]; ok {
|
||||
return p, nil
|
||||
}
|
||||
return 0, errors.New("invalid permission string")
|
||||
}
|
||||
|
||||
// PermissionFromID converts an ID to its Permission enum
|
||||
func PermissionFromID(id int) (Permission, error) {
|
||||
if p, ok := IdToPermission[id]; ok {
|
||||
return p, nil
|
||||
}
|
||||
return 0, errors.New("invalid permission ID")
|
||||
}
|
15
pkg/analyzer/analyzers/gitlab/permissions.yaml
Normal file
15
pkg/analyzer/analyzers/gitlab/permissions.yaml
Normal file
|
@ -0,0 +1,15 @@
|
|||
permissions:
|
||||
- api
|
||||
- read_user
|
||||
- read_api
|
||||
- read_repository
|
||||
- write_repository
|
||||
- read_registry
|
||||
- write_registry
|
||||
- sudo
|
||||
- admin_mode
|
||||
- create_runner
|
||||
- manage_runner
|
||||
- ai_features
|
||||
- k8s_proxy
|
||||
- read_service_ping
|
|
@ -75,6 +75,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||
s1.ExtraData = extraData
|
||||
|
||||
s1.SetVerificationError(verificationErr, resMatch)
|
||||
s1.AnalysisInfo = map[string]string{
|
||||
"key": resMatch,
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, s1)
|
||||
|
|
|
@ -187,6 +187,7 @@ func TestGitlab_FromChunk(t *testing.T) {
|
|||
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
|
||||
t.Fatalf(" wantVerificationError = %v, verification error = %v,", tt.wantVerificationErr, got[i].VerificationError())
|
||||
}
|
||||
got[i].AnalysisInfo = nil
|
||||
}
|
||||
opts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
|
||||
if diff := cmp.Diff(got, tt.want, opts); diff != "" {
|
||||
|
|
|
@ -87,6 +87,7 @@ func TestGitlab_FromChunk_WithV2Secrets(t *testing.T) {
|
|||
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
|
||||
t.Fatalf(" wantVerificationError = %v, verification error = %v,", tt.wantVerificationErr, got[i].VerificationError())
|
||||
}
|
||||
got[i].AnalysisInfo = nil
|
||||
}
|
||||
opts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
|
||||
if diff := cmp.Diff(got, tt.want, opts); diff != "" {
|
||||
|
|
|
@ -98,6 +98,7 @@ func TestGitlabV2_FromChunk_WithV1Secrets(t *testing.T) {
|
|||
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
|
||||
t.Fatalf(" wantVerificationError = %v, verification error = %v,", tt.wantVerificationErr, got[i].VerificationError())
|
||||
}
|
||||
got[i].AnalysisInfo = nil
|
||||
}
|
||||
opts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
|
||||
if diff := cmp.Diff(got, tt.want, opts); diff != "" {
|
||||
|
|
|
@ -64,6 +64,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||
s1.ExtraData = extraData
|
||||
|
||||
s1.SetVerificationError(verificationErr, resMatch)
|
||||
s1.AnalysisInfo = map[string]string{
|
||||
"key": resMatch,
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, s1)
|
||||
|
|
|
@ -167,6 +167,7 @@ func TestGitlabV2_FromChunk(t *testing.T) {
|
|||
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
|
||||
t.Fatalf(" wantVerificationError = %v, verification error = %v,", tt.wantVerificationErr, got[i].VerificationError())
|
||||
}
|
||||
got[i].AnalysisInfo = nil
|
||||
}
|
||||
opts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
|
||||
if diff := cmp.Diff(got, tt.want, opts); diff != "" {
|
||||
|
|
Loading…
Reference in a new issue