//go:generate generate_permissions permissions.yaml permissions.go privatekey package privatekey import ( "errors" "fmt" "os" "regexp" "strings" "sync" "time" "github.com/fatih/color" "github.com/jedib0t/go-pretty/v6/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, analyzers.NewAnalysisError(a.Type().String(), analyzers.OperationValidateCredentials, analyzers.ServiceConfig, "", errors.New("token not found in credInfo")) } info, err := AnalyzePermissions(ctx, a.Cfg, token) if err != nil { return nil, analyzers.NewAnalysisError(a.Type().String(), analyzers.OperationAnalyzePermissions, analyzers.ServiceCrypto, "", 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 } // key entered through command line may have spaces instead of newlines, replace them token = replaceSpacesWithNewlines(token) 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) } // replaceSpacesWithNewlines extracts the base64 part, replaces spaces with newlines if needed, and reconstructs the key. func replaceSpacesWithNewlines(privateKey string) string { // Regex pattern to extract the key content re := regexp.MustCompile(`(?i)(-----\s*BEGIN[ A-Z0-9_-]*PRIVATE KEY\s*-----)\s*([\s\S]*?)\s*(-----\s*END[ A-Z0-9_-]*PRIVATE KEY\s*-----)`) // Find matches matches := re.FindStringSubmatch(privateKey) if len(matches) != 4 { // no need to process return privateKey } header := matches[1] // BEGIN line base64Part := matches[2] // Base64 content footer := matches[3] // END line // Replace spaces with newlines formattedBase64 := strings.ReplaceAll(base64Part, " ", "\n") // Reconstruct the private key return fmt.Sprintf("%s\n%s\n%s", header, formattedBase64, footer) }