diff --git a/pkg/analyzer/analyzers/analyzers.go b/pkg/analyzer/analyzers/analyzers.go index 06eea241f..c58c25130 100644 --- a/pkg/analyzer/analyzers/analyzers.go +++ b/pkg/analyzer/analyzers/analyzers.go @@ -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 } diff --git a/pkg/analyzer/analyzers/privatekey/expected_output.json b/pkg/analyzer/analyzers/privatekey/expected_output.json new file mode 100644 index 000000000..56d005263 --- /dev/null +++ b/pkg/analyzer/analyzers/privatekey/expected_output.json @@ -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} \ No newline at end of file diff --git a/pkg/analyzer/analyzers/privatekey/permissions.go b/pkg/analyzer/analyzers/privatekey/permissions.go new file mode 100644 index 000000000..d2fbaa045 --- /dev/null +++ b/pkg/analyzer/analyzers/privatekey/permissions.go @@ -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") +} diff --git a/pkg/analyzer/analyzers/privatekey/permissions.yaml b/pkg/analyzer/analyzers/privatekey/permissions.yaml new file mode 100644 index 000000000..583f61a37 --- /dev/null +++ b/pkg/analyzer/analyzers/privatekey/permissions.yaml @@ -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 \ No newline at end of file diff --git a/pkg/analyzer/analyzers/privatekey/privatekey.go b/pkg/analyzer/analyzers/privatekey/privatekey.go new file mode 100644 index 000000000..03a1f0a70 --- /dev/null +++ b/pkg/analyzer/analyzers/privatekey/privatekey.go @@ -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) +} diff --git a/pkg/analyzer/analyzers/privatekey/privatekey_test.go b/pkg/analyzer/analyzers/privatekey/privatekey_test.go new file mode 100644 index 000000000..7849579e8 --- /dev/null +++ b/pkg/analyzer/analyzers/privatekey/privatekey_test.go @@ -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) + } + }) + } +} diff --git a/pkg/analyzer/cli.go b/pkg/analyzer/cli.go index d3f8004e4..b3cf2c7bc 100644 --- a/pkg/analyzer/cli.go +++ b/pkg/analyzer/cli.go @@ -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"]) } } diff --git a/pkg/detectors/privatekey/cracker.go b/pkg/detectors/privatekey/cracker.go index b0744643b..af5b65b9b 100644 --- a/pkg/detectors/privatekey/cracker.go +++ b/pkg/detectors/privatekey/cracker.go @@ -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 { diff --git a/pkg/detectors/privatekey/cracker_test.go b/pkg/detectors/privatekey/cracker_test.go index fe56d23ed..de31c9b61 100644 --- a/pkg/detectors/privatekey/cracker_test.go +++ b/pkg/detectors/privatekey/cracker_test.go @@ -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) } } diff --git a/pkg/detectors/privatekey/normalize.go b/pkg/detectors/privatekey/normalize.go index 8b232ab3b..3e3f45306 100644 --- a/pkg/detectors/privatekey/normalize.go +++ b/pkg/detectors/privatekey/normalize.go @@ -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", "") diff --git a/pkg/detectors/privatekey/privatekey.go b/pkg/detectors/privatekey/privatekey.go index 8fe43c580..2dc16c6ec 100644 --- a/pkg/detectors/privatekey/privatekey.go +++ b/pkg/detectors/privatekey/privatekey.go @@ -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() } diff --git a/pkg/detectors/privatekey/privatekey_integration_test.go b/pkg/detectors/privatekey/privatekey_integration_test.go index 44c9cf4f9..cf80de52a 100644 --- a/pkg/detectors/privatekey/privatekey_integration_test.go +++ b/pkg/detectors/privatekey/privatekey_integration_test.go @@ -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 diff --git a/pkg/detectors/privatekey/ssh_integration.go b/pkg/detectors/privatekey/ssh_integration.go index b7e11d37b..5f0a8cbc5 100644 --- a/pkg/detectors/privatekey/ssh_integration.go +++ b/pkg/detectors/privatekey/ssh_integration.go @@ -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 diff --git a/pkg/detectors/privatekey/ssh_integration_test.go b/pkg/detectors/privatekey/ssh_integration_test.go index f77e1b31f..14da9f908 100644 --- a/pkg/detectors/privatekey/ssh_integration_test.go +++ b/pkg/detectors/privatekey/ssh_integration_test.go @@ -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) }