fix: smarter secret masking based on secret length (#8629)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-03-30 19:59:08 +00:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 3876902a7b
commit bfc2aefdb8
2 changed files with 18 additions and 10 deletions
@@ -215,8 +215,8 @@ export async function main() {
"scenario 1: secret value leaked in logs\nLogs:\n{logs1}"
);
assert!(
logs1.contains("The secret value is: alp*****"),
"scenario 1: expected masked output with first 3 chars\nLogs:\n{logs1}"
logs1.contains("The secret value is: alp*****k2m"),
"scenario 1: expected masked output with first 3 + last 3 chars\nLogs:\n{logs1}"
);
assert!(
logs1.contains("[windmill] secret value was masked for security reasons, use string transformations to display full value"),
@@ -277,11 +277,11 @@ export async function main() {
"scenario 3: secret2 leaked\nLogs:\n{logs3}"
);
assert!(
logs3.contains("secret1=alp*****"),
logs3.contains("secret1=alp*****k2m"),
"scenario 3: secret1 not masked\nLogs:\n{logs3}"
);
assert!(
logs3.contains("secret2=bet*****"),
logs3.contains("secret2=bet*****n3p"),
"scenario 3: secret2 not masked\nLogs:\n{logs3}"
);
@@ -309,7 +309,7 @@ export async function main() {
"scenario 4: secret leaked mid-string\nLogs:\n{logs4}"
);
assert!(
logs4.contains("token=alp*****&user=bob&format=json"),
logs4.contains("token=alp*****k2m&user=bob&format=json"),
"scenario 4: mid-string masking failed\nLogs:\n{logs4}"
);
@@ -338,7 +338,7 @@ export async function main() {
!logs5.contains(secret2),
"scenario 5: secret leaked\nLogs:\n{logs5}"
);
let mask_count = logs5.matches("bet*****").count();
let mask_count = logs5.matches("bet*****n3p").count();
assert!(
mask_count >= 3,
"scenario 5: expected >= 3 masked occurrences, found {mask_count}\nLogs:\n{logs5}"
@@ -374,7 +374,7 @@ export async function main() {
"scenario 6: encrypted password leaked\nLogs:\n{logs6}"
);
assert!(
logs6.contains("password is: enc*****"),
logs6.contains("password is: enc*****q5r"),
"scenario 6: encrypted password not masked\nLogs:\n{logs6}"
);
@@ -403,7 +403,7 @@ export async function main() {
"scenario 7: resource secret leaked\nLogs:\n{logs7}"
);
assert!(
logs7.contains("db password: res*****"),
logs7.contains("db password: res*****7t2"),
"scenario 7: resource secret not masked\nLogs:\n{logs7}"
);
// Non-secret field should remain visible
@@ -88,8 +88,16 @@ pub fn snapshot(job_id: &Uuid) -> Option<MaskSnapshot> {
let replacements: Vec<String> = sorted
.iter()
.map(|s| {
let prefix: String = s.chars().take(3).collect();
format!("{}*****", prefix)
let char_count = s.chars().count();
if char_count > 20 {
let prefix: String = s.chars().take(3).collect();
let suffix: String = s.chars().skip(char_count - 3).collect();
format!("{}*****{}", prefix, suffix)
} else {
let first: String = s.chars().take(1).collect();
let last: String = s.chars().skip(char_count - 1).collect();
format!("{}*****{}", first, last)
}
})
.collect();