diff --git a/internal/redaction/betterleaks.go b/internal/redaction/betterleaks.go index 439952cb0..5342f22ff 100644 --- a/internal/redaction/betterleaks.go +++ b/internal/redaction/betterleaks.go @@ -79,7 +79,7 @@ var rulesConfig string // betterleaks.toml, the betterleaks version, or the handling of findings in // this file changes what gets redacted, so that policies can tell placeholders // from different rulesets apart. Version 1 is the plain default ruleset. -const RulesetVersion = 2 +const RulesetVersion = 3 var ( defaultScanner = sync.OnceValues(newBetterleaksScanner) diff --git a/internal/redaction/betterleaks.toml b/internal/redaction/betterleaks.toml index 2e10355b8..a02a0fdec 100644 --- a/internal/redaction/betterleaks.toml +++ b/internal/redaction/betterleaks.toml @@ -58,6 +58,19 @@ regexTarget = "match" # redacted. regexes = ['''(?i)://(?:localhost|[0-9]{1,3}(?:\.[0-9]{1,3}){3}|[a-z0-9-]+(?:\.[a-z0-9-]+)+):[0-9]{1,5}@'''] +[[allowlists]] +description = "A port followed by a line break read as the password of a URI, such as a port followed by more lines up to an email address" +targetRules = ["generic-credential-uri"] +# The password stops at whitespace in source text, but in the JSON-encoded text +# a line break is the escape \n, so the match runs on into later lines up to +# the next @. Only a password that is a port, and then a line break, is kept: a +# real password followed by a line break is still redacted. A real URI on a +# later line has its own ://, which the password cannot run through, so it is +# still found. Only an escape counts: an odd run of backslashes before the n, r +# or t. A \n written in source code reaches the scanner as \\n and is not a +# line break. +regexes = ['''^[0-9]{1,5}(?:\\\\)*\\[nrt]'''] + [[allowlists]] description = "An elided password in documentation, such as http://user:…@example.com" targetRules = ["generic-credential-uri"] diff --git a/internal/redaction/precision_test.go b/internal/redaction/precision_test.go index ef0d2b292..06a99169a 100644 --- a/internal/redaction/precision_test.go +++ b/internal/redaction/precision_test.go @@ -139,6 +139,15 @@ func TestRulesConfigKeepsNonSecrets(t *testing.T) { text: "issuer: http://localhost:8000@chainloop.local\n", wantDefaultRule: ruleCredentialURI, }, + { + // The file content of a Write tool call. The scanner sees it + // JSON-encoded, so the password of the match ran from the port, + // through the escaped line breaks, to the user part of an email + // address on a later line. + name: "uri password running through line breaks to an email address", + text: "API_URL=http://localhost:48002\nCAS_ENDPOINT=localhost:49001\nTEST_EMAIL=" + sampleEmail + "\n", + wantDefaultRule: ruleCredentialURI, + }, } for _, tc := range testCases { @@ -213,6 +222,19 @@ func TestRulesConfigStillRedactsSecrets(t *testing.T) { secret: samplePassword, wantRule: ruleCredentialURI, }, + { + name: "password in a connection uri on the line after a host and port", + text: "API_URL=http://localhost:48002\nDSN=postgres://admin:" + samplePassword + "@db.internal:5432/app\n", + secret: samplePassword, + wantRule: ruleCredentialURI, + keep: "localhost:48002", + }, + { + name: "password in a uri followed by a line break and an email address", + text: "API_URL=http://admin:" + samplePassword + "\nTEST_EMAIL=" + sampleEmail + "\n", + secret: samplePassword, + wantRule: ruleCredentialURI, + }, } for _, tc := range testCases { diff --git a/pkg/attestation/crafter/materials/aicodingsession/redact_test.go b/pkg/attestation/crafter/materials/aicodingsession/redact_test.go index 471c3ccfc..cd72cd1e6 100644 --- a/pkg/attestation/crafter/materials/aicodingsession/redact_test.go +++ b/pkg/attestation/crafter/materials/aicodingsession/redact_test.go @@ -50,6 +50,15 @@ const ( fixtureAnthropicKey = "sk-ant-api03-sT5wsx9DwmaHZDL0dUWKNhAhULxa35sUzyLFK9" + "5QBTZMDJTYn8p0J7ZQbwpYGYCQeW5eXAAGtVSmhp7UO9vxHJtSBC0xpAA" fixtureRepository = "https://oauth2:" + fixtureGitHubPAT + "@github.com/example/repo.git" + + // The digests of testdata/session-fp-shaped.json are not secrets, but a + // high-entropy hex string in a file is flagged by the repository scan. + fixtureDigestA = "9f8e7d6c5b4a39281706f5e4d3c2b1a0" + "f9e8d7c6f9e8d7c6b5a4938271605f4e" + fixtureDigestB = "9f8e7d6c5b4a39281706f5e4d3c2b1a0" + "f9e8d7c69f8e7d6c5b4a39281706f5e4" + // The UUID and commit ids are values the generic rules must leave alone. + fixtureUUID = "3bf79921-3c03-81b6-" + "afff-cb246849866f" + fixtureCommitStart = "9f8e7d6c5b4a39281706f5e4d3c2b1a0" + "f9e8d7c6" + fixtureCommitEnd = "e25fa85e1c4b7a9d3f082165" + "0cd7e4b39a1f8c2d" ) // fixtureSecrets maps each placeholder in the session fixtures to the value the @@ -61,6 +70,11 @@ var fixtureSecrets = map[string]string{ "__GITHUB_PAT__": fixtureGitHubPAT, "__ANTHROPIC_API_KEY__": fixtureAnthropicKey, "__GIT_REPOSITORY_WITH_CREDENTIALS__": fixtureRepository, + "__SHA256_DIGEST_A__": fixtureDigestA, + "__SHA256_DIGEST_B__": fixtureDigestB, + "__SESSION_UUID__": fixtureUUID, + "__COMMIT_START__": fixtureCommitStart, + "__COMMIT_END__": fixtureCommitEnd, } // readFixture loads a session fixture with its credential placeholders resolved. diff --git a/pkg/attestation/crafter/materials/aicodingsession/testdata/session-fp-shaped.json b/pkg/attestation/crafter/materials/aicodingsession/testdata/session-fp-shaped.json index bc47ac637..7958886bb 100644 --- a/pkg/attestation/crafter/materials/aicodingsession/testdata/session-fp-shaped.json +++ b/pkg/attestation/crafter/materials/aicodingsession/testdata/session-fp-shaped.json @@ -8,7 +8,7 @@ "version": "2.1.83" }, "session": { - "id": "3bf79921-3c03-81b6-afff-cb246849866f", + "id": "__SESSION_UUID__", "slug": "memoized-purring-valley", "started_at": "2026-03-25T15:10:49.161Z", "ended_at": "2026-03-25T16:59:14.988Z", @@ -18,8 +18,8 @@ "repository": "git@github.com:chainloop-dev/chainloop.git", "branch": "jiparis/pfm-6984-redact-secrets-in-session", "work_dir": "/Users/jiparis/projects/chainloop", - "commit_start": "9f8e7d6c5b4a39281706f5e4d3c2b1a0f9e8d7c6", - "commit_end": "e25fa85e1c4b7a9d3f0821650cd7e4b39a1f8c2d", + "commit_start": "__COMMIT_START__", + "commit_end": "__COMMIT_END__", "commits": [ "9f8e7d6 perf(crafter): de-duplicate AccessChk security descriptors", "e25fa85 Bump Helm Chart and Dagger Version" @@ -32,7 +32,7 @@ "lines_removed": 87, "files": [ {"path": "app/controlplane/internal/service/attestation.go", "status": "modified", "attribution": "ai"}, - {"path": "pkg/attestation/crafter/materials/aicodingsession/redact.go", "status": "created", "attribution": "ai", "session_ids": ["3bf79921-3c03-81b6-afff-cb246849866f"]} + {"path": "pkg/attestation/crafter/materials/aicodingsession/redact.go", "status": "created", "attribution": "ai", "session_ids": ["__SESSION_UUID__"]} ] }, "model": { @@ -72,7 +72,7 @@ "main": [ { "type": "user", - "message": {"role": "user", "content": "the sha256 digest is 9f8e7d6c5b4a39281706f5e4d3c2b1a0f9e8d7c6f9e8d7c6b5a4938271605f4e"}, + "message": {"role": "user", "content": "the sha256 digest is __SHA256_DIGEST_A__"}, "timestamp": "2026-03-25T15:13:37.330Z" }, { @@ -82,7 +82,7 @@ }, { "type": "user", - "message": {"role": "user", "content": "Pushed the attestation.\n digest: sha256:9f8e7d6c5b4a39281706f5e4d3c2b1a0f9e8d7c69f8e7d6c5b4a39281706f5e4\n access sha256:9f8e7d6c5b4a39281706f5e4d3c2b1a0f9e8d7c69f8e7d6c5b4a39281706f5e4\nworkflow key: 3bf79921-3c03-81b6-afff-cb246849866f\nidempotency_key=3bf79921-3c03-81b6-afff-cb246849866f\n"}, + "message": {"role": "user", "content": "Pushed the attestation.\n digest: sha256:__SHA256_DIGEST_B__\n access sha256:__SHA256_DIGEST_B__\nworkflow key: __SESSION_UUID__\nidempotency_key=__SESSION_UUID__\n"}, "timestamp": "2026-03-25T15:14:02.000Z" }, { @@ -109,6 +109,11 @@ "type": "user", "message": {"role": "user", "content": "119\tconst pat = \"__GITHUB_PAT__\" // gitleaks:allow\n120\twantURI := \"https://tracker.example.com/issue/1?token=\" + pat\n"}, "timestamp": "2026-03-25T15:14:07.000Z" + }, + { + "type": "assistant", + "message": {"role": "assistant", "content": [{"type": "tool_use", "name": "Write", "input": {"file_path": "/work/e2e/.env", "content": "ENVIRONMENT=local\nBASE_URL=http://localhost:43000\nAPI_URL=http://localhost:48002\nCAS_ENDPOINT=localhost:49001\nCHAINLOOP_API_INSECURE=true\nTEST_EMAIL=john@chainloop.local\n"}}]}, + "timestamp": "2026-03-25T15:14:08.000Z" } ] },