|
| 1 | +package io.kestra.plugin.soda; |
| 2 | + |
| 3 | +import java.lang.reflect.Method; |
| 4 | +import java.util.LinkedHashMap; |
| 5 | +import java.util.Map; |
| 6 | + |
| 7 | +import org.junit.jupiter.api.Test; |
| 8 | + |
| 9 | +import static org.hamcrest.MatcherAssert.assertThat; |
| 10 | +import static org.hamcrest.Matchers.*; |
| 11 | + |
| 12 | +/** |
| 13 | + * Fast, dependency-free unit tests for the security-hardening helpers introduced in the |
| 14 | + * 2026-06-30 audit remediation PR: shell-quoting of pip requirements ({@link AbstractSoda}) |
| 15 | + * and sensitive-value scrubbing of the rendered configuration ({@link Scan}). Both helpers are |
| 16 | + * {@code private static} pure functions, so they are exercised directly via reflection rather than |
| 17 | + * through the Docker/BigQuery integration path used by {@code ScanTest}. |
| 18 | + */ |
| 19 | +class SecurityHardeningTest { |
| 20 | + |
| 21 | + private static String shellQuote(String value) throws Exception { |
| 22 | + Method method = AbstractSoda.class.getDeclaredMethod("shellQuote", String.class); |
| 23 | + method.setAccessible(true); |
| 24 | + return (String) method.invoke(null, value); |
| 25 | + } |
| 26 | + |
| 27 | + @SuppressWarnings("unchecked") |
| 28 | + private static Map<String, Object> scrub(Map<String, Object> input) throws Exception { |
| 29 | + Method method = Scan.class.getDeclaredMethod("scrubSensitiveValues", Map.class); |
| 30 | + method.setAccessible(true); |
| 31 | + return (Map<String, Object>) method.invoke(null, input); |
| 32 | + } |
| 33 | + |
| 34 | + // --- shellQuote ------------------------------------------------------------------------- |
| 35 | + |
| 36 | + @Test |
| 37 | + void shellQuote_wrapsPlainRequirement() throws Exception { |
| 38 | + assertThat(shellQuote("soda-core-postgres"), is("'soda-core-postgres'")); |
| 39 | + } |
| 40 | + |
| 41 | + @Test |
| 42 | + void shellQuote_preservesVersionOperatorsLiterally() throws Exception { |
| 43 | + // `>` and `<` used to be interpreted as shell redirection when interpolated bare. |
| 44 | + assertThat(shellQuote("soda-core[postgres]>=3.0,<4.0"), is("'soda-core[postgres]>=3.0,<4.0'")); |
| 45 | + } |
| 46 | + |
| 47 | + @Test |
| 48 | + void shellQuote_neutralizesShellMetacharacters() throws Exception { |
| 49 | + // Command-injection attempt: the `;` and everything after must stay inside the quotes. |
| 50 | + String quoted = shellQuote("pkg; rm -rf /"); |
| 51 | + assertThat(quoted, is("'pkg; rm -rf /'")); |
| 52 | + // Nothing escapes the single-quoted span, so no bare metacharacter is exposed to the shell. |
| 53 | + assertThat(quoted.startsWith("'"), is(true)); |
| 54 | + assertThat(quoted.endsWith("'"), is(true)); |
| 55 | + } |
| 56 | + |
| 57 | + @Test |
| 58 | + void shellQuote_escapesEmbeddedSingleQuote() throws Exception { |
| 59 | + // The one character that cannot live inside single quotes is escaped as '\'' . |
| 60 | + assertThat(shellQuote("a'b"), is("'a'\\''b'")); |
| 61 | + } |
| 62 | + |
| 63 | + @Test |
| 64 | + void shellQuote_preservesEnvironmentMarkers() throws Exception { |
| 65 | + assertThat( |
| 66 | + shellQuote("pkg; python_version >= \"3.8\""), |
| 67 | + is("'pkg; python_version >= \"3.8\"'") |
| 68 | + ); |
| 69 | + } |
| 70 | + |
| 71 | + // --- scrubSensitiveValues --------------------------------------------------------------- |
| 72 | + |
| 73 | + @Test |
| 74 | + void scrub_redactsTopLevelSensitiveKeys() throws Exception { |
| 75 | + Map<String, Object> input = new LinkedHashMap<>(); |
| 76 | + input.put("password", "hunter2"); |
| 77 | + input.put("host", "db.internal"); |
| 78 | + |
| 79 | + Map<String, Object> result = scrub(input); |
| 80 | + |
| 81 | + assertThat(result.get("password"), is("******")); |
| 82 | + assertThat(result.get("host"), is("db.internal")); |
| 83 | + } |
| 84 | + |
| 85 | + @Test |
| 86 | + void scrub_recursesIntoNestedMaps() throws Exception { |
| 87 | + Map<String, Object> connection = new LinkedHashMap<>(); |
| 88 | + connection.put("password", "s3cret"); |
| 89 | + connection.put("port", 5432); |
| 90 | + |
| 91 | + Map<String, Object> input = new LinkedHashMap<>(); |
| 92 | + input.put("connection", connection); |
| 93 | + |
| 94 | + Map<String, Object> result = scrub(input); |
| 95 | + |
| 96 | + @SuppressWarnings("unchecked") |
| 97 | + Map<String, Object> scrubbedConnection = (Map<String, Object>) result.get("connection"); |
| 98 | + assertThat(scrubbedConnection.get("password"), is("******")); |
| 99 | + // Non-sensitive leaf values keep their original value and type. |
| 100 | + assertThat(scrubbedConnection.get("port"), is(5432)); |
| 101 | + } |
| 102 | + |
| 103 | + @Test |
| 104 | + void scrub_redactsSecretKeyVariants() throws Exception { |
| 105 | + Map<String, Object> input = new LinkedHashMap<>(); |
| 106 | + input.put("api_key", "abc"); |
| 107 | + input.put("token", "xyz"); |
| 108 | + input.put("account_info_json", "{...}"); |
| 109 | + input.put("type", "postgres"); |
| 110 | + |
| 111 | + Map<String, Object> result = scrub(input); |
| 112 | + |
| 113 | + assertThat(result.get("api_key"), is("******")); |
| 114 | + assertThat(result.get("token"), is("******")); |
| 115 | + assertThat(result.get("account_info_json"), is("******")); |
| 116 | + assertThat(result.get("type"), is("postgres")); |
| 117 | + } |
| 118 | +} |
0 commit comments