[analyze] Add Analyzer for Opsgenie (#3181)

* implement analyzer interface for opsgenie and add unit tests

* Add analyzer interface for opsgenie

linked detector  with analyzers
fixed test cases.

* generate permissions for opsgenie and change scope names to lowercase for consistency

* fixed unboundedresources slice issue.
username as fullqualifiedname

---------

Co-authored-by: Abdul Basit <abasit@folio3.com>
This commit is contained in:
Abdul Basit 2024-09-05 03:50:19 +05:00 committed by GitHub
parent 5ce1578a6f
commit d6e1627f16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 321 additions and 6 deletions

View file

@ -1,3 +1,5 @@
//go:generate generate_permissions permissions.yaml permissions.go opsgenie
package opsgenie
import (
@ -14,8 +16,79 @@ import (
"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_Opsgenie }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, errors.New("missing key in credInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzerpb.AnalyzerType_Opsgenie,
Metadata: nil,
Bindings: make([]analyzers.Binding, len(info.Permissions)),
UnboundedResources: make([]analyzers.Resource, len(info.Users)),
}
// Opsgenie has API integrations, so the key does not belong
// to a particular user or account, it itself is a resource
resource := analyzers.Resource{
Name: "Opsgenie API Integration Key",
FullyQualifiedName: "Opsgenie API Integration Key",
Type: "API Key",
Metadata: map[string]any{
"expires": "never",
},
}
for idx, permission := range info.Permissions {
result.Bindings[idx] = analyzers.Binding{
Resource: resource,
Permission: analyzers.Permission{
Value: permission,
},
}
}
// We can find list of users in the current account
// if the API key has Configuration Access, so these can be
// unbounded resources
for idx, user := range info.Users {
result.UnboundedResources[idx] = analyzers.Resource{
Name: user.FullName,
FullyQualifiedName: user.Username,
Type: "user",
Metadata: map[string]any{
"username": user.Username,
"role": user.Role.Name,
},
}
}
return &result
}
//go:embed scopes.json
var scopesConfig []byte
@ -195,7 +268,7 @@ func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
info.Permissions = permissions
if contains(permissions, "Configuration Access") {
if contains(permissions, "configuration_access") {
users, err := getUserList(cfg, key)
if err != nil {
return nil, fmt.Errorf("getting user list: %w", err)

View file

@ -0,0 +1,158 @@
package opsgenie
import (
"encoding/json"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
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)
}
key := testSecrets.MustGetField("OPSGENIE")
tests := []struct {
name string
key string
want string // JSON string
wantErr bool
}{
{
name: "valid Opsgenie API key",
key: key,
want: `{
"AnalyzerType": 11,
"Bindings": [
{
"Resource": {
"Name": "Opsgenie API Integration Key",
"FullyQualifiedName": "Opsgenie API Integration Key",
"Type": "API Key",
"Metadata": {
"expires": "never"
},
"Parent": null
},
"Permission": {
"Value": "configuration_access",
"Parent": null
}
},
{
"Resource": {
"Name": "Opsgenie API Integration Key",
"FullyQualifiedName": "Opsgenie API Integration Key",
"Type": "API Key",
"Metadata": {
"expires": "never"
},
"Parent": null
},
"Permission": {
"Value": "read",
"Parent": null
}
},
{
"Resource": {
"Name": "Opsgenie API Integration Key",
"FullyQualifiedName": "Opsgenie API Integration Key",
"Type": "API Key",
"Metadata": {
"expires": "never"
},
"Parent": null
},
"Permission": {
"Value": "delete",
"Parent": null
}
},
{
"Resource": {
"Name": "Opsgenie API Integration Key",
"FullyQualifiedName": "Opsgenie API Integration Key",
"Type": "API Key",
"Metadata": {
"expires": "never"
},
"Parent": null
},
"Permission": {
"Value": "create_and_update",
"Parent": null
}
}
],
"UnboundedResources": [
{
"Name": "John Scanner",
"FullyQualifiedName": "secretscanner02@zohomail.com",
"Type": "user",
"Metadata": {
"role": "Owner",
"username": "secretscanner02@zohomail.com"
},
"Parent": null
}
],
"Metadata": null
}`,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
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, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}

View file

@ -0,0 +1,76 @@
// Code generated by go generate; DO NOT EDIT.
package opsgenie
import "errors"
type Permission int
const (
Invalid Permission = iota
ConfigurationAccess Permission = iota
Read Permission = iota
Delete Permission = iota
CreateAndUpdate Permission = iota
)
var (
PermissionStrings = map[Permission]string{
ConfigurationAccess: "configuration_access",
Read: "read",
Delete: "delete",
CreateAndUpdate: "create_and_update",
}
StringToPermission = map[string]Permission{
"configuration_access": ConfigurationAccess,
"read": Read,
"delete": Delete,
"create_and_update": CreateAndUpdate,
}
PermissionIDs = map[Permission]int{
ConfigurationAccess: 1,
Read: 2,
Delete: 3,
CreateAndUpdate: 4,
}
IdToPermission = map[int]Permission{
1: ConfigurationAccess,
2: Read,
3: Delete,
4: CreateAndUpdate,
}
)
// 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")
}

View file

@ -0,0 +1,5 @@
permissions:
- configuration_access
- read
- delete
- create_and_update

View file

@ -1,6 +1,6 @@
[
{
"name": "Configuration Access",
"name": "configuration_access",
"test": {
"endpoint": "https://api.opsgenie.com/v2/account",
"method": "GET",
@ -9,7 +9,7 @@
}
},
{
"name": "Read",
"name": "read",
"test": {
"endpoint": "https://api.opsgenie.com/v2/alerts",
"method": "GET",
@ -18,7 +18,7 @@
}
},
{
"name": "Delete",
"name": "delete",
"test": {
"endpoint": "https://api.opsgenie.com/v2/alerts/`nowaythiscanexist",
"method": "DELETE",
@ -27,7 +27,7 @@
}
},
{
"name": "Create and Update",
"name": "create_and_update",
"test": {
"endpoint": "https://api.opsgenie.com/v2/alerts/`nowaycanthisexist/message",
"method": "PUT",

View file

@ -87,7 +87,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
}
} else {
s1.Verified = false
}
s1.AnalysisInfo = map[string]string{
"key": resMatch,
}
}

View file

@ -92,6 +92,7 @@ func TestOpsgenie_FromChunk(t *testing.T) {
t.Fatalf("no raw secret present: \n %+v", got[i])
}
got[i].Raw = nil
got[i].AnalysisInfo = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Opsgenie.FromData() %s diff: (-got +want)\n%s", tt.name, diff)