Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/redaction/betterleaks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions internal/redaction/betterleaks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
22 changes: 22 additions & 0 deletions internal/redaction/precision_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
14 changes: 14 additions & 0 deletions pkg/attestation/crafter/materials/aicodingsession/redact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand All @@ -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": {
Expand Down Expand Up @@ -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"
},
{
Expand All @@ -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"
},
{
Expand All @@ -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"
}
]
},
Expand Down
Loading