Skip to content

Commit

Permalink
[analyze] Add Analyzer interface for Gitlab (#3232)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
abmussani and abasit-folio3 authored Oct 30, 2024
1 parent f4670aa commit 9b2cef5
Show file tree
Hide file tree
Showing 11 changed files with 303 additions and 3 deletions.
1 change: 1 addition & 0 deletions pkg/analyzer/analyzers/gitlab/expected_output.json
Original file line number Diff line number Diff line change
@@ -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"}}
76 changes: 73 additions & 3 deletions pkg/analyzer/analyzers/gitlab/gitlab.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
//go:generate generate_permissions permissions.yaml permissions.go gitlab

package gitlab

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
78 changes: 78 additions & 0 deletions pkg/analyzer/analyzers/gitlab/gitlab_test.go
Original file line number Diff line number Diff line change
@@ -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 changes: 126 additions & 0 deletions pkg/analyzer/analyzers/gitlab/permissions.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions pkg/analyzer/analyzers/gitlab/permissions.yaml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions pkg/detectors/gitlab/v1/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions pkg/detectors/gitlab/v1/gitlab_v1_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down
1 change: 1 addition & 0 deletions pkg/detectors/gitlab/v1/gitlab_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down
1 change: 1 addition & 0 deletions pkg/detectors/gitlab/v2/gitlab_v1_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down
3 changes: 3 additions & 0 deletions pkg/detectors/gitlab/v2/gitlab_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions pkg/detectors/gitlab/v2/gitlab_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down

0 comments on commit 9b2cef5

Please sign in to comment.