Lint / golangci-lint (push) Waiting to run
Lint / semgrep (push) Waiting to run
Release / Release (push) Waiting to run
Scan for secrets / test (push) Waiting to run
Snifftest / Run Snifftest (push) Waiting to run
Test / test (push) Waiting to run
Test / test-community (push) Waiting to run
Only used for tracking analyzer types
302 lines
6.9 KiB
Go
302 lines
6.9 KiB
Go
//go:generate generate_permissions permissions.yaml permissions.go opsgenie
|
|
|
|
package opsgenie
|
|
|
|
import (
|
|
"bytes"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
|
|
"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/context"
|
|
)
|
|
|
|
var _ analyzers.Analyzer = (*Analyzer)(nil)
|
|
|
|
type Analyzer struct {
|
|
Cfg *config.Config
|
|
}
|
|
|
|
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeOpsgenie }
|
|
|
|
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: analyzers.AnalyzerTypeOpsgenie,
|
|
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
|
|
|
|
type User struct {
|
|
FullName string `json:"fullName"`
|
|
Username string `json:"username"`
|
|
Role struct {
|
|
Name string `json:"name"`
|
|
} `json:"role"`
|
|
}
|
|
|
|
type UsersJSON struct {
|
|
Users []User `json:"data"`
|
|
}
|
|
|
|
type HttpStatusTest struct {
|
|
Endpoint string `json:"endpoint"`
|
|
Method string `json:"method"`
|
|
Payload interface{} `json:"payload"`
|
|
ValidStatuses []int `json:"valid_status_code"`
|
|
InvalidStatuses []int `json:"invalid_status_code"`
|
|
}
|
|
|
|
func StatusContains(status int, vals []int) bool {
|
|
for _, v := range vals {
|
|
if status == v {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (h *HttpStatusTest) RunTest(cfg *config.Config, headers map[string]string) (bool, error) {
|
|
// If body data, marshal to JSON
|
|
var data io.Reader
|
|
if h.Payload != nil {
|
|
jsonData, err := json.Marshal(h.Payload)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
data = bytes.NewBuffer(jsonData)
|
|
}
|
|
|
|
// Create new HTTP request
|
|
client := analyzers.NewAnalyzeClient(cfg)
|
|
req, err := http.NewRequest(h.Method, h.Endpoint, data)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Add custom headers if provided
|
|
for key, value := range headers {
|
|
req.Header.Set(key, value)
|
|
}
|
|
|
|
// Execute HTTP Request
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Check response status code
|
|
switch {
|
|
case StatusContains(resp.StatusCode, h.ValidStatuses):
|
|
return true, nil
|
|
case StatusContains(resp.StatusCode, h.InvalidStatuses):
|
|
return false, nil
|
|
default:
|
|
return false, errors.New("error checking response status code")
|
|
}
|
|
}
|
|
|
|
type Scope struct {
|
|
Name string `json:"name"`
|
|
HttpTest HttpStatusTest `json:"test"`
|
|
}
|
|
|
|
func readInScopes() ([]Scope, error) {
|
|
var scopes []Scope
|
|
if err := json.Unmarshal(scopesConfig, &scopes); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return scopes, nil
|
|
}
|
|
|
|
func checkPermissions(cfg *config.Config, key string) ([]string, error) {
|
|
scopes, err := readInScopes()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading in scopes: %w", err)
|
|
}
|
|
|
|
permissions := make([]string, 0)
|
|
for _, scope := range scopes {
|
|
status, err := scope.HttpTest.RunTest(cfg, map[string]string{"Authorization": "GenieKey " + key})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("running test: %w", err)
|
|
}
|
|
if status {
|
|
permissions = append(permissions, scope.Name)
|
|
}
|
|
}
|
|
|
|
return permissions, nil
|
|
}
|
|
|
|
func contains(s []string, e string) bool {
|
|
for _, a := range s {
|
|
if a == e {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func getUserList(cfg *config.Config, key string) ([]User, error) {
|
|
// Create new HTTP request
|
|
client := analyzers.NewAnalyzeClient(cfg)
|
|
req, err := http.NewRequest("GET", "https://api.opsgenie.com/v2/users", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Add custom headers if provided
|
|
req.Header.Set("Authorization", "GenieKey "+key)
|
|
|
|
// Execute HTTP Request
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Decode response body
|
|
var userList UsersJSON
|
|
err = json.NewDecoder(resp.Body).Decode(&userList)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return userList.Users, nil
|
|
}
|
|
|
|
type SecretInfo struct {
|
|
Users []User
|
|
Permissions []string
|
|
}
|
|
|
|
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
|
|
info, err := AnalyzePermissions(cfg, key)
|
|
if err != nil {
|
|
color.Red("[x] Error : %s", err.Error())
|
|
return
|
|
}
|
|
|
|
color.Green("[!] Valid OpsGenie API key\n\n")
|
|
printPermissions(info.Permissions)
|
|
if len(info.Users) > 0 {
|
|
printUsers(info.Users)
|
|
}
|
|
color.Yellow("\n[i] Expires: Never")
|
|
|
|
}
|
|
|
|
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
|
|
var info = &SecretInfo{}
|
|
|
|
permissions, err := checkPermissions(cfg, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(permissions) == 0 {
|
|
return nil, fmt.Errorf("invalid OpsGenie API key")
|
|
}
|
|
|
|
info.Permissions = permissions
|
|
|
|
if contains(permissions, "configuration_access") {
|
|
users, err := getUserList(cfg, key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting user list: %w", err)
|
|
}
|
|
info.Users = users
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
func printPermissions(permissions []string) {
|
|
color.Yellow("[i] Permissions:")
|
|
t := table.NewWriter()
|
|
t.SetOutputMirror(os.Stdout)
|
|
t.AppendHeader(table.Row{"Permission"})
|
|
for _, permission := range permissions {
|
|
t.AppendRow(table.Row{color.GreenString(permission)})
|
|
}
|
|
t.Render()
|
|
}
|
|
|
|
func printUsers(users []User) {
|
|
color.Green("\n[i] Users:")
|
|
t := table.NewWriter()
|
|
t.SetOutputMirror(os.Stdout)
|
|
t.AppendHeader(table.Row{"Name", "Username", "Role"})
|
|
for _, user := range users {
|
|
t.AppendRow(table.Row{color.GreenString(user.FullName), color.GreenString(user.Username), color.GreenString(user.Role.Name)})
|
|
}
|
|
t.Render()
|
|
}
|