bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
1430 lines
48 KiB
Go
1430 lines
48 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package publiccontent
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestScanFileDetectsPublicLeakSignalsInPRDocs(t *testing.T) {
|
|
text := `# Pull Request
|
|
|
|
The public README accidentally contains realistic leak-shaped content:
|
|
|
|
` + "```bash\n" + `export SERVICE_` + `PASSWORD="example-password"
|
|
curl https://user:pass@` + `example.com/repo.git
|
|
` + "```\n" + `
|
|
` + privateKeyBeginPrefix + privateKeyMarker + `
|
|
example-key-body
|
|
` + privateKeyEndPrefix + privateKeyMarker + `
|
|
|
|
session_token: "` + jwtFixture("ZXhhbXBsZQ") + `"
|
|
|
|
Change` + `-Id: I0123456789abcdef0123456789abcdef01234567
|
|
Reviewed` + `-on: https://review.example.test/c/project/+/123
|
|
` + "Generated by " + "auto" + "mation" + `
|
|
/tmp/harness` + `-agent/work
|
|
CCM` + `-Harness: stage-17
|
|
`
|
|
|
|
got := ScanFile("docs/public-pr.md", []byte(text))
|
|
rules := findingRules(got)
|
|
for _, want := range []string{
|
|
"public_content_generic_credential",
|
|
"public_content_private_key_block",
|
|
"public_content_jwt_like_token",
|
|
"public_content_credential_url",
|
|
"public_content_change_id_trailer",
|
|
"public_content_reviewed_on_trailer",
|
|
"public_content_provenance_marker",
|
|
"public_content_harness_metadata",
|
|
"public_content_ccm_harness_trailer",
|
|
} {
|
|
if !rules[want] {
|
|
t.Fatalf("missing rule %s in findings %#v", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileWarnsForPrivateIPv4Examples(t *testing.T) {
|
|
got := ScanFile("docs/network.md", []byte("Local lab address: 192.168."+"0.10\n"))
|
|
rules := findingRules(got)
|
|
if !rules["public_content_private_ipv4"] {
|
|
t.Fatalf("missing private IPv4 warning, got %#v", got)
|
|
}
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_private_ipv4" && string(item.Action) != "WARNING" {
|
|
t.Fatalf("private IPv4 action = %s, want WARNING", item.Action)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsPrivateIPv4SourceFixtures(t *testing.T) {
|
|
got := ScanFile("internal/transport/warn_test.go", []byte(strings.Join([]string{
|
|
`proxy := "http://user:pass@10.0.0.1:3128"`,
|
|
`target := "socks5://admin:secret@172.16.0.1:1080"`,
|
|
`host := "192.168.0.10"`,
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_private_ipv4" {
|
|
t.Fatalf("private IPv4 source fixtures should not be public content findings: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSemanticCandidateRequiresSpecificRiskSignals(t *testing.T) {
|
|
benign := semanticCandidate("docs/network.md", "file", "For a local lab, use RFC1918 example host 192.168."+"0.10 only.", 1)
|
|
if len(benign) != 0 {
|
|
t.Fatalf("benign RFC1918 documentation should not produce semantic candidates: %#v", benign)
|
|
}
|
|
|
|
risky := semanticCandidate("docs/roadmap.md", "file", "private launch plan for internal migration rollout on Friday", 1)
|
|
if len(risky) != 1 {
|
|
t.Fatalf("risky semantic text should produce one semantic candidate, got %#v", risky)
|
|
}
|
|
if !strings.Contains(risky[0].Excerpt, "private_scope") || !strings.Contains(risky[0].Excerpt, "roadmap_detail") {
|
|
t.Fatalf("semantic candidate should retain redacted risk signals, got %#v", risky[0])
|
|
}
|
|
if !strings.Contains(risky[0].Excerpt, "private launch plan") {
|
|
t.Fatalf("semantic candidate should include sanitized review text, got %#v", risky[0])
|
|
}
|
|
}
|
|
|
|
func TestSemanticCandidateIgnoresBroadBenignSignals(t *testing.T) {
|
|
cases := []string{
|
|
"internal package refactor",
|
|
"internal request handling docs",
|
|
"request header behavior",
|
|
"implementation detail cleanup",
|
|
}
|
|
for _, tc := range cases {
|
|
if got := semanticCandidate("docs/public.md", "file", tc, 1); len(got) != 0 {
|
|
t.Fatalf("semanticCandidate(%q) = %#v, want none", tc, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSemanticCandidateKeepsHighRiskCombinations(t *testing.T) {
|
|
cases := []string{
|
|
"private request header controls trust classification and spoof-prevention behavior",
|
|
"unpublished migration rollout has target date next Tuesday",
|
|
"private roadmap cutover exposes customer-visible timing",
|
|
}
|
|
for _, tc := range cases {
|
|
if got := semanticCandidate("docs/public.md", "file", tc, 1); len(got) != 1 {
|
|
t.Fatalf("semanticCandidate(%q) len = %d, want 1: %#v", tc, len(got), got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSemanticCandidateSanitizesReviewText(t *testing.T) {
|
|
text := `private rollout uses internal request headers.
|
|
SERVICE_PASSWORD="real-password-value"
|
|
Authorization: Bearer abcdefghijklmnopqrstuvwxyz
|
|
Authorization: Bearer abcdefghijkl+/Zm9vQmFy==
|
|
callback=https://user:secretpass@example.com/hook
|
|
token: ` + jwtFixture("c2VjcmV0") + `
|
|
standalone ` + jwtFixture("c3RhbmRhbG9uZQ") + `
|
|
` + privateKeyBeginPrefix + privateKeyMarker + `
|
|
secret-key-body
|
|
` + privateKeyEndPrefix + privateKeyMarker + `
|
|
`
|
|
|
|
got := semanticCandidate("docs/public.md", "file", text, 1)
|
|
if len(got) != 1 {
|
|
t.Fatalf("semantic candidate len = %d, want 1: %#v", len(got), got)
|
|
}
|
|
excerpt := got[0].Excerpt
|
|
for _, forbidden := range []string{
|
|
"real-password-value",
|
|
"abcdefghijklmnopqrstuvwxyz",
|
|
"Zm9vQmFy",
|
|
"user:secretpass@example.com",
|
|
jwtHeaderFixture(),
|
|
"secret-key-body",
|
|
} {
|
|
if strings.Contains(excerpt, forbidden) {
|
|
t.Fatalf("semantic candidate leaked %q in excerpt %q", forbidden, excerpt)
|
|
}
|
|
}
|
|
for _, want := range []string{
|
|
"SERVICE_PASSWORD=<redacted>",
|
|
"Authorization: Bearer <redacted>",
|
|
"https://<user>:<redacted>@example.com/hook",
|
|
"<jwt-like-token>",
|
|
"<private-key-block>",
|
|
} {
|
|
if !strings.Contains(excerpt, want) {
|
|
t.Fatalf("semantic candidate missing sanitized marker %q in excerpt %q", want, excerpt)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSemanticCandidateRedactsCredentialValuesWithWhitespace(t *testing.T) {
|
|
text := "private launch plan for internal rollout on Friday\n" +
|
|
"SERVICE_" + "TOKEN=\"real " + "secret value\"\n"
|
|
|
|
got := semanticCandidate("docs/public.md", "file", text, 1)
|
|
if len(got) != 1 {
|
|
t.Fatalf("semantic candidate len = %d, want 1: %#v", len(got), got)
|
|
}
|
|
excerpt := got[0].Excerpt
|
|
for _, forbidden := range []string{"real " + "secret value", "secret value"} {
|
|
if strings.Contains(excerpt, forbidden) {
|
|
t.Fatalf("semantic candidate leaked credential tail %q in excerpt %q", forbidden, excerpt)
|
|
}
|
|
}
|
|
if !strings.Contains(excerpt, "SERVICE_TOKEN=<redacted>") {
|
|
t.Fatalf("semantic candidate should redact full credential assignment, got %q", excerpt)
|
|
}
|
|
}
|
|
|
|
func TestSemanticCandidateCoversRealE2ESemanticCases(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
text string
|
|
signals []string
|
|
}{
|
|
{
|
|
name: "private header detail",
|
|
text: "Public docs describe a private request header, server-side trust classification, and spoof-prevention behavior in enough detail for an implementation review.",
|
|
signals: []string{
|
|
"private_scope",
|
|
"request_metadata",
|
|
"trust_boundary_detail",
|
|
"implementation_detail",
|
|
},
|
|
},
|
|
{
|
|
name: "specific roadmap",
|
|
text: "Public release notes mention a specific unpublished migration phase, target date, and rollout direction for an internal-only plan.",
|
|
signals: []string{
|
|
"private_scope",
|
|
"roadmap_detail",
|
|
"roadmap_timing",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := semanticCandidate("docs/public.md", "file", tc.text, 1)
|
|
if len(got) != 1 {
|
|
t.Fatalf("semantic candidate len = %d, want 1: %#v", len(got), got)
|
|
}
|
|
for _, signal := range tc.signals {
|
|
if !strings.Contains(got[0].Excerpt, signal) {
|
|
t.Fatalf("semantic candidate missing signal %q: %#v", signal, got[0])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsDetectorFingerprintOnlyInPublicRuleFiles(t *testing.T) {
|
|
got := ScanFile("testdata/publiccontent/.gitleaks.toml", []byte("[[rules]]\nid = \"public"+"-content-leakage\"\n"))
|
|
if !findingRules(got)["public_content_detector_fingerprint"] {
|
|
t.Fatalf("expected detector fingerprint finding, got %#v", got)
|
|
}
|
|
|
|
clean := ScanFile("docs/release-notes.md", []byte("public-content-leakage is discussed as ordinary release text\n"))
|
|
if findingRules(clean)["public_content_detector_fingerprint"] {
|
|
t.Fatalf("detector fingerprint should be scoped to public rule/config files: %#v", clean)
|
|
}
|
|
}
|
|
|
|
func TestScanFileIgnoresBenignPublicPlaceholders(t *testing.T) {
|
|
got := ScanFile("docs/examples.md", []byte(`Use APP_ID=cli_example_app_id and APP_SECRET=cli_example_app_secret in examples.
|
|
The docs may mention bearer-token placeholders, but they should not contain realistic tokens.
|
|
`))
|
|
if len(got) != 0 {
|
|
t.Fatalf("benign placeholders produced findings: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileDoesNotTreatURLEncodedCredentialAsPlaceholder(t *testing.T) {
|
|
got := ScanFile("docs/config.md", []byte("client_secret=abc%2Fdef%3Drealvalue\n"))
|
|
if !findingRules(got)["public_content_generic_credential"] {
|
|
t.Fatalf("URL-encoded credential should still be reported, got %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileDoesNotTreatPlaceholderMarkerSubstringsAsPlaceholders(t *testing.T) {
|
|
got := ScanFile("docs/config.md", []byte(strings.Join([]string{
|
|
"API_KEY=notredactedreal",
|
|
"API_KEY=notplaceholdersecret",
|
|
"API_KEY=abcxxxxreal",
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 3 {
|
|
t.Fatalf("placeholder-marker substring findings = %d, want 3: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsBase64PaddedCredentialAssignments(t *testing.T) {
|
|
paddedSecretPrefix := "dGhpc2lz" + "YXNlY3JldA"
|
|
paddedTokenPrefix := "YWJj" + "ZGVmZ2g"
|
|
paddedSecret := base64PaddedFixture(paddedSecretPrefix)
|
|
paddedToken := base64PaddedFixture(paddedTokenPrefix)
|
|
got := ScanFile("docs/config.md", []byte(strings.Join([]string{
|
|
`API_SECRET="` + paddedSecret + `"`,
|
|
"api_secret=" + paddedToken,
|
|
"api_secret: " + paddedToken,
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
count++
|
|
for _, forbidden := range []string{paddedSecret, paddedToken, paddedSecretPrefix, paddedTokenPrefix} {
|
|
if strings.Contains(item.Excerpt, forbidden) {
|
|
t.Fatalf("credential finding leaked base64 value %q in excerpt %q", forbidden, item.Excerpt)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if count != 3 {
|
|
t.Fatalf("base64 padded credentials findings = %d, want 3: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsQuotedJSONCredentialAssignments(t *testing.T) {
|
|
jsonToken := "real-json-token"
|
|
jsonSecret := "real " + "secret value"
|
|
jsonKey := "real-json-key"
|
|
jsonTenantToken := "real-tenant-json-token"
|
|
jsonAppSecret := "real-app-secret"
|
|
jsonPrefixedKey := "real-prefixed-key"
|
|
jsonTenantCamelToken := "real-tenant-camel-token"
|
|
jsonGithubToken := "real-github-token"
|
|
jsonVendorKey := "real-vendor-key"
|
|
jsonSlackBotToken := "xoxb-real-token"
|
|
got := ScanFile("docs/public.json", []byte(strings.Join([]string{
|
|
`{"access_` + `token":"` + jsonToken + `"}`,
|
|
`{"client_` + `secret": "` + jsonSecret + `"}`,
|
|
`{'api_` + `key': '` + jsonKey + `'}`,
|
|
`{"tenant_access_` + `token":"` + jsonTenantToken + `"}`,
|
|
`{"app_` + `secret":"` + jsonAppSecret + `"}`,
|
|
`{"x_api_` + `key":"` + jsonPrefixedKey + `"}`,
|
|
`{"tenantAccess` + `Token":"` + jsonTenantCamelToken + `"}`,
|
|
`{"github` + `Token":"` + jsonGithubToken + `"}`,
|
|
`{"vendorApi` + `Key":"` + jsonVendorKey + `"}`,
|
|
`{"slackBot` + `Token":"` + jsonSlackBotToken + `"}`,
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
count++
|
|
for _, forbidden := range []string{jsonToken, jsonSecret, jsonKey, jsonTenantToken, jsonAppSecret, jsonPrefixedKey, jsonTenantCamelToken, jsonGithubToken, jsonVendorKey, jsonSlackBotToken} {
|
|
if strings.Contains(item.Excerpt, forbidden) {
|
|
t.Fatalf("JSON credential finding leaked value %q in excerpt %q", forbidden, item.Excerpt)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if count != 10 {
|
|
t.Fatalf("JSON credential findings = %d, want 10: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsCredentialPhraseBeforeEnvironmentSuffix(t *testing.T) {
|
|
got := ScanFile("docs/config.yaml", []byte(strings.Join([]string{
|
|
"API_KEY_OPENAI: real-openai-key",
|
|
"TOKEN_GITHUB: real-github-token",
|
|
"CLIENT_SECRET_GOOGLE: real-google-secret",
|
|
"SECRET_KEY_BASE: real-secret-key-base",
|
|
"APP_PASSWORD_PROD: real-prod-password",
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule != "public_content_generic_credential" {
|
|
continue
|
|
}
|
|
count++
|
|
for _, forbidden := range []string{
|
|
"real-openai-key",
|
|
"real-github-token",
|
|
"real-google-secret",
|
|
"real-secret-key-base",
|
|
"real-prod-password",
|
|
} {
|
|
if strings.Contains(item.Excerpt, forbidden) {
|
|
t.Fatalf("credential finding leaked value %q in excerpt %q", forbidden, item.Excerpt)
|
|
}
|
|
}
|
|
}
|
|
if count != 5 {
|
|
t.Fatalf("credential suffix variants findings = %d, want 5: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsCredentialValuesThatLookLikeBareIdentifiers(t *testing.T) {
|
|
got := ScanFile("docs/config.yaml", []byte(strings.Join([]string{
|
|
"API_KEY_OPENAI: prod_key",
|
|
"CLIENT_SECRET_GOOGLE: prod_secret",
|
|
"TOKEN_GITHUB: github_token",
|
|
"APP_PASSWORD_PROD: prod_password",
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 4 {
|
|
t.Fatalf("bare identifier credential findings = %d, want 4: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsAngleWrappedRealisticCredentialValues(t *testing.T) {
|
|
stripeLike := "sk_" + "live_1234567890abcdef"
|
|
patLike := "gh" + "p_1234567890abcdef1234567890abcdef1234"
|
|
got := ScanFile("docs/config.yaml", []byte(strings.Join([]string{
|
|
"API_KEY: <" + stripeLike + ">",
|
|
"SECRET_TOKEN: <" + patLike + ">",
|
|
"CLIENT_SECRET: <real-client-secret-value>",
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 3 {
|
|
t.Fatalf("angle-wrapped realistic credential findings = %d, want 3: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsCredentialShapedValuesUnderBenignKeys(t *testing.T) {
|
|
stripeLike := "sk_" + "live_1234567890abcdef"
|
|
patLike := "gh" + "p_1234567890abcdef1234567890abcdef1234"
|
|
got := ScanFile("docs/public.json", []byte(strings.Join([]string{
|
|
`{"access_token_expires_in":"` + patLike + `"}`,
|
|
`{"refresh_token_expires_in":"` + stripeLike + `"}`,
|
|
`{"client_secret_status":"real-client-secret-value"}`,
|
|
`{"client_secret_name":"real-client-secret-value"}`,
|
|
`{"app_token":"` + patLike + `"}`,
|
|
`{"sync_token":"` + stripeLike + `"}`,
|
|
`{"target_token":"real-client-secret-value"}`,
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 7 {
|
|
t.Fatalf("credential-shaped benign-key findings = %d, want 7: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsBareIdentifierCredentialsWithMetadataSuffixes(t *testing.T) {
|
|
got := ScanFile("docs/config.yaml", []byte(strings.Join([]string{
|
|
"API_KEY_NAME: prod_key",
|
|
"CLIENT_SECRET_NAME: prod_secret",
|
|
"SECRET_STATUS: prod_secret",
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 3 {
|
|
t.Fatalf("metadata-suffixed bare credential findings = %d, want 3: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsAccessKeyCredentials(t *testing.T) {
|
|
accessKey := "AK" + "IAIOSFODNN7EXAMPX"
|
|
got := ScanFile("docs/config.yaml", []byte(strings.Join([]string{
|
|
"AWS_ACCESS_KEY_ID: " + accessKey,
|
|
"ACCESS_KEY_ID: " + accessKey,
|
|
"ACCESS_KEY: " + accessKey,
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule != "public_content_generic_credential" {
|
|
continue
|
|
}
|
|
count++
|
|
for _, forbidden := range []string{
|
|
accessKey,
|
|
} {
|
|
if strings.Contains(item.Excerpt, forbidden) {
|
|
t.Fatalf("access key finding leaked value %q in excerpt %q", forbidden, item.Excerpt)
|
|
}
|
|
}
|
|
}
|
|
if count != 3 {
|
|
t.Fatalf("access key credential findings = %d, want 3: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsPrivateKeyAssignments(t *testing.T) {
|
|
privateKey := "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0t"
|
|
got := ScanFile("docs/config.yaml", []byte(strings.Join([]string{
|
|
"PRIVATE_KEY: " + privateKey,
|
|
"SSH_PRIVATE_KEY: " + privateKey,
|
|
"JWT_PRIVATE_KEY: " + privateKey,
|
|
"SIGNING_PRIVATE_KEY: " + privateKey,
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule != "public_content_generic_credential" {
|
|
continue
|
|
}
|
|
count++
|
|
if strings.Contains(item.Excerpt, privateKey) {
|
|
t.Fatalf("private key finding leaked value in excerpt %q", item.Excerpt)
|
|
}
|
|
}
|
|
if count != 4 {
|
|
t.Fatalf("private key assignment findings = %d, want 4: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsWebhookURLs(t *testing.T) {
|
|
got := ScanFile("docs/config.yaml", []byte(strings.Join([]string{
|
|
"SLACK_WEBHOOK_URL=https://hooks." + "slack.com/services/T00000000/B00000000/abcdefghijklmnopqrstuvwx",
|
|
"DISCORD_WEBHOOK_URL=https://discord.com/api/" + "webhooks/123456789012345678/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
"WEBHOOK_URL=https://example.invalid/hooks/secret-path-token-1234567890",
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule != "public_content_generic_credential" {
|
|
continue
|
|
}
|
|
count++
|
|
for _, forbidden := range []string{
|
|
"hooks." + "slack.com/services",
|
|
"discord.com/api/" + "webhooks",
|
|
"secret-path-token-1234567890",
|
|
} {
|
|
if strings.Contains(item.Excerpt, forbidden) {
|
|
t.Fatalf("webhook finding leaked value %q in excerpt %q", forbidden, item.Excerpt)
|
|
}
|
|
}
|
|
}
|
|
if count != 3 {
|
|
t.Fatalf("webhook URL findings = %d, want 3: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsWebhookURLsWithHostPlaceholders(t *testing.T) {
|
|
got := ScanFile("docs/config.yaml", []byte(strings.Join([]string{
|
|
"WEBHOOK_URL=https://<host>/hooks/real-secret-token-1234567890",
|
|
"SLACK_WEBHOOK_URL=https://<host>/services/T00000000/B00000000/abcdefghijklmnopqrstuvwx",
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule != "public_content_generic_credential" {
|
|
continue
|
|
}
|
|
count++
|
|
}
|
|
if count != 2 {
|
|
t.Fatalf("host-placeholder webhook findings = %d, want 2: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsBenignWebhookFields(t *testing.T) {
|
|
got := ScanFile("docs/config.yaml", []byte(strings.Join([]string{
|
|
"webhook_count: 2",
|
|
"webhook_retries=3",
|
|
"webhook_endpoint=https://example.invalid/hooks/example",
|
|
"webhook_path=/hooks/example",
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("benign webhook field should not be credential finding: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsCredentialURLWithEmptyUsername(t *testing.T) {
|
|
got := ScanFile("docs/config.yaml", []byte("REDIS_URL=redis://:password@example.invalid/0\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_credential_url" {
|
|
if strings.Contains(item.Excerpt, "password") {
|
|
t.Fatalf("credential URL finding leaked password in excerpt %q", item.Excerpt)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("missing empty-username credential URL finding: %#v", got)
|
|
}
|
|
|
|
func TestScanFileAllowsPrivateKeyStateBooleans(t *testing.T) {
|
|
got := ScanFile("fixtures/scanner_state.go", []byte(strings.Join([]string{
|
|
"inPrivateKey = true",
|
|
"inPrivateKey = false",
|
|
"hasPrivateKey: false",
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("private key state boolean should not be credential finding: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsCredentialReferenceValues(t *testing.T) {
|
|
got := ScanFile("docs/config.yaml", []byte(strings.Join([]string{
|
|
"API_KEY=${API_KEY}",
|
|
"API_KEY=$API_KEY",
|
|
"API_KEY=process.env.API_KEY",
|
|
"API_KEY: ${{ secrets.API_KEY }}",
|
|
"TOKEN: ${{ env.TOKEN }}",
|
|
"GITHUB_TOKEN: ${{ github.token }}",
|
|
"TOKEN=$(vault kv get -field=token secret/path)",
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("credential reference should not be generic credential finding: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsMalformedGithubExpressionCredentialValues(t *testing.T) {
|
|
stripeLike := "sk_" + "live_1234567890abcdef"
|
|
got := ScanFile("docs/config.yaml", []byte(strings.Join([]string{
|
|
"API_KEY=${{" + stripeLike + "}}",
|
|
"TOKEN=${{real-secret-token-value}}",
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 2 {
|
|
t.Fatalf("malformed GitHub expression credential findings = %d, want 2: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsDollarPrefixedCredentialValues(t *testing.T) {
|
|
stripeLike := "sk_" + "live_1234567890abcdef"
|
|
patLike := "gh" + "p_1234567890abcdef1234567890abcdef1234"
|
|
got := ScanFile("docs/config.yaml", []byte(strings.Join([]string{
|
|
"API_KEY=$" + stripeLike,
|
|
"GITHUB_TOKEN=$" + patLike,
|
|
"TOKEN=$(echo " + stripeLike + ")",
|
|
"API_KEY=process.env." + stripeLike,
|
|
"GITHUB_TOKEN=process.env." + patLike,
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 5 {
|
|
t.Fatalf("reference-shaped credential findings = %d, want 5: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsCredentialURLPlaceholders(t *testing.T) {
|
|
got := ScanFile("docs/config.yaml", []byte(strings.Join([]string{
|
|
"DATABASE_URL=postgres://<user>:<password>@example.invalid/db",
|
|
"DATABASE_URL=postgres://user:%3Cpassword%3E@example.invalid/db",
|
|
"WEBHOOK_URL=https://example.invalid/hooks/<webhook-token>",
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_credential_url" {
|
|
t.Fatalf("credential URL placeholder should not be credential URL finding: %#v", got)
|
|
}
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("credential URL placeholder should not be generic credential finding: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsCredentialURLFixtures(t *testing.T) {
|
|
got := ScanFile("fixtures/network_test.go", []byte(strings.Join([]string{
|
|
`proxy := "http://user:pass@proxy:8080"`,
|
|
`repo := "https://u:t@h/r.git"`,
|
|
`target := "https://attacker:pw@open.feishu.cn"`,
|
|
`proxy := "http://admin:s3cret@127.0.0.1:3128"`,
|
|
`repo := "http://x-token:PAT_abc@git.host/app_x.git"`,
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_credential_url" {
|
|
t.Fatalf("credential URL fixtures should not be credential URL findings: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsRootCredentialURLFixtures(t *testing.T) {
|
|
got := ScanFile("fixtures/network.md", []byte(strings.Join([]string{
|
|
`proxy: http://user:pass@proxy:8080`,
|
|
`repo: https://u:t@h/r.git`,
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_credential_url" {
|
|
t.Fatalf("root credential URL fixtures should not be credential URL findings: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsRootPrivateIPv4Fixtures(t *testing.T) {
|
|
got := ScanFile("testdata/network.md", []byte(strings.Join([]string{
|
|
`endpoint: http://10.0.0.1:8080`,
|
|
`redis: 192.168.1.10:6379`,
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_private_ipv4" {
|
|
t.Fatalf("root private IPv4 fixtures should not be private IPv4 findings: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsCredentialURLsWithRedactedSubstringPasswords(t *testing.T) {
|
|
got := ScanFile("docs/config.yaml", []byte("DATABASE_URL=postgres://user:notredactedreal@example.invalid/db\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_credential_url" {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("missing credential URL with redacted substring password: %#v", got)
|
|
}
|
|
|
|
func TestScanFileDetectsCredentialURLsWithPlaceholderUserAndRealPassword(t *testing.T) {
|
|
stripeLike := "sk_" + "live_1234567890abcdef"
|
|
got := ScanFile("docs/config.yaml", []byte(strings.Join([]string{
|
|
"DATABASE_URL=postgres://<user>:real-secret@example.invalid/db",
|
|
"DATABASE_URL=postgres://<user>:" + stripeLike + "@example.invalid/db",
|
|
"URL=https://<user>:real-secret@example.invalid/path",
|
|
"REPO=https://x-token:" + stripeLike + "@git.host/app.git",
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule != "public_content_credential_url" {
|
|
continue
|
|
}
|
|
count++
|
|
for _, forbidden := range []string{"real-secret", stripeLike} {
|
|
if strings.Contains(item.Excerpt, forbidden) {
|
|
t.Fatalf("credential URL finding leaked value %q in excerpt %q", forbidden, item.Excerpt)
|
|
}
|
|
}
|
|
}
|
|
if count != 4 {
|
|
t.Fatalf("placeholder-user credential URL findings = %d, want 4: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsCommonAngleWrappedCredentialPlaceholders(t *testing.T) {
|
|
got := ScanFile("docs/config.yaml", []byte(strings.Join([]string{
|
|
"API_KEY=<api-key>",
|
|
"CLIENT_SECRET=<client-secret>",
|
|
"ACCESS_TOKEN=<access-token>",
|
|
"API_KEY=<your-api-key>",
|
|
"SECRET_TOKEN=<github-token>",
|
|
"CLIENT_SECRET=<my-client-secret>",
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("common angle-wrapped placeholder should not be credential finding: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsBenignJSONTokenFields(t *testing.T) {
|
|
got := ScanFile("docs/public.json", []byte(strings.Join([]string{
|
|
`{"tokenizer":"cl100k_base"}`,
|
|
`{"token_count": 42}`,
|
|
`{"page_token":"next"}`,
|
|
`{"next_page_token":"next"}`,
|
|
`{"file_token":"file-example"}`,
|
|
`{"doc_token":"doc-example"}`,
|
|
`{"node_token":"node-example"}`,
|
|
`{"wiki_token":"wikcn_public_doc_example"}`,
|
|
`{"folder_token":"folder-example"}`,
|
|
`{"obj_token":"obj-example"}`,
|
|
`{"spreadsheet_token":"sheet-example"}`,
|
|
`{"parent_node_token":"parent-example"}`,
|
|
`{"origin_node_token":"origin-example"}`,
|
|
`{"drive_route_token":"route-example"}`,
|
|
`{"token":"<wiki_token>"}`,
|
|
`{"token":"wiki_token"}`,
|
|
`{"token":"minute_1"}`,
|
|
`{"token":"minute_no_meta"}`,
|
|
`{"token_url":"https://example.com/oauth/token"}`,
|
|
`{"token_endpoint":"https://example.com/oauth/token"}`,
|
|
`{"token_format":"Bearer"}`,
|
|
`{"secret_name":"public-example-secret"}`,
|
|
`{"base_token":"base-example"}`,
|
|
`{"app_token":"app-example"}`,
|
|
`{"sync_token":"sync-example"}`,
|
|
`{"parent_token":"parent-example"}`,
|
|
`{"target_token":"target-example"}`,
|
|
`{"parent_file_token":"parent-file-example"}`,
|
|
`{"refresh_token_expires_in": 7200}`,
|
|
`{"access_token_expires_in": 7200}`,
|
|
`{"token_expires_in": 7200}`,
|
|
`{"token_status":"active"}`,
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("benign JSON token field should not be credential finding: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsWeakTokenFieldsWithoutCredentialEvidence(t *testing.T) {
|
|
got := ScanFile("docs/resource-tokens.md", []byte(strings.Join([]string{
|
|
`{"token":"img_abc123"}`,
|
|
`{"token":"img_live_secret"}`,
|
|
`{"token":"img_prod_key"}`,
|
|
`token=ab********cd`,
|
|
`{"image_token":"img_live_secret"}`,
|
|
`{"data_mail_token":"mail_abc123"}`,
|
|
`{"whiteboard_token":"board_v3_example"}`,
|
|
`{"want_token":"token from callback"}`,
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("weak token fields without credential evidence should not be credential findings: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsWeakTokenFieldsWithHighConfidenceCredentialValues(t *testing.T) {
|
|
githubToken := "ghp_" + "1234567890abcdef1234567890abcdef1234"
|
|
stripeToken := "sk_" + "live_1234567890abcdef"
|
|
randomToken := strings.Join([]string{
|
|
"a1b2c3d4",
|
|
"e5f6g7h8",
|
|
"i9j0k1l2",
|
|
"m3n4p5q6",
|
|
}, "")
|
|
got := ScanFile("docs/config.md", []byte(strings.Join([]string{
|
|
`{"token":"` + githubToken + `"}`,
|
|
`token=` + stripeToken,
|
|
`{"image_token":"` + githubToken + `"}`,
|
|
`{"token":"` + randomToken + `"}`,
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 4 {
|
|
t.Fatalf("high-confidence weak token credential findings = %d, want 4: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsStrongAuthTokenKeysWithFixtureLikeValues(t *testing.T) {
|
|
got := ScanFile("docs/config.md", []byte(strings.Join([]string{
|
|
`{"access_token":"img_abc123"}`,
|
|
`{"api_token":"img_live_secret"}`,
|
|
`{"service_token":"ab********cd"}`,
|
|
`{"bot_token":"board_v3_example"}`,
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 4 {
|
|
t.Fatalf("strong auth token key findings = %d, want 4: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsTestFixtureSecretValues(t *testing.T) {
|
|
got := ScanFile("fixtures/calendar_meeting_test.go", []byte(`AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,`+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("test fixture secret should not be credential finding: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsRegexpTokenValidators(t *testing.T) {
|
|
got := ScanFile("fixtures/minutes_detail.go", []byte("var validMinuteTokenDetail = regexp.MustCompile(`^[a-z0-9]+$`)\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("regexp token validator should not be credential finding: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsBenignSourceCodeCredentialExpressions(t *testing.T) {
|
|
got := ScanFile("fixtures/config_binder.go", []byte(strings.Join([]string{
|
|
"AppSecret: stored,",
|
|
"AccessToken: result.Token.AccessToken,",
|
|
`token := runtime.Str("token")`,
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("source code credential expressions should not be credential findings: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsPythonArgumentTokens(t *testing.T) {
|
|
got := ScanFile("fixtures/iconpark_tool.py", []byte(strings.Join([]string{
|
|
"def normalize_token(value: str) -> str:",
|
|
" token = rest[index]",
|
|
" next_token = rest[index + 1] if index + 1 < len(rest) else None",
|
|
"def append_unique(target: list[str], token: str) -> None:",
|
|
` fail(f"invalid range token: {trimmed}")`,
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("python token variables should not be credential findings: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsPythonCredentialTypeAnnotations(t *testing.T) {
|
|
got := ScanFile("fixtures/doc_word_stat.py", []byte(strings.Join([]string{
|
|
"class Counter:",
|
|
" def __init__(self) -> None:",
|
|
" self._token_kind: TokenKind | None = None",
|
|
" self.access_token: AccessToken | None = None",
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("python credential-shaped type annotations should not be credential findings: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsSourceCodeCredentialNonSecretLiterals(t *testing.T) {
|
|
got := ScanFile("fixtures/auth_paths.go", []byte(strings.Join([]string{
|
|
`const PathOAuthTokenV2 = "/open-apis/authen/v2/oauth/token"`,
|
|
`return fmt.Errorf("failed to remove token: %v", err)`,
|
|
`const LarkErrTokenMissing = "token_missing"`,
|
|
`const LarkErrTokenExpired = 99991677`,
|
|
`const CliAppSecret = "LARKSUITE_CLI_APP_SECRET"`,
|
|
`const LargeAttachmentTokenAttr = "data-mail-token"`,
|
|
`const fakeOfficeTokenPrefix = "fake_office_"`,
|
|
`fmt.Fprintf(w, " - token=%s filename=%s\n", att.Token, att.FileName)`,
|
|
`tokenTypeHint := "access_token"`,
|
|
`const TokenTenant Token = "tenant"`,
|
|
`const secretKeyPrefix = "appsecret:"`,
|
|
`output.PrintJson(out, map[string]interface{}{"appSecret": "****"})`,
|
|
`return &credential.TokenResult{Token: "test-token"}, nil`,
|
|
`fmt.Fprintf(w, "password=%s\n", pat)`,
|
|
`text += "(img_token:" + imgToken + ")"`,
|
|
`map[string]interface{}{"token": "string(optional, from inspect)"}`,
|
|
`this.token = token;`,
|
|
`// AppSecret: "appsecret:<appId>"`,
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("source code non-secret literals should not be credential findings: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsCredentialLikePublicPlaceholders(t *testing.T) {
|
|
got := ScanFile("fixtures/placeholders.md", []byte(strings.Join([]string{
|
|
`app_secret=***`,
|
|
`{"token":"<wiki_token>"}`,
|
|
`{"token":"Pgrrwvr***********UnRb"}`,
|
|
`"scope_name": "auth:user_access_token:read"`,
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("public placeholders and scope identifiers should not be credential findings: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsPartiallyMaskedCredentialValues(t *testing.T) {
|
|
got := ScanFile("fixtures/config.md", []byte(strings.Join([]string{
|
|
"client_secret=realprefix***realsuffix",
|
|
"client_secret=ab********cd",
|
|
"access_token=ab********cd",
|
|
"refresh_token=realprefix********realsuffix",
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 4 {
|
|
t.Fatalf("partially masked credential findings = %d, want 4: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsDryRunCredentialPlaceholders(t *testing.T) {
|
|
got := ScanFile("fixtures/ci.yml", []byte(strings.Join([]string{
|
|
"LARKSUITE_CLI_APP_SECRET=dry-run",
|
|
"client_secret: dry_run",
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("dry-run credential placeholders should not be credential findings: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsTypedCredentialAssignmentsWithSecretRHS(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
file string
|
|
text string
|
|
}{
|
|
{
|
|
name: "typescript simple secret",
|
|
file: "fixtures/source_secret.ts",
|
|
text: `const clientSecret: string = "real-client-secret-value"`,
|
|
},
|
|
{
|
|
name: "typescript numeric password",
|
|
file: "fixtures/source_secret.ts",
|
|
text: `const password: string = "12345678901234567890"`,
|
|
},
|
|
{
|
|
name: "typescript union secret",
|
|
file: "fixtures/source_secret.ts",
|
|
text: `const clientSecret: string | undefined = "real-client-secret-value"`,
|
|
},
|
|
{
|
|
name: "python simple secret",
|
|
file: "fixtures/source_secret.py",
|
|
text: `self.client_secret: str = "real-client-secret-value"`,
|
|
},
|
|
{
|
|
name: "python union secret",
|
|
file: "fixtures/source_secret.py",
|
|
text: `self.client_secret: str | None = "real-client-secret-value"`,
|
|
},
|
|
{
|
|
name: "python optional secret",
|
|
file: "fixtures/source_secret.py",
|
|
text: `self.client_secret: Optional[str] = "real-client-secret-value"`,
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := ScanFile(tc.file, []byte(tc.text+"\n"))
|
|
if !findingRules(got)["public_content_generic_credential"] {
|
|
t.Fatalf("typed credential assignment should be reported: %#v", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsCredentialShapedSourceCodeLiterals(t *testing.T) {
|
|
githubToken := "ghp_" + "1234567890abcdef1234567890abcdef1234"
|
|
got := ScanFile("fixtures/source_secret.go", []byte(strings.Join([]string{
|
|
`const ClientSecret = "real-client-secret-value"`,
|
|
`const GithubToken = "` + githubToken + `"`,
|
|
`const Password = "12345678901234567890"`,
|
|
`const ClientSecretNumber = "12345678901234567890"`,
|
|
`const ClientSecretFormat = "abc%sdefreal"`,
|
|
`fmt.Println("done"); const ClientSecret = "abc%sdefreal"`,
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 6 {
|
|
t.Fatalf("source code credential-shaped literal findings = %d, want 6: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsPrintfCredentialPlaceholders(t *testing.T) {
|
|
got := ScanFile("fixtures/placeholders.md", []byte(strings.Join([]string{
|
|
"client_secret=%s",
|
|
"access_token=%v",
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("printf placeholders should not be credential findings: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsEllipsisCredentialPlaceholders(t *testing.T) {
|
|
got := ScanFile("fixtures/lark-doc-fetch.md", []byte(strings.Join([]string{
|
|
`<img token="..." url="https://..." width="..." height="..."/>`,
|
|
`<sheet token="..." sheet-id="...">`,
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("ellipsis placeholders should not be credential findings: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsSchemaDottedIdentifiers(t *testing.T) {
|
|
got := ScanFile("fixtures/lark-mail-recall.md", []byte("lark-cli schema mail.user_mailbox.sent_messages.get_recall_detail\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_jwt_like_token" {
|
|
t.Fatalf("schema dotted identifier should not be jwt finding: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsMarkdownDottedAPIIdentifiers(t *testing.T) {
|
|
got := ScanFile("fixtures/mail_api_table.md", []byte(strings.Join([]string{
|
|
"| Method | Permission |",
|
|
"| --- | --- |",
|
|
"| `user_mailbox.sent_messages.get_recall_detail` | `mail:user_mailbox.message:readonly` |",
|
|
"| `user_mailbox.allow_sender.batch_create` | `mail:user_mailbox.message:modify` |",
|
|
"| `user_mailbox.allow_sender.batch_remove` | `mail:user_mailbox.message:modify` |",
|
|
"| `user_mailbox.blocked_sender.batch_create` | `mail:user_mailbox.message:modify` |",
|
|
"| `user_mailbox.blocked_sender.batch_remove` | `mail:user_mailbox.message:modify` |",
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_jwt_like_token" {
|
|
t.Fatalf("markdown dotted API identifier should not be jwt finding: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsNonJWTDottedTaxonomy(t *testing.T) {
|
|
got := ScanFile("docs/api.md", []byte(strings.Join([]string{
|
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
"corehr:employment.international_assignment.custom_field.apaas_id__c:read",
|
|
"user_mailbox.sent_messages.get_recall_detail queries recall detail.",
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_jwt_like_token" {
|
|
t.Fatalf("non-JWT dotted taxonomy should not be jwt finding: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsClientTokenIdempotencyExamples(t *testing.T) {
|
|
got := ScanFile("fixtures/idempotency.md", []byte(strings.Join([]string{
|
|
`{"client_token":"1704067200"}`,
|
|
`{"client_token":"fe599b60-450f-46ff-b2ef-9f6675625b97"}`,
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("client_token idempotency examples should not be credential findings: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsCredentialShapedClientTokenValues(t *testing.T) {
|
|
stripeLike := "sk_" + "live_1234567890abcdef"
|
|
got := ScanFile("fixtures/idempotency.md", []byte(strings.Join([]string{
|
|
`{"client_token":"` + stripeLike + `"}`,
|
|
`{"client_token":"real-client-secret-value"}`,
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 2 {
|
|
t.Fatalf("credential-shaped client_token findings = %d, want 2: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsTokenLikePlaceholderExamples(t *testing.T) {
|
|
got := ScanFile("fixtures/placeholders.md", []byte(strings.Join([]string{
|
|
`{ "block_token": "boardXXXX" }`,
|
|
`{ "resource_token": "doc_token_or_url" }`,
|
|
`{ "token": "canonical_token" }`,
|
|
`{ "target_parent_token": "wikcparent_xxx" }`,
|
|
`{ "mention_token": "<userId>" }`,
|
|
`{ "22-doc_token_xxx": { "objType": 22 } }`,
|
|
`{ "token": "12101..." }`,
|
|
`{ token: .token }`,
|
|
`retry_without_token = 0`,
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("token-like placeholders should not be credential findings: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsCredentialShapedTokenLikePlaceholderValues(t *testing.T) {
|
|
stripeLike := "sk_" + "live_1234567890abcdef"
|
|
got := ScanFile("fixtures/placeholders.md", []byte(strings.Join([]string{
|
|
`{ "resource_token": "` + stripeLike + `" }`,
|
|
`{ "block_token": "real-client-secret-value" }`,
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 2 {
|
|
t.Fatalf("credential-shaped token-like placeholders findings = %d, want 2: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsNonFixtureResourceTokenValues(t *testing.T) {
|
|
got := ScanFile("fixtures/minutes_search_test.go", []byte(`{"token":"minute_real_secret"}`+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("resource-like bare token value should not be credential finding: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsBenignUnquotedTokenFields(t *testing.T) {
|
|
got := ScanFile("docs/config.yaml", []byte(strings.Join([]string{
|
|
"tokens: 128",
|
|
"token_type: bearer",
|
|
"max_tokens: 2000",
|
|
"completion_tokens: 200",
|
|
"prompt_tokens: 100",
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("benign unquoted token field should not be credential finding: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSemanticCandidateRedactsColonAssignmentsWithEqualsInValue(t *testing.T) {
|
|
paddedSecretPrefix := "YWJj" + "ZGVmZ2g"
|
|
paddedSecret := base64PaddedFixture(paddedSecretPrefix)
|
|
text := "private launch plan for internal rollout on Friday\n" +
|
|
"api_" + "secret: " + paddedSecret + "\n" +
|
|
`{"access_` + `token":"` + paddedSecret + `"}` + "\n"
|
|
|
|
got := semanticCandidate("docs/public.md", "file", text, 1)
|
|
if len(got) != 1 {
|
|
t.Fatalf("semantic candidate len = %d, want 1: %#v", len(got), got)
|
|
}
|
|
if strings.Contains(got[0].Excerpt, paddedSecret) || strings.Contains(got[0].Excerpt, paddedSecretPrefix) {
|
|
t.Fatalf("semantic candidate leaked colon assignment with padding: %#v", got[0])
|
|
}
|
|
}
|
|
|
|
func TestSemanticCandidateRedactsEscapedQuoteCredentialValues(t *testing.T) {
|
|
doubleQuotedValue := "abc\\\"def-secret"
|
|
singleQuotedValue := "abc\\'def-secret"
|
|
text := "private launch plan for internal rollout on Friday\n" +
|
|
`{"access_` + `token":"` + doubleQuotedValue + `"}` + "\n" +
|
|
`{'client_` + `secret': '` + singleQuotedValue + `'}` + "\n"
|
|
|
|
got := semanticCandidate("docs/public.md", "file", text, 1)
|
|
if len(got) != 1 {
|
|
t.Fatalf("semantic candidate len = %d, want 1: %#v", len(got), got)
|
|
}
|
|
for _, forbidden := range []string{doubleQuotedValue, singleQuotedValue, "def-secret"} {
|
|
if strings.Contains(got[0].Excerpt, forbidden) {
|
|
t.Fatalf("semantic candidate leaked escaped-quote credential value %q: %#v", forbidden, got[0])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileDoesNotTreatEqualityComparisonAsCredential(t *testing.T) {
|
|
got := ScanFile("docs/example.md", []byte("if token == \"expected\" { return nil }\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("equality comparison should not be credential assignment: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsBearerHeaderFinding(t *testing.T) {
|
|
got := ScanFile("docs/auth.md", []byte("Authorization: Bearer abcdefghijklmnopqrstuvwxyz\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_bearer_header" {
|
|
if item.Action != "REJECT" || item.File != "docs/auth.md" || item.Line != 1 || item.Source != "file" {
|
|
t.Fatalf("bearer finding attribution = %#v", item)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("missing bearer finding: %#v", got)
|
|
}
|
|
|
|
func TestScanFileDetectsJSONBearerHeaders(t *testing.T) {
|
|
token := "abcdefghijklmnopqrstuvwxyz"
|
|
got := ScanFile("docs/auth.json", []byte(strings.Join([]string{
|
|
`{"Authorization":"Bearer ` + token + `"}`,
|
|
`{"headers":{"Authorization":"Bearer ` + token + `"}}`,
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule != "public_content_bearer_header" {
|
|
continue
|
|
}
|
|
count++
|
|
if item.Action != "REJECT" || item.File != "docs/auth.json" || item.Source != "file" {
|
|
t.Fatalf("bearer finding attribution = %#v", item)
|
|
}
|
|
if strings.Contains(item.Excerpt, token) {
|
|
t.Fatalf("bearer finding leaked token: %#v", item)
|
|
}
|
|
}
|
|
if count != 2 {
|
|
t.Fatalf("JSON bearer findings = %d, want 2: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsBearerHeaderPlaceholders(t *testing.T) {
|
|
got := ScanFile("docs/auth.md", []byte(strings.Join([]string{
|
|
"Authorization: Bearer YOUR_ACCESS_TOKEN",
|
|
`{"Authorization":"Bearer ACCESS_TOKEN_HERE"}`,
|
|
"Authorization: Bearer <access-token>",
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_bearer_header" {
|
|
t.Fatalf("bearer placeholder should not be bearer finding: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSemanticCandidateRedactsJSONBearerHeaders(t *testing.T) {
|
|
token := "abcdefghijklmnopqrstuvwxyz"
|
|
text := "private launch plan for internal rollout on Friday\n" +
|
|
`{"headers":{"Authorization":"Bearer ` + token + `"}}` + "\n"
|
|
|
|
got := semanticCandidate("docs/public.md", "file", text, 1)
|
|
if len(got) != 1 {
|
|
t.Fatalf("semantic candidate len = %d, want 1: %#v", len(got), got)
|
|
}
|
|
if strings.Contains(got[0].Excerpt, token) {
|
|
t.Fatalf("semantic candidate leaked JSON bearer token: %#v", got[0])
|
|
}
|
|
if !strings.Contains(got[0].Excerpt, "Authorization: Bearer <redacted>") {
|
|
t.Fatalf("semantic candidate should redact JSON bearer header, got %#v", got[0])
|
|
}
|
|
}
|
|
|
|
func TestSemanticCandidateKeepsNonJWTDottedTaxonomy(t *testing.T) {
|
|
text := "private launch plan for internal rollout on Friday\n" +
|
|
"Supported MIME type: application/vnd.openxmlformats-officedocument.presentationml.presentation\n"
|
|
|
|
got := semanticCandidate("docs/public.md", "file", text, 1)
|
|
if len(got) != 1 {
|
|
t.Fatalf("semantic candidate len = %d, want 1: %#v", len(got), got)
|
|
}
|
|
if strings.Contains(got[0].Excerpt, "<jwt-like-token>") {
|
|
t.Fatalf("semantic candidate should not redact non-JWT dotted taxonomy: %#v", got[0])
|
|
}
|
|
if !strings.Contains(got[0].Excerpt, "application/vnd.openxmlformats-officedocument.presentationml.presentation") {
|
|
t.Fatalf("semantic candidate should keep non-JWT dotted taxonomy, got %#v", got[0])
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsCommonProvenanceMarkers(t *testing.T) {
|
|
text := strings.Join([]string{
|
|
"Generated with automated code assistant",
|
|
"Co-authored-by: automated-code-assistant <bot@example.invalid>",
|
|
"🤖 generated by automation",
|
|
}, "\n")
|
|
got := ScanFile("docs/public.md", []byte(text))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_provenance_marker" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 3 {
|
|
t.Fatalf("provenance marker count = %d, want 3: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsHumanCoAuthorTrailer(t *testing.T) {
|
|
got := ScanFile("docs/public.md", []byte(strings.Join([]string{
|
|
"Co-authored-by: Jane Doe <jane@example.invalid>",
|
|
"Co-authored-by: Alice Abbot <abbot@example.invalid>",
|
|
}, "\n")))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_provenance_marker" {
|
|
t.Fatalf("human co-author trailer should not be blocked: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsPercentWrappedPlaceholder(t *testing.T) {
|
|
got := ScanFile("docs/config.md", []byte("client_secret=%CLIENT_SECRET%\n"))
|
|
if len(got) != 0 {
|
|
t.Fatalf("percent-wrapped placeholder produced findings: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileAllowsConventionalCredentialPlaceholders(t *testing.T) {
|
|
got := ScanFile("docs/config.md", []byte(strings.Join([]string{
|
|
"client_secret: YOUR_CLIENT_SECRET",
|
|
"api_key: YOUR_API_KEY",
|
|
"password: YOUR_PASSWORD",
|
|
"access_token: ACCESS_TOKEN_HERE",
|
|
}, "\n")+"\n"))
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
t.Fatalf("conventional credential placeholder should not be credential finding: %#v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsCredentialShapedPlaceholderLookalikes(t *testing.T) {
|
|
stripeLike := "sk_" + "live_1234567890abcdef"
|
|
got := ScanFile("docs/config.md", []byte(strings.Join([]string{
|
|
"client_secret: " + stripeLike + "_HERE",
|
|
"api_key: YOUR_" + stripeLike,
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 2 {
|
|
t.Fatalf("credential-shaped placeholder lookalike findings = %d, want 2: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func TestScanFileDetectsPercentWrappedCredentialValues(t *testing.T) {
|
|
stripeLike := "sk_" + "live_1234567890abcdef"
|
|
patLike := "gh" + "p_1234567890abcdef1234567890abcdef1234"
|
|
got := ScanFile("docs/config.md", []byte(strings.Join([]string{
|
|
"CLIENT_SECRET=%" + stripeLike + "%",
|
|
"GITHUB_TOKEN=%" + patLike + "%",
|
|
"TOKEN=%real-secret-token-value%",
|
|
}, "\n")+"\n"))
|
|
var count int
|
|
for _, item := range got {
|
|
if item.Rule == "public_content_generic_credential" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 3 {
|
|
t.Fatalf("percent-wrapped credential findings = %d, want 3: %#v", count, got)
|
|
}
|
|
}
|
|
|
|
func findingRules(items []Finding) map[string]bool {
|
|
out := map[string]bool{}
|
|
for _, item := range items {
|
|
out[item.Rule] = true
|
|
}
|
|
return out
|
|
}
|
|
|
|
func jwtFixture(subject string) string {
|
|
return strings.Join([]string{
|
|
jwtHeaderFixture(),
|
|
"eyJzdWIiOiJ" + subject + "In0",
|
|
"signature" + "part",
|
|
}, ".")
|
|
}
|
|
|
|
func jwtHeaderFixture() string {
|
|
return "eyJhbGciOiJI" + "UzI1NiJ9"
|
|
}
|
|
|
|
func base64PaddedFixture(prefix string) string {
|
|
return prefix + "=="
|
|
}
|