Skip to content

Commit a96840b

Browse files
jymaireclaude
andcommitted
fix(security): prefer over-redaction for config secrets
Match sensitive keys by substring again so any key containing a sensitive fragment (keyfile, keyspace, client_secret, ...) is redacted, favoring safety over occasional masking of benign values. Update test accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a20b53d commit a96840b

2 files changed

Lines changed: 24 additions & 56 deletions

File tree

src/main/java/io/kestra/plugin/soda/Scan.java

Lines changed: 13 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -110,29 +110,22 @@ public class Scan extends AbstractSoda implements RunnableTask<Scan.Output> {
110110
private static final String REDACTED = "******";
111111

112112
/**
113-
* Whole-token markers: a key is sensitive when one of its tokens (split on separators and
114-
* camelCase boundaries) exactly equals one of these. Matching on tokens rather than substrings
115-
* avoids over-redacting benign keys such as {@code keyspace}, {@code sortkey} or {@code author}.
116-
* Note {@code key} as a token already covers {@code api_key}, {@code access_key},
117-
* {@code private_key}, {@code apiKey}, etc.
113+
* Sensitive fragments matched as substrings of the normalized (alphanumeric-only, lowercased)
114+
* key. Substring matching deliberately errs toward over-redaction — any key merely containing
115+
* one of these (e.g. {@code keyfile}, {@code keyspace}, {@code client_secret}) is redacted — so
116+
* that a secret is never leaked into task Output at the cost of occasionally masking a benign
117+
* value. {@code key} already covers {@code api_key}, {@code access_key}, {@code private_key}, etc.
118118
*/
119-
private static final Set<String> SENSITIVE_KEY_TOKENS = Set.of(
119+
private static final Set<String> SENSITIVE_KEY_PATTERNS = Set.of(
120120
"password", "passwd", "pwd",
121-
"secret", "secrets",
122-
"token", "tokens",
123-
"key", "keys",
124-
"credential", "credentials",
121+
"secret",
122+
"token",
123+
"key",
124+
"credential",
125+
"accountinfojson",
125126
"auth"
126127
);
127128

128-
/**
129-
* Compound key names matched against the fully normalized (alphanumeric-only, lowercased) key,
130-
* for sensitive keys whose individual tokens are not themselves secret markers.
131-
*/
132-
private static final Set<String> SENSITIVE_NORMALIZED_KEYS = Set.of(
133-
"accountinfojson"
134-
);
135-
136129
@Schema(
137130
title = "SodaCL checks definition",
138131
description = "Required map rendered to `checks.yml` and executed against the `kestra` data source. Follow SodaCL syntax; failing checks mark the task accordingly."
@@ -264,42 +257,15 @@ private static boolean isSensitiveKey(String key) {
264257
}
265258

266259
String normalized = key.toLowerCase().replaceAll("[^a-z0-9]", "");
267-
if (SENSITIVE_NORMALIZED_KEYS.contains(normalized)) {
268-
return true;
269-
}
270-
271-
for (String token : tokenize(key)) {
272-
if (SENSITIVE_KEY_TOKENS.contains(token)) {
260+
for (String pattern : SENSITIVE_KEY_PATTERNS) {
261+
if (normalized.contains(pattern)) {
273262
return true;
274263
}
275264
}
276265

277266
return false;
278267
}
279268

280-
/**
281-
* Splits a key into lowercase tokens on non-alphanumeric separators and camelCase boundaries,
282-
* so {@code account_info_json}, {@code apiKey} and {@code APIKey} all yield word-level tokens.
283-
*/
284-
private static List<String> tokenize(String key) {
285-
String spaced = key
286-
.replaceAll("([A-Z]+)([A-Z][a-z])", "$1 $2")
287-
.replaceAll("([a-z0-9])([A-Z])", "$1 $2")
288-
.replaceAll("[^a-zA-Z0-9]+", " ")
289-
.trim();
290-
291-
List<String> tokens = new ArrayList<>();
292-
if (spaced.isEmpty()) {
293-
return tokens;
294-
}
295-
296-
for (String token : spaced.split("\\s+")) {
297-
tokens.add(token.toLowerCase());
298-
}
299-
300-
return tokens;
301-
}
302-
303269
protected ScanResult parseResult(RunContext runContext, ScriptOutput output) throws IOException {
304270
ScanResult scanResult = JacksonMapper.ofJson(false).readValue(
305271
runContext.storage().getFile(output.getOutputFiles().get("result.json")),

src/test/java/io/kestra/plugin/soda/SecurityHardeningTest.java

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -148,18 +148,20 @@ void scrub_recursesIntoLists() throws Exception {
148148
}
149149

150150
@Test
151-
void scrub_doesNotOverRedactBenignKeys() throws Exception {
152-
// These all contain a sensitive pattern as a substring but no sensitive whole token,
153-
// so their values must be preserved.
151+
void scrub_redactsAnyKeyContainingSensitiveSubstring() throws Exception {
152+
// Deliberate over-redaction: matching is by substring, so any key merely containing a
153+
// sensitive fragment is masked rather than risk leaking a secret.
154154
Map<String, Object> input = new LinkedHashMap<>();
155-
input.put("keyspace", "analytics"); // contains "key"
156-
input.put("sortkey", "created_at"); // contains "key"
157-
input.put("author", "alice"); // contains "auth"
155+
input.put("keyfile", "/path/to/sa.json");
156+
input.put("keyspace", "analytics");
157+
input.put("sortkey", "created_at");
158+
input.put("author", "alice");
158159

159160
Map<String, Object> result = scrub(input);
160161

161-
assertThat(result.get("keyspace"), is("analytics"));
162-
assertThat(result.get("sortkey"), is("created_at"));
163-
assertThat(result.get("author"), is("alice"));
162+
assertThat(result.get("keyfile"), is("******"));
163+
assertThat(result.get("keyspace"), is("******"));
164+
assertThat(result.get("sortkey"), is("******"));
165+
assertThat(result.get("author"), is("******"));
164166
}
165167
}

0 commit comments

Comments
 (0)