[Feat] PrivateKey Analyzer (#3854)
* private key analyzer initial impl - basic structure * Impl analyzePermission method for PrivateKey analyzer. export few private functions for reusability in analyzers * add permissions.yaml and generate permissions * permissions fix * AnalyzeAndPrintPermissions impl * analyzer test pushed * Impl analyzer func called from detector. added and regenerated permissions. * filter empty strings in result and generate expected_output for test * some refactoring * comment added for better readability. * fixed code breaking changes in detectors due to exporting private functions. * log insufficient information message on no certificate results
This commit is contained in:
@@ -80,6 +80,7 @@ const (
|
||||
AnalyzerTypeSquare
|
||||
AnalyzerTypeStripe
|
||||
AnalyzerTypeTwilio
|
||||
AnalyzerTypePrivateKey
|
||||
// Add new items here with AnalyzerType prefix
|
||||
)
|
||||
|
||||
@@ -106,6 +107,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{
|
||||
AnalyzerTypeSquare: "Square",
|
||||
AnalyzerTypeStripe: "Stripe",
|
||||
AnalyzerTypeTwilio: "Twilio",
|
||||
AnalyzerTypePrivateKey: "PrivateKey",
|
||||
// Add new mappings here
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"AnalyzerType":21,"Bindings":[],"UnboundedResources":[{"Name":"*.gruponu3.com","FullyQualifiedName":"/*.gruponu3.com","Type":"certificate","Metadata":null,"Parent":null},{"Name":"techautm.in","FullyQualifiedName":"/techautm.in","Type":"certificate","Metadata":null,"Parent":null}],"Metadata":null}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
package privatekey
|
||||
|
||||
import "errors"
|
||||
|
||||
type Permission int
|
||||
|
||||
const (
|
||||
Invalid Permission = iota
|
||||
Digitalsignature Permission = iota
|
||||
Nonrepudiation Permission = iota
|
||||
Keyencipherment Permission = iota
|
||||
Dataencipherment Permission = iota
|
||||
Keyagreement Permission = iota
|
||||
Certificatesigning Permission = iota
|
||||
Crlsigning Permission = iota
|
||||
Encipheronly Permission = iota
|
||||
Decipheronly Permission = iota
|
||||
Serverauth Permission = iota
|
||||
Clientauth Permission = iota
|
||||
Codesigning Permission = iota
|
||||
Emailprotection Permission = iota
|
||||
Timestamping Permission = iota
|
||||
Ocspsigning Permission = iota
|
||||
Clone Permission = iota
|
||||
Push Permission = iota
|
||||
)
|
||||
|
||||
var (
|
||||
PermissionStrings = map[Permission]string{
|
||||
Digitalsignature: "DigitalSignature",
|
||||
Nonrepudiation: "NonRepudiation",
|
||||
Keyencipherment: "KeyEncipherment",
|
||||
Dataencipherment: "DataEncipherment",
|
||||
Keyagreement: "KeyAgreement",
|
||||
Certificatesigning: "CertificateSigning",
|
||||
Crlsigning: "CRLSigning",
|
||||
Encipheronly: "EncipherOnly",
|
||||
Decipheronly: "DecipherOnly",
|
||||
Serverauth: "ServerAuth",
|
||||
Clientauth: "ClientAuth",
|
||||
Codesigning: "CodeSigning",
|
||||
Emailprotection: "EmailProtection",
|
||||
Timestamping: "TimeStamping",
|
||||
Ocspsigning: "OCSPSigning",
|
||||
Clone: "Clone",
|
||||
Push: "Push",
|
||||
}
|
||||
|
||||
StringToPermission = map[string]Permission{
|
||||
"DigitalSignature": Digitalsignature,
|
||||
"NonRepudiation": Nonrepudiation,
|
||||
"KeyEncipherment": Keyencipherment,
|
||||
"DataEncipherment": Dataencipherment,
|
||||
"KeyAgreement": Keyagreement,
|
||||
"CertificateSigning": Certificatesigning,
|
||||
"CRLSigning": Crlsigning,
|
||||
"EncipherOnly": Encipheronly,
|
||||
"DecipherOnly": Decipheronly,
|
||||
"ServerAuth": Serverauth,
|
||||
"ClientAuth": Clientauth,
|
||||
"CodeSigning": Codesigning,
|
||||
"EmailProtection": Emailprotection,
|
||||
"TimeStamping": Timestamping,
|
||||
"OCSPSigning": Ocspsigning,
|
||||
"Clone": Clone,
|
||||
"Push": Push,
|
||||
}
|
||||
|
||||
PermissionIDs = map[Permission]int{
|
||||
Digitalsignature: 1,
|
||||
Nonrepudiation: 2,
|
||||
Keyencipherment: 3,
|
||||
Dataencipherment: 4,
|
||||
Keyagreement: 5,
|
||||
Certificatesigning: 6,
|
||||
Crlsigning: 7,
|
||||
Encipheronly: 8,
|
||||
Decipheronly: 9,
|
||||
Serverauth: 10,
|
||||
Clientauth: 11,
|
||||
Codesigning: 12,
|
||||
Emailprotection: 13,
|
||||
Timestamping: 14,
|
||||
Ocspsigning: 15,
|
||||
Clone: 16,
|
||||
Push: 17,
|
||||
}
|
||||
|
||||
IdToPermission = map[int]Permission{
|
||||
1: Digitalsignature,
|
||||
2: Nonrepudiation,
|
||||
3: Keyencipherment,
|
||||
4: Dataencipherment,
|
||||
5: Keyagreement,
|
||||
6: Certificatesigning,
|
||||
7: Crlsigning,
|
||||
8: Encipheronly,
|
||||
9: Decipheronly,
|
||||
10: Serverauth,
|
||||
11: Clientauth,
|
||||
12: Codesigning,
|
||||
13: Emailprotection,
|
||||
14: Timestamping,
|
||||
15: Ocspsigning,
|
||||
16: Clone,
|
||||
17: Push,
|
||||
}
|
||||
)
|
||||
|
||||
// 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")
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
permissions:
|
||||
# TLS:
|
||||
# KeyUsuage: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3
|
||||
# ExtendedKeyUsage: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.12
|
||||
- DigitalSignature
|
||||
- NonRepudiation
|
||||
- KeyEncipherment
|
||||
- DataEncipherment
|
||||
- KeyAgreement
|
||||
- CertificateSigning
|
||||
- CRLSigning
|
||||
- EncipherOnly
|
||||
- DecipherOnly
|
||||
- ServerAuth
|
||||
- ClientAuth
|
||||
- CodeSigning
|
||||
- EmailProtection
|
||||
- TimeStamping
|
||||
- OCSPSigning
|
||||
|
||||
# Github/Gitlab
|
||||
- Clone
|
||||
- Push
|
||||
@@ -0,0 +1,303 @@
|
||||
//go:generate generate_permissions permissions.yaml permissions.go privatekey
|
||||
|
||||
package privatekey
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/privatekey"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var _ analyzers.Analyzer = (*Analyzer)(nil)
|
||||
|
||||
type Analyzer struct {
|
||||
Cfg *config.Config
|
||||
}
|
||||
|
||||
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypePrivateKey }
|
||||
|
||||
func (a Analyzer) Analyze(ctx context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
|
||||
// token will be already normalized by the time it reaches here
|
||||
token, ok := credInfo["token"]
|
||||
if !ok {
|
||||
return nil, errors.New("token not found in credInfo")
|
||||
}
|
||||
|
||||
info, err := AnalyzePermissions(ctx, a.Cfg, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return secretInfoToAnalyzerResult(info), nil
|
||||
}
|
||||
|
||||
type SecretInfo struct {
|
||||
TLSCertificateResult *privatekey.DriftwoodResult
|
||||
GithubUsername *string
|
||||
GitlabUsername *string
|
||||
}
|
||||
|
||||
func AnalyzePermissions(ctx context.Context, cfg *config.Config, token string) (*SecretInfo, error) {
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
parsedKey any
|
||||
err error
|
||||
analyzerErrors = privatekey.NewVerificationErrors(3)
|
||||
info = &SecretInfo{}
|
||||
)
|
||||
|
||||
parsedKey, err = ssh.ParseRawPrivateKey([]byte(token))
|
||||
if err != nil && strings.Contains(err.Error(), "private key is passphrase protected") {
|
||||
// key is password protected
|
||||
parsedKey, _, err = privatekey.Crack([]byte(token))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fingerprint, err := privatekey.FingerprintPEMKey(parsedKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Look up certificate information.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
data, err := analyzeFingerprint(ctx, fingerprint)
|
||||
if err != nil {
|
||||
analyzerErrors.Add(err)
|
||||
} else {
|
||||
info.TLSCertificateResult = data
|
||||
}
|
||||
}()
|
||||
|
||||
// Test SSH key against github.com
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
user, err := analyzeGithubUser(ctx, parsedKey)
|
||||
if err != nil {
|
||||
analyzerErrors.Add(err)
|
||||
} else if user != nil {
|
||||
info.GithubUsername = user
|
||||
}
|
||||
}()
|
||||
|
||||
// Test SSH key against gitlab.com
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
user, err := analyzeGitlabUser(ctx, parsedKey)
|
||||
if err != nil {
|
||||
analyzerErrors.Add(err)
|
||||
} else if user != nil {
|
||||
info.GitlabUsername = user
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
if len(analyzerErrors.Errors) == 3 {
|
||||
return nil, fmt.Errorf("analyzer failures: %s", strings.Join(analyzerErrors.Errors, ", "))
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
|
||||
if cfg.LoggingEnabled {
|
||||
color.Red("[x] Logging is not supported for this analyzer.")
|
||||
return
|
||||
}
|
||||
|
||||
token := privatekey.Normalize(key)
|
||||
if len(token) < 64 {
|
||||
color.Red("[x] Error: Invalid Private Key")
|
||||
return
|
||||
}
|
||||
|
||||
info, err := AnalyzePermissions(context.Background(), cfg, token)
|
||||
if err != nil {
|
||||
color.Red("[x] Error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
color.Green("[!] Valid Private Key\n\n")
|
||||
|
||||
if info.GithubUsername == nil && info.GitlabUsername == nil && info.TLSCertificateResult == nil {
|
||||
color.Yellow("[i] Insufficient information returned from fingerprint analysis. No permissions found.")
|
||||
return
|
||||
}
|
||||
|
||||
if info.GithubUsername != nil {
|
||||
color.Yellow("[i] GitHub Details:")
|
||||
printUserInfo(*info.GithubUsername)
|
||||
}
|
||||
if info.GitlabUsername != nil {
|
||||
color.Yellow("[i] GitLab Details:")
|
||||
printUserInfo(*info.GitlabUsername)
|
||||
}
|
||||
if info.TLSCertificateResult != nil {
|
||||
printTLSCertificateResult(info.TLSCertificateResult)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func printUserInfo(username string) {
|
||||
color.Yellow("[i] Username: %s", username)
|
||||
color.Yellow("[i] Permissions: %s\n\n", color.GreenString("Clone/Push"))
|
||||
}
|
||||
|
||||
func printTLSCertificateResult(result *privatekey.DriftwoodResult) {
|
||||
color.Yellow("[i] TLS Certificate Details:")
|
||||
fmt.Print("\n")
|
||||
t := table.NewWriter()
|
||||
t.SetOutputMirror(os.Stdout)
|
||||
t.AppendHeader(
|
||||
table.Row{"Subject Key ID", "Subject Name", "Subject Organization", "Permissions", "Expiration Date", "Domains"})
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
for _, certificateResult := range result.CertificateResults {
|
||||
t.AppendRow([]interface{}{
|
||||
green(certificateResult.SubjectKeyID),
|
||||
green(certificateResult.SubjectName),
|
||||
green(strings.Join(certificateResult.SubjectOrganization, ", ")),
|
||||
green(strings.Join(append(certificateResult.KeyUsages, certificateResult.ExtendedKeyUsages...), ", ")),
|
||||
green(certificateResult.ExpirationTimestamp.Format(time.RFC3339)),
|
||||
green(strings.Join(certificateResult.Domains, ", ")),
|
||||
})
|
||||
}
|
||||
t.Render()
|
||||
}
|
||||
|
||||
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := analyzers.AnalyzerResult{
|
||||
AnalyzerType: analyzers.AnalyzerTypePrivateKey,
|
||||
Metadata: nil,
|
||||
Bindings: []analyzers.Binding{},
|
||||
UnboundedResources: []analyzers.Resource{},
|
||||
}
|
||||
|
||||
if info.TLSCertificateResult != nil {
|
||||
bounded, unbounded := bakeTLSResources(info.TLSCertificateResult)
|
||||
result.Bindings = append(result.Bindings, bounded...)
|
||||
result.UnboundedResources = append(result.UnboundedResources, unbounded...)
|
||||
}
|
||||
|
||||
if info.GithubUsername != nil {
|
||||
result.Bindings = append(result.Bindings, bakeGithubResources(info.GithubUsername)...)
|
||||
}
|
||||
|
||||
if info.GitlabUsername != nil {
|
||||
result.Bindings = append(result.Bindings, bakeGitlabResources(info.GitlabUsername)...)
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
func bakeGithubResources(username *string) []analyzers.Binding {
|
||||
resource := &analyzers.Resource{
|
||||
Name: *username,
|
||||
FullyQualifiedName: fmt.Sprintf("github.com/user/%s", *username),
|
||||
Type: "user", // always user ???
|
||||
}
|
||||
|
||||
permissions := []analyzers.Permission{
|
||||
{Value: PermissionStrings[Clone], Parent: nil},
|
||||
{Value: PermissionStrings[Push], Parent: nil},
|
||||
}
|
||||
|
||||
return analyzers.BindAllPermissions(*resource, permissions...)
|
||||
}
|
||||
|
||||
func bakeGitlabResources(username *string) []analyzers.Binding {
|
||||
resource := &analyzers.Resource{
|
||||
Name: *username,
|
||||
FullyQualifiedName: fmt.Sprintf("gitlab.com/user/%s", *username),
|
||||
Type: "user", // always user ???
|
||||
}
|
||||
|
||||
permissions := []analyzers.Permission{
|
||||
{Value: PermissionStrings[Clone], Parent: nil},
|
||||
{Value: PermissionStrings[Push], Parent: nil},
|
||||
}
|
||||
|
||||
return analyzers.BindAllPermissions(*resource, permissions...)
|
||||
}
|
||||
|
||||
func bakeTLSResources(result *privatekey.DriftwoodResult) ([]analyzers.Binding, []analyzers.Resource) {
|
||||
|
||||
unboundedResources := make([]analyzers.Resource, 0, len(result.CertificateResults))
|
||||
boundedResources := make([]analyzers.Binding, 0, len(result.CertificateResults))
|
||||
|
||||
// iterate result.CertificateResults
|
||||
for _, cert := range result.CertificateResults {
|
||||
if cert.SubjectName == "" && cert.SubjectKeyID == "" {
|
||||
continue
|
||||
}
|
||||
resource := &analyzers.Resource{
|
||||
Name: cert.SubjectName,
|
||||
FullyQualifiedName: fmt.Sprintf("%s/%s", cert.SubjectKeyID, cert.SubjectName),
|
||||
Type: "certificate",
|
||||
}
|
||||
certPermissions := append(cert.KeyUsages, cert.ExtendedKeyUsages...)
|
||||
permissions := make([]analyzers.Permission, 0, len(certPermissions))
|
||||
for _, perm := range certPermissions {
|
||||
perm, ok := StringToPermission[perm]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
permissions = append(permissions, analyzers.Permission{
|
||||
Value: PermissionStrings[perm],
|
||||
Parent: nil,
|
||||
})
|
||||
}
|
||||
|
||||
if len(permissions) > 0 {
|
||||
// bind all permissions with resources
|
||||
boundedResources = append(boundedResources, analyzers.BindAllPermissions(*resource, permissions...)...)
|
||||
} else {
|
||||
unboundedResources = append(unboundedResources, *resource)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return boundedResources, unboundedResources
|
||||
}
|
||||
|
||||
func analyzeFingerprint(ctx context.Context, fingerprint string) (*privatekey.DriftwoodResult, error) {
|
||||
|
||||
result, err := privatekey.LookupFingerprint(ctx, fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(result.CertificateResults) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func analyzeGithubUser(ctx context.Context, parsedKey any) (*string, error) {
|
||||
return privatekey.VerifyGitHubUser(ctx, parsedKey)
|
||||
}
|
||||
|
||||
func analyzeGitlabUser(ctx context.Context, parsedKey any) (*string, error) {
|
||||
return privatekey.VerifyGitLabUser(ctx, parsedKey)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package privatekey
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"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"
|
||||
)
|
||||
|
||||
//go:embed expected_output.json
|
||||
var expectedOutput []byte
|
||||
|
||||
func TestAnalyzer_Analyze(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
|
||||
if err != nil {
|
||||
t.Fatalf("could not get test secrets from GCP: %s", err)
|
||||
}
|
||||
|
||||
privateKey := testSecrets.MustGetField("PRIVATEKEY_TLS")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
storeUrl string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid TLS key",
|
||||
key: privateKey,
|
||||
want: string(expectedOutput),
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/opsgenie"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/postgres"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/postman"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/privatekey"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/sendgrid"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/shopify"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/slack"
|
||||
@@ -102,5 +103,7 @@ func Run(cmd string) {
|
||||
shopify.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"], secretInfo.Parts["url"])
|
||||
case "opsgenie":
|
||||
opsgenie.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||
case "privatekey":
|
||||
privatekey.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ var (
|
||||
ErrUncrackable = errors.New("unable to crack encryption")
|
||||
)
|
||||
|
||||
func crack(in []byte) (any, string, error) {
|
||||
func Crack(in []byte) (any, string, error) {
|
||||
for _, passphrase := range passphrases {
|
||||
parsed, err := ssh.ParseRawPrivateKeyWithPassphrase(in, passphrase)
|
||||
if err != nil {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
var (
|
||||
testEncryptedKeyCorrectPassword = []byte("123456")
|
||||
testEncryptedKeyIncorrectPassword = []byte("incorrect")
|
||||
testEncryptedKey = []byte(normalize(`-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
testEncryptedKey = []byte(Normalize(`-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAjNIZuun
|
||||
xgLkM8KuzfmQuRAAAAEAAAAAEAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQDe3Al0EMPz
|
||||
utVNk5DixaYrGMK56RqUoqGBinke6SWVWmqom1lBcJWzor6HlnMRPPr7YCEsJKL4IpuVwu
|
||||
@@ -70,7 +70,7 @@ func Test_crack(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, passphrase, err := crack(tt.in)
|
||||
_, passphrase, err := Crack(tt.in)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("crack() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
@@ -97,6 +97,6 @@ func BenchmarkParseRightPassword(b *testing.B) {
|
||||
|
||||
func BenchmarkCracker(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
crack(testEncryptedKey)
|
||||
Crack(testEncryptedKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func normalize(in string) string {
|
||||
func Normalize(in string) string {
|
||||
in = strings.ReplaceAll(in, `"`, "")
|
||||
in = strings.ReplaceAll(in, `'`, "")
|
||||
in = strings.ReplaceAll(in, "\t", "")
|
||||
|
||||
@@ -50,7 +50,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
||||
|
||||
matches := keyPat.FindAllString(dataStr, -1)
|
||||
for _, match := range matches {
|
||||
token := normalize(match)
|
||||
token := Normalize(match)
|
||||
if len(token) < 64 {
|
||||
continue
|
||||
}
|
||||
@@ -66,7 +66,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
||||
parsedKey, err := ssh.ParseRawPrivateKey([]byte(token))
|
||||
if err != nil && strings.Contains(err.Error(), "private key is passphrase protected") {
|
||||
s1.ExtraData["encrypted"] = "true"
|
||||
parsedKey, passphrase, err = crack([]byte(token))
|
||||
parsedKey, passphrase, err = Crack([]byte(token))
|
||||
if err != nil {
|
||||
s1.SetVerificationError(err, token)
|
||||
continue
|
||||
@@ -88,14 +88,14 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
extraData = newExtraData()
|
||||
verificationErrors = newVerificationErrors()
|
||||
verificationErrors = NewVerificationErrors(3)
|
||||
)
|
||||
|
||||
// Look up certificate information.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
data, err := lookupFingerprint(ctx, fingerprint, s.IncludeExpired)
|
||||
data, err := lookupFingerprintCertificateUrls(ctx, fingerprint, s.IncludeExpired)
|
||||
if err == nil {
|
||||
if data != nil {
|
||||
extraData.Add("certificate_urls", strings.Join(data.CertificateURLs, ", "))
|
||||
@@ -109,7 +109,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
user, err := verifyGitHubUser(ctx, parsedKey)
|
||||
user, err := VerifyGitHubUser(ctx, parsedKey)
|
||||
if err != nil && !errors.Is(err, errPermissionDenied) {
|
||||
verificationErrors.Add(err)
|
||||
}
|
||||
@@ -122,7 +122,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
user, err := verifyGitLabUser(ctx, parsedKey)
|
||||
user, err := VerifyGitLabUser(ctx, parsedKey)
|
||||
if err != nil && !errors.Is(err, errPermissionDenied) {
|
||||
verificationErrors.Add(err)
|
||||
}
|
||||
@@ -137,11 +137,16 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
||||
for k, v := range extraData.data {
|
||||
s1.ExtraData[k] = v
|
||||
}
|
||||
|
||||
// enabled th
|
||||
s1.AnalysisInfo = map[string]string{
|
||||
"token": token,
|
||||
}
|
||||
} else {
|
||||
s1.ExtraData = nil
|
||||
}
|
||||
if len(verificationErrors.errors) > 0 {
|
||||
s1.SetVerificationError(fmt.Errorf("verification failures: %s", strings.Join(verificationErrors.errors, ", ")), token)
|
||||
if len(verificationErrors.Errors) > 0 {
|
||||
s1.SetVerificationError(fmt.Errorf("verification failures: %s", strings.Join(verificationErrors.Errors, ", ")), token)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,19 +169,15 @@ type result struct {
|
||||
GitHubUsername string
|
||||
}
|
||||
|
||||
func lookupFingerprint(ctx context.Context, publicKeyFingerprintInHex string, includeExpired bool) (*result, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://keychecker.trufflesecurity.com/fingerprint/%s", publicKeyFingerprintInHex), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
results := DriftwoodResult{}
|
||||
err = json.NewDecoder(res.Body).Decode(&results)
|
||||
func lookupFingerprintCertificateUrls(
|
||||
ctx context.Context,
|
||||
publicKeyFingerprintInHex string,
|
||||
includeExpired bool,
|
||||
) (*result, error) {
|
||||
results, err := LookupFingerprint(
|
||||
ctx,
|
||||
publicKeyFingerprintInHex,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -201,10 +202,39 @@ func lookupFingerprint(ctx context.Context, publicKeyFingerprintInHex string, in
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func LookupFingerprint(ctx context.Context, publicKeyFingerprintInHex string) (*DriftwoodResult, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://keychecker.trufflesecurity.com/fingerprint/%s", publicKeyFingerprintInHex), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
results := DriftwoodResult{}
|
||||
err = json.NewDecoder(res.Body).Decode(&results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &results, nil
|
||||
}
|
||||
|
||||
type DriftwoodResult struct {
|
||||
CertificateResults []struct {
|
||||
Domains []string `json:",omitempty"`
|
||||
CertificateFingerprint string `json:"CertificateFingerprint"`
|
||||
ExpirationTimestamp time.Time `json:"ExpirationTimestamp"`
|
||||
IssuerName string `json:",omitempty"` // CA information
|
||||
SubjectName string `json:",omitempty"` // Certificate subject
|
||||
IssuerOrganization []string `json:",omitempty"` // CA organization(s)
|
||||
SubjectOrganization []string `json:",omitempty"` // Subject organization(s)
|
||||
KeyUsages []string `json:",omitempty"` // e.g., ["DigitalSignature", "KeyEncipherment"]
|
||||
ExtendedKeyUsages []string `json:",omitempty"` // e.g., ["ServerAuth", "ClientAuth"]
|
||||
SubjectKeyID string `json:",omitempty"` // hex encoded
|
||||
AuthorityKeyID string `json:",omitempty"` // hex encoded
|
||||
SerialNumber string `json:",omitempty"` // hex encoded
|
||||
} `json:"CertificateResults"`
|
||||
GitHubSSHResults []struct {
|
||||
Username string `json:"Username"`
|
||||
@@ -228,20 +258,20 @@ func (e *extraData) Add(key string, value string) {
|
||||
e.mutex.Unlock()
|
||||
}
|
||||
|
||||
type verificationErrors struct {
|
||||
type VerificationErrors struct {
|
||||
mutex sync.Mutex
|
||||
errors []string
|
||||
Errors []string
|
||||
}
|
||||
|
||||
func newVerificationErrors() *verificationErrors {
|
||||
return &verificationErrors{
|
||||
errors: make([]string, 0, 3),
|
||||
func NewVerificationErrors(capacity int) *VerificationErrors {
|
||||
return &VerificationErrors{
|
||||
Errors: make([]string, 0, capacity),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *verificationErrors) Add(err error) {
|
||||
func (e *VerificationErrors) Add(err error) {
|
||||
e.mutex.Lock()
|
||||
e.errors = append(e.errors, err.Error())
|
||||
e.Errors = append(e.Errors, err.Error())
|
||||
e.mutex.Unlock()
|
||||
}
|
||||
|
||||
|
||||
@@ -154,6 +154,7 @@ func TestPrivatekey_FromChunk(t *testing.T) {
|
||||
t.Fatal("no raw secret present")
|
||||
}
|
||||
got[i].Raw = nil
|
||||
got[i].AnalysisInfo = nil
|
||||
}
|
||||
if diff := pretty.Compare(got, tt.want); diff != "" {
|
||||
t.Errorf("PrivatekeyCI.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
|
||||
@@ -203,7 +204,7 @@ func Test_lookupFingerprint(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotFingerprints, err := lookupFingerprint(context.TODO(), tt.publicKeyFingerprintInHex, tt.includeExpired)
|
||||
gotFingerprints, err := lookupFingerprintCertificateUrls(context.TODO(), tt.publicKeyFingerprintInHex, tt.includeExpired)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("lookupFingerprint() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
||||
@@ -103,7 +103,7 @@ func sshDialWithContext(ctx context.Context, network, addr string, config *ssh.C
|
||||
|
||||
var errPermissionDenied = errors.New("permission denied")
|
||||
|
||||
func verifyGitHubUser(ctx context.Context, parsedKey any) (*string, error) {
|
||||
func VerifyGitHubUser(ctx context.Context, parsedKey any) (*string, error) {
|
||||
output, err := firstResponseFromSSH(ctx, parsedKey, "git", "github.com:22")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -121,7 +121,7 @@ func verifyGitHubUser(ctx context.Context, parsedKey any) (*string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func verifyGitLabUser(ctx context.Context, parsedKey any) (*string, error) {
|
||||
func VerifyGitLabUser(ctx context.Context, parsedKey any) (*string, error) {
|
||||
output, err := firstResponseFromSSH(ctx, parsedKey, "git", "gitlab.com:22")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestFirstResponseFromSSH(t *testing.T) {
|
||||
}
|
||||
secretGitHub := testSecrets.MustGetField("PRIVATEKEY_GITHUB")
|
||||
|
||||
parsedKey, err := ssh.ParseRawPrivateKey([]byte(normalize(secretGitHub)))
|
||||
parsedKey, err := ssh.ParseRawPrivateKey([]byte(Normalize(secretGitHub)))
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse test secret: %s", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user