Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[analyze] Add Analyzer interface for Gitlab #3232

Merged
merged 14 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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":"test-project-token","FullyQualifiedName":"github.com/token/10470457","Type":"access_token","Metadata":{"created_at":"2024-08-15T06:33:00.337Z","expires_at":"2025-08-15","last_used_at":"2024-09-09T16:41:06.941Z","revoked":false},"Parent":null},"Permission":{"Value":"read_api","Parent":null}},{"Resource":{"Name":"test-project-token","FullyQualifiedName":"github.com/token/10470457","Type":"access_token","Metadata":{"created_at":"2024-08-15T06:33:00.337Z","expires_at":"2025-08-15","last_used_at":"2024-09-09T16:41:06.941Z","revoked":false},"Parent":null},"Permission":{"Value":"read_repository","Parent":null}},{"Resource":{"Name":"truffletester / trufflehog","FullyQualifiedName":"github.com/project/60871295","Type":"project","Metadata":null,"Parent":null},"Permission":{"Value":"Developer","Parent":null}}],"UnboundedResources":null,"Metadata":{"enterprise":true,"version":"17.4.0-pre"}}
70 changes: 68 additions & 2 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 @@ -26,18 +29,80 @@ type Analyzer struct {
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"])
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: analyzerpb.AnalyzerType_GitLab,
Metadata: map[string]any{
"version": info.Metadata.Version,
"enterprise": info.Metadata.Enterprise,
},
Bindings: []analyzers.Binding{},
}

// Add token and it's permissions to bindings
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure token should be a resource here? Does the credential we're analyzing grant permission to perform actions on the token?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, The permission determine what action can be perform using that token.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Permissions operate on a resource though, so in this case the permissions would describe operations that could be performed on the token itself, which seems wrong to me..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mcastorina I had gone through the login. PAT token does belongs to user and we do get userID in response. So, resource can be a User with those permissions. What do you think ?

tokenResource := analyzers.Resource{
Name: info.AccessToken.Name,
FullyQualifiedName: fmt.Sprintf("github.com/token/%d", info.AccessToken.ID),
mcastorina marked this conversation as resolved.
Show resolved Hide resolved
Type: "access_token",
Metadata: map[string]any{
"last_used_at": info.AccessToken.LastUsedAt,
mcastorina marked this conversation as resolved.
Show resolved Hide resolved
"created_at": info.AccessToken.CreatedAt,
"revoked": info.AccessToken.Revoked,
"expires_at": info.AccessToken.ExpiresAt,
},
}

for _, scope := range info.AccessToken.Scopes {
result.Bindings = append(result.Bindings, analyzers.Binding{
Resource: tokenResource,
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("github.com/project/%d", project.ID),
mcastorina marked this conversation as resolved.
Show resolved Hide resolved
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.

type AccessTokenJSON struct {
ID int `json:"id"`
Name string `json:"name"`
Revoked bool `json:"revoked"`
CreatedAt string `json:"created_at"`
Expand All @@ -47,6 +112,7 @@ type AccessTokenJSON struct {
}

type ProjectsJSON struct {
ID int `json:"id"`
NameWithNamespace string `json:"name_with_namespace"`
Permissions struct {
ProjectAccess struct {
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 @@ -69,6 +69,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
isVerified, verificationErr := s.verifyGitlab(ctx, resMatch)
s1.Verified = isVerified
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
Loading