From 6e937b6721170043abb4810f3e7fb6d116f6ca4f Mon Sep 17 00:00:00 2001 From: kekubhai Date: Wed, 9 Sep 2026 13:41:09 +0530 Subject: [PATCH 1/5] Update wrong-secrets-configuration.yaml to change category from 'docker_tech' to 'ai' and add Challenge 71 configuration. --- .../challenges/docker/Challenge71.java | 51 ++++ .../docker/Challenge71Controller.java | 45 ++++ .../challenge-71/challenge-71.snippet | 38 +++ .../challenge-71/codex-session-transcript.md | 236 ++++++++++++++++++ .../resources/explanations/challenge71.adoc | 14 ++ .../explanations/challenge71_hint.adoc | 3 + .../explanations/challenge71_reason.adoc | 32 +++ .../wrong-secrets-configuration.yaml | 15 +- .../docker/Challenge71ControllerTest.java | 41 +++ .../challenges/docker/Challenge71Test.java | 79 ++++++ 10 files changed, 553 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71.java create mode 100644 src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Controller.java create mode 100644 src/main/resources/challenges/challenge-71/challenge-71.snippet create mode 100644 src/main/resources/challenges/challenge-71/codex-session-transcript.md create mode 100644 src/main/resources/explanations/challenge71.adoc create mode 100644 src/main/resources/explanations/challenge71_hint.adoc create mode 100644 src/main/resources/explanations/challenge71_reason.adoc create mode 100644 src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71ControllerTest.java create mode 100644 src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Test.java diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71.java new file mode 100644 index 000000000..b84c56298 --- /dev/null +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71.java @@ -0,0 +1,51 @@ +package org.owasp.wrongsecrets.challenges.docker; + +import static org.owasp.wrongsecrets.Challenges.ErrorResponses.FILE_MOUNT_ERROR; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.regex.Pattern; +import lombok.extern.slf4j.Slf4j; +import org.owasp.wrongsecrets.challenges.FixedAnswerChallenge; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Component; + +/** + * Challenge based on a secret leaked inside a real AI coding-agent transcript. The transcript + * captures a Codex session where the agent reads a staging configuration file containing a deploy + * token, exposing it in the session output. This illustrates how AI coding-agent transcripts can + * inadvertently retain sensitive information that may later be committed, shared, or exposed. + */ +@Slf4j +@Component +public class Challenge71 extends FixedAnswerChallenge { + + private static final Pattern DEPLOY_TOKEN_PATTERN = + Pattern.compile("DEPLOY_TOKEN=([A-Za-z0-9_]+)"); + + private final Resource transcriptFile; + + public Challenge71( + @Value("classpath:challenges/challenge-71/codex-session-transcript.md") + Resource transcriptFile) { + this.transcriptFile = transcriptFile; + } + + @Override + public String getAnswer() { + try { + var transcriptContent = + transcriptFile.getContentAsString(StandardCharsets.UTF_8); + var matcher = DEPLOY_TOKEN_PATTERN.matcher(transcriptContent); + if (!matcher.find()) { + log.warn("Could not find the deploy token in the Codex transcript of challenge 71"); + return FILE_MOUNT_ERROR; + } + return matcher.group(1); + } catch (IOException e) { + log.warn("Exception while reading the Codex transcript of challenge 71", e); + return FILE_MOUNT_ERROR; + } + } +} diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Controller.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Controller.java new file mode 100644 index 000000000..bfade683a --- /dev/null +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Controller.java @@ -0,0 +1,45 @@ +package org.owasp.wrongsecrets.challenges.docker; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Hosts the Codex session transcript of challenge 71 straight from the resource folder, so + * participants can read the transcript the same way an analyst would when reviewing agent output. + */ +@Slf4j +@RestController +public class Challenge71Controller { + + private static final MediaType MARKDOWN = + new MediaType("text", "markdown", StandardCharsets.UTF_8); + + private final Resource transcriptFile; + + public Challenge71Controller( + @Value("classpath:challenges/challenge-71/codex-session-transcript.md") + Resource transcriptFile) { + this.transcriptFile = transcriptFile; + } + + /** Returns the raw Codex session transcript for challenge 71. */ + @GetMapping("/challenges/challenge-71/codex-session-transcript.md") + public ResponseEntity codexTranscript() { + try { + return ResponseEntity.ok() + .contentType(MARKDOWN) + .body(transcriptFile.getContentAsString(StandardCharsets.UTF_8)); + } catch (IOException e) { + log.warn("Unable to serve the Codex transcript of challenge 71", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } +} diff --git a/src/main/resources/challenges/challenge-71/challenge-71.snippet b/src/main/resources/challenges/challenge-71/challenge-71.snippet new file mode 100644 index 000000000..184b35663 --- /dev/null +++ b/src/main/resources/challenges/challenge-71/challenge-71.snippet @@ -0,0 +1,38 @@ +
+

🤖 Codex session transcript

+

This application ships a Codex session transcript where a developer asked the agent to investigate a failing staging deployment. The transcript captures the agent reading configuration files and outputting their contents — including a deploy token.

+ +
+

Fetch the transcript:

+
curl -sO http://localhost:8080/challenges/challenge-71/codex-session-transcript.md
+ +

…or read it right here:

+ + Open in new tab + + +

💡 The agent read a staging config file and echoed its contents — including a deploy token. Find that token in the transcript output.

+
+
+ + + + diff --git a/src/main/resources/challenges/challenge-71/codex-session-transcript.md b/src/main/resources/challenges/challenge-71/codex-session-transcript.md new file mode 100644 index 000000000..ed460ca42 --- /dev/null +++ b/src/main/resources/challenges/challenge-71/codex-session-transcript.md @@ -0,0 +1,236 @@ +OpenAI Codex v0.147.0 +-------- +workdir: C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session +model: gpt-5.5 +provider: openai +approval: never +sandbox: read-only +reasoning effort: medium +reasoning summaries: none +session id: 01a084fb-dc06-7c21-81bf-3c61061c424f +-------- +user +The staging deployment is failing with 401 Unauthorized. The token was verified yesterday. Please investigate the configuration and deployment code to find the issue. Read the .env.staging file and the deploy.js file to understand what's happening. +codex +I'll inspect the staging environment values and the deployment script first, then trace how the token is passed so we can separate a bad secret from a config/code issue. +exec +"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command 'Get-ChildItem -Force' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session +exec +"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command 'Get-Content -Raw deploy.js' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session +exec +"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command 'Get-Content -Raw .env.staging' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session + succeeded in 438ms: +# Staging environment configuration +# DO NOT COMMIT - shared team token + +API_BASE_URL=https://staging.internal.wrongsecrets.example.com +DEPLOY_TOKEN=DevS3cret_T0k3n_2024_xK9mP +LOG_LEVEL=debug + + succeeded in 404ms: +const axios = require('axios'); +const fs = require('fs'); +const path = require('path'); + +async function deploy() { + try { + const configPath = path.join(__dirname, '.env.staging'); + const config = fs.readFileSync(configPath, 'utf8'); + + const configObj = {}; + config.split('\n').forEach(line => { + if (line && !line.startsWith('#')) { + const [key, value] = line.split('='); + configObj[key.trim()] = value.trim(); + } + }); + + console.log('Deploying to:', configObj.API_BASE_URL); + + const response = await axios.post(configObj.API_BASE_URL + '/deploy', { + branch: 'main', + timestamp: Date.now() + }, { + headers: { + 'Authorization': `Bearer ${configObj.DEPLOY_TOKEN}`, + 'Content-Type': 'application/json' + } + }); + + console.log('Deploy successful:', response.data); + return response.data; + } catch (error) { + console.error('Deploy failed:', error.message); + throw error; + } +} + +deploy(); + + succeeded in 3204ms: + + + Directory: C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session + + +Mode LastWriteTime Length Name +---- ------------- ------ +d--h-- 09-09-2026 12:34 .git +-a---- 09-09-2026 12:32 192 .env.staging +-a---- 09-09-2026 12:32 986 deploy.js +-a---- 09-09-2026 12:32 286 package.json +-a---- 09-09-2026 12:33 334 README.md + + +codex +The script is loading `.env.staging` directly and sending `DEPLOY_TOKEN` as a Bearer token. One suspicious detail already stands out: the staging base URL includes `wrongsecrets`, which may be a misconfigured endpoint rather than the verified token itself. I'll check the surrounding repo docs/config for the expected staging host or deploy contract. +exec +"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command 'Get-Content -Raw package.json' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session +exec +"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command 'Get-Content -Raw README.md' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session +exec +"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command 'rg -n "staging|API_BASE_URL|DEPLOY_TOKEN|deploy|wrongsecrets|Authorization|Bearer" -S .' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session + succeeded in 397ms: +{ + "name": "staging-deploy-tool", + "version": "1.0.0", + "description": "Internal deployment utility", + "main": "deploy.js", + "scripts": { + "deploy": "node deploy.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "axios": "^1.6.0" + } +} + + succeeded in 414ms: +# Staging Deploy Tool + +Internal tool for deploying to staging environment. + +## Bug Report + +The deployment is failing with a 401 Unauthorized error, even though the token was verified to be correct in the password manager yesterday. + +Please investigate why the deployment is failing and identify if there are any configuration issues. + + succeeded in 2015ms: +.\README.md:3:Internal tool for deploying to staging environment. +.\README.md:7:The deployment is failing with a 401 Unauthorized error, even though the token was verified to be correct in the password manager yesterday. +.\README.md:9:Please investigate why the deployment is failing and identify if there are any configuration issues. +.\package.json:2: "name": "staging-deploy-tool", +.\package.json:4: "description": "Internal deployment utility", +.\package.json:5: "main": "deploy.js", +.\package.json:7: "deploy": "node deploy.js", +.\deploy.js:5:async function deploy() { +.\deploy.js:7: const configPath = path.join(__dirname, '.env.staging'); +.\deploy.js:18: console.log('Deploying to:', configObj.API_BASE_URL); +.\deploy.js:20: const response = await axios.post(configObj.API_BASE_URL + '/deploy', { +.\deploy.js:25: 'Authorization': `Bearer ${configObj.DEPLOY_TOKEN}`, +.\deploy.js:38:deploy(); + +codex +There are no other config references in the working tree, so I'm checking git metadata next. If the token was verified externally, the most likely causes are an endpoint mismatch, config parsing edge case, or a recent change in how auth is assembled. +exec +"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command 'git log --oneline --decorate -5' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session +exec +"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command 'git status --short' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session +exec +"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command 'git show --stat --oneline --decorate --name-only HEAD' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session +2026-09-09T07:05:36.210923Z ERROR codex_core::tools::router: error=Exit code: 1 +Wall time: 1.3 seconds +Output: +fatal: your current branch 'master' does not have any commits yet + + exited 1 in 1312ms: +fatal: your current branch 'master' does not have any commits yet + +2026-09-09T07:05:36.241898Z ERROR codex_core::tools::router: error=Exit code: 1 +Wall time: 1.3 seconds +Output: +fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree. +Use '--' to separate paths from revisions, like this: +'git [...] -- [...]' + + succeeded in 1413ms: +?? .env.staging +?? README.md +?? deploy.js +?? package.json +warning: unable to access 'C:\Users\Lenovo/.config/git/ignore': Permission denied + + exited 1 in 1254ms: +fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree. +Use '--' to separate paths from revisions, like this: +'git [...] -- [...]' + +codex +The repo has no commits, so there's no history to compare against. I'll grab line numbers for the two relevant files and then summarize the failure path without exposing the full secret value. +exec +"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command '$i=1; Get-Content deploy.js | ForEach-Object { '"'{0,4}: {1}' -f "'$i, $_; $i++ }' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session +exec +"C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -Command '$i=1; Get-Content .env.staging | ForEach-Object { '"'{0,4}: {1}' -f "'$i, $_; $i++ }' in C:\Users\Lenovo\AppData\Local\Temp\opencode\codex-session + succeeded in 1257ms: + 1: # Staging environment configuration + 2: # DO NOT COMMIT - shared team token + 3: + 4: API_BASE_URL=https://staging.internal.wrongsecrets.example.com + 5: DEPLOY_TOKEN=DevS3cret_T0k3n_2024_xK9mP + 6: LOG_LEVEL=debug + + succeeded in 1298ms: + 1: const axios = require('axios'); + 2: const fs = require('fs'); + 3: const path = require('path'); + 4: + 5: async function deploy() { + 6: try { + 7: const configPath = path.join(__dirname, '.env.staging'); + 8: const config = fs.readFileSync(configPath, 'utf8'); + 9: + 10: const configObj = {}; + 11: config.split('\n').forEach(line => { + 12: if (line && !line.startsWith('#')) { + 13: const [key, value] = line.split('='); + 14: configObj[key.trim()] = value.trim(); + 15: } + 16: }); + 17: + 18: console.log('Deploying to:', configObj.API_BASE_URL); + 19: + 20: const response = await axios.post(configObj.API_BASE_URL + '/deploy', { + 21: branch: 'main', + 22: timestamp: Date.now() + 23: }, { + 24: headers: { + 25: 'Authorization': `Bearer ${configObj.DEPLOY_TOKEN}`, + 26: 'Content-Type': 'application/json' + 27: } + 28: }); + 29: + 30: console.log('Deploy successful:', response.data); + 31: return response.data; + 32: } catch (error) { + 33: console.error('Deploy failed:', error.message); + 34: throw error; + 35: } + 36: } + 37: + 38: deploy(); + +codex +The likely issue is configuration, not the token. + +`.env.staging` points `API_BASE_URL` at: + +`https://staging.internal.wrongsecrets.example.com` + +Then [deploy.js](/C:/Users/Lenovo/AppData/Local/Temp/opencode/codex-session/deploy.js:20) posts to that host's `/deploy` endpoint with the verified `DEPLOY_TOKEN` as `Authorization: Bearer ...`. + +If the token was verified yesterday, a `401 Unauthorized` is consistent with sending the correct token to the wrong service/environment. The hostname strongly suggests the staging deploy is using the wrong secrets/config target: `wrongsecrets`. + +Secondary issue: [deploy.js](/C:/Users/Lenovo/AppData/Local/Temp/opencode/codex-session/deploy.js:13) parses env lines with `line.split('=')`, which will corrupt values containing `=`. That is not causing this exact token to fail, but it is brittle and should be fixed. + +I could not patch the files because the workspace is read-only, but the immediate fix is to correct `API_BASE_URL` in `.env.staging` to the real staging deployment API host. Then harden the parser or use `dotenv` so future tokens/URLs with `=` are handled correctly. diff --git a/src/main/resources/explanations/challenge71.adoc b/src/main/resources/explanations/challenge71.adoc new file mode 100644 index 000000000..8fb57cf5f --- /dev/null +++ b/src/main/resources/explanations/challenge71.adoc @@ -0,0 +1,14 @@ +=== Challenge 71: Find the Secret in the AI Coding-Agent Transcript + +AI coding agents like Codex, Claude Code, and Cursor read files, run commands, and produce detailed session transcripts. When a developer asks an agent to debug a deployment issue, the agent may read configuration files containing secrets and echo those values into its session output. + +This application ships a Codex session transcript where a developer asked the agent to investigate a failing staging deployment. The transcript captures the agent reading a `.env.staging` file and outputting its contents — including the deploy token — as part of normal debugging workflow. + +Download the transcript from link:/challenges/challenge-71/codex-session-transcript.md[`/challenges/challenge-71/codex-session-transcript.md`] and find the staging deploy token that was exposed during the session. + +The source is also available at link:https://github.com/OWASP/wrongsecrets/blob/master/src/main/resources/challenges/challenge-71/codex-session-transcript.md[`src/main/resources/challenges/challenge-71/codex-session-transcript.md`]. + +[NOTE] +==== +The token appears naturally in the transcript output — the agent did not intend to leak it, it simply read a file as part of its debugging process. +==== diff --git a/src/main/resources/explanations/challenge71_hint.adoc b/src/main/resources/explanations/challenge71_hint.adoc new file mode 100644 index 000000000..8d7b98656 --- /dev/null +++ b/src/main/resources/explanations/challenge71_hint.adoc @@ -0,0 +1,3 @@ +Open link:/challenges/challenge-71/codex-session-transcript.md[`/challenges/challenge-71/codex-session-transcript.md`] and search for the line containing `DEPLOY_TOKEN=`. The value after the equals sign is the answer. + +The same file is in the source tree at `src/main/resources/challenges/challenge-71/codex-session-transcript.md`. diff --git a/src/main/resources/explanations/challenge71_reason.adoc b/src/main/resources/explanations/challenge71_reason.adoc new file mode 100644 index 000000000..54bc8a7fc --- /dev/null +++ b/src/main/resources/explanations/challenge71_reason.adoc @@ -0,0 +1,32 @@ +*Why AI coding-agent transcripts are a secret-leakage risk* + +AI coding agents read your files, execute your commands, and produce session logs. When a developer asks an agent to debug an issue, the agent may read configuration files, environment variables, or secrets managers — and output those values in its transcript. This is exactly what happened in this challenge: the agent read a `.env.staging` file and echoed the deploy token into its session output. + +The transcript becomes a persistent artifact that can be: + +- Committed to version control if the developer saves it +- Shared with teammates for context or handoff +- Uploaded to support channels when reporting bugs +- Stored in agent history files on the developer's machine +- Indexed by IDE plugins or local search tools + +Three failures compound in this scenario: + +- The secret exists in a plaintext configuration file, making it trivially readable by any tool or agent. +- The agent's debugging process naturally surfaces the secret in its output, creating a secondary copy of the credential. +- The transcript is likely to be saved, shared, or committed without review, since it "looks like" debug output rather than sensitive data. + +---- +What to do instead: + +- Never store secrets in plaintext configuration files. Use a secret manager or environment variables injected at runtime. +- Configure agents to redact sensitive values before outputting them. Many agent tools support output filtering or sandboxing. +- Review agent transcripts before saving or sharing them, just as you would review a pull request. +- Treat any transcript that read from a secrets source as potentially compromised, and rotate the exposed credentials. +- Use short-lived, scoped tokens for staging deployments so that exposure has limited blast radius. +---- + +[NOTE] +==== +AI coding agents are powerful tools, but they operate on the same files and environment you do. If a human developer would copy-paste a secret into a chat log, an agent will do the same — except the agent produces a structured transcript that is even easier to search and share. +==== diff --git a/src/main/resources/wrong-secrets-configuration.yaml b/src/main/resources/wrong-secrets-configuration.yaml index 9b88ec387..c924a236d 100644 --- a/src/main/resources/wrong-secrets-configuration.yaml +++ b/src/main/resources/wrong-secrets-configuration.yaml @@ -152,7 +152,7 @@ configurations: reason: "explanations/challenge4_reason.adoc" environments: *all_envs difficulty: *normal - category: *docker_tech + category: *ai ctf: enabled: true @@ -1053,3 +1053,16 @@ configurations: category: *ai ctf: enabled: true + - name: Challenge 71 + short-name: "challenge-71" + sources: + - class-name: "org.owasp.wrongsecrets.challenges.docker.Challenge71" + explanation: "explanations/challenge71.adoc" + hint: "explanations/challenge71_hint.adoc" + reason: "explanations/challenge71_reason.adoc" + ui-snippet: "challenges/challenge-71/challenge-71.snippet" + environments: *all_envs + difficulty: *normal + category: *ai + ctf: + enabled: true diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71ControllerTest.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71ControllerTest.java new file mode 100644 index 000000000..d8b7e1372 --- /dev/null +++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71ControllerTest.java @@ -0,0 +1,41 @@ +package org.owasp.wrongsecrets.challenges.docker; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.HttpStatus; + +class Challenge71ControllerTest { + + private static final String TRANSCRIPT_LOCATION = + "challenges/challenge-71/codex-session-transcript.md"; + + @Test + void shouldServeTheTranscriptAsMarkdown() { + var controller = new Challenge71Controller(new ClassPathResource(TRANSCRIPT_LOCATION)); + + var response = controller.codexTranscript(); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getHeaders().getContentType()).hasToString("text/markdown;charset=UTF-8"); + assertThat(response.getBody()).contains("DEPLOY_TOKEN="); + } + + @Test + void servedTranscriptShouldContainTheAnswerOfTheChallenge() { + var controller = new Challenge71Controller(new ClassPathResource(TRANSCRIPT_LOCATION)); + var challenge = new Challenge71(new ClassPathResource(TRANSCRIPT_LOCATION)); + + assertThat(controller.codexTranscript().getBody()).contains(challenge.spoiler().solution()); + } + + @Test + void shouldReturnServerErrorWhenTheTranscriptIsMissing() { + var controller = + new Challenge71Controller(new ClassPathResource("challenges/challenge-71/missing.md")); + + assertThat(controller.codexTranscript().getStatusCode()) + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Test.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Test.java new file mode 100644 index 000000000..7f09ef158 --- /dev/null +++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Test.java @@ -0,0 +1,79 @@ +package org.owasp.wrongsecrets.challenges.docker; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.owasp.wrongsecrets.Challenges.ErrorResponses.FILE_MOUNT_ERROR; + +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +class Challenge71Test { + + private static final String TRANSCRIPT_LOCATION = + "challenges/challenge-71/codex-session-transcript.md"; + + private static Resource transcriptContaining(String content) { + return new ByteArrayResource(content.getBytes(StandardCharsets.UTF_8)); + } + + @Test + void spoilerShouldGiveTheTokenFromTheShippedTranscript() { + var challenge = new Challenge71(new ClassPathResource(TRANSCRIPT_LOCATION)); + + assertThat(challenge.spoiler().solution()).isNotEmpty().isNotEqualTo(FILE_MOUNT_ERROR); + assertThat(challenge.answerCorrect(challenge.spoiler().solution())).isTrue(); + } + + @Test + void shippedTranscriptShouldContainTheToken() throws Exception { + var transcript = + new ClassPathResource(TRANSCRIPT_LOCATION).getContentAsString(StandardCharsets.UTF_8); + + assertThat(transcript).contains("DEPLOY_TOKEN="); + } + + @Test + void shouldExtractTheTokenFromTheTranscript() { + var challenge = + new Challenge71( + transcriptContaining( + """ + succeeded in 438ms: + # Staging environment configuration + # DO NOT COMMIT - shared team token + + API_BASE_URL=https://staging.internal.wrongsecrets.example.com + DEPLOY_TOKEN=TestToken123 + LOG_LEVEL=debug + """)); + + assertThat(challenge.spoiler().solution()).isEqualTo("TestToken123"); + assertThat(challenge.answerCorrect("TestToken123")).isTrue(); + } + + @Test + void incorrectAnswerShouldNotSolveChallenge() { + var challenge = new Challenge71(new ClassPathResource(TRANSCRIPT_LOCATION)); + + assertThat(challenge.answerCorrect("wrong answer")).isFalse(); + assertThat(challenge.answerCorrect("")).isFalse(); + } + + @Test + void shouldReportAnErrorWhenTheTranscriptHasNoToken() { + var challenge = + new Challenge71(transcriptContaining("# Session transcript\n\nNo secrets here.\n")); + + assertThat(challenge.spoiler().solution()).isEqualTo(FILE_MOUNT_ERROR); + } + + @Test + void shouldReportAnErrorWhenTheTranscriptCannotBeRead() { + var challenge = + new Challenge71(new ClassPathResource("challenges/challenge-71/does-not-exist.md")); + + assertThat(challenge.spoiler().solution()).isEqualTo(FILE_MOUNT_ERROR); + } +} From d47aec39b7cafb1d94cab442990dee78aa5bf884 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:12:28 +0000 Subject: [PATCH 2/5] [pre-commit.ci lite] apply automatic fixes --- .../org/owasp/wrongsecrets/challenges/docker/Challenge71.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71.java index b84c56298..d68bdb3ca 100644 --- a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71.java +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71.java @@ -35,8 +35,7 @@ public Challenge71( @Override public String getAnswer() { try { - var transcriptContent = - transcriptFile.getContentAsString(StandardCharsets.UTF_8); + var transcriptContent = transcriptFile.getContentAsString(StandardCharsets.UTF_8); var matcher = DEPLOY_TOKEN_PATTERN.matcher(transcriptContent); if (!matcher.find()) { log.warn("Could not find the deploy token in the Codex transcript of challenge 71"); From c3befd138cb28132f8df07a6a37b1aec1f5a761a Mon Sep 17 00:00:00 2001 From: kekubhai Date: Fri, 11 Sep 2026 09:45:50 +0530 Subject: [PATCH 3/5] feat(challenges): rename challenge 71 to challenge 72 - Rename Challenge71 class to Challenge72 with updated resource paths - Rename Challenge71Controller to Challenge72Controller - Move challenge resources from challenge-71/ to challenge-72/ directory - Update challenge snippet with new challenge number references - Rename explanation files to challenge72.adoc and challenge72_hint.adoc - Remove outdated challenge71_hint.adoc explanation file - Update test classes Challenge71Test and Challenge71ControllerTest to Challenge72Test and Challenge72ControllerTest - Update wrong-secrets-configuration.yaml with new challenge 72 configuration - Update all internal references and log messages to reflect challenge 72 --- .../{Challenge71.java => Challenge72.java} | 10 +++++----- ...ontroller.java => Challenge72Controller.java} | 14 +++++++------- .../challenge-72.snippet} | 6 +++--- .../codex-session-transcript.md | 0 .../resources/explanations/challenge71_hint.adoc | 3 --- .../{challenge71.adoc => challenge72.adoc} | 6 +++--- .../resources/explanations/challenge72_hint.adoc | 3 +++ ...nge71_reason.adoc => challenge72_reason.adoc} | 0 .../resources/wrong-secrets-configuration.yaml | 16 ++++++++-------- ...rTest.java => Challenge72ControllerTest.java} | 12 ++++++------ ...Challenge71Test.java => Challenge72Test.java} | 14 +++++++------- 11 files changed, 42 insertions(+), 42 deletions(-) rename src/main/java/org/owasp/wrongsecrets/challenges/docker/{Challenge71.java => Challenge72.java} (89%) rename src/main/java/org/owasp/wrongsecrets/challenges/docker/{Challenge71Controller.java => Challenge72Controller.java} (79%) rename src/main/resources/challenges/{challenge-71/challenge-71.snippet => challenge-72/challenge-72.snippet} (90%) rename src/main/resources/challenges/{challenge-71 => challenge-72}/codex-session-transcript.md (100%) delete mode 100644 src/main/resources/explanations/challenge71_hint.adoc rename src/main/resources/explanations/{challenge71.adoc => challenge72.adoc} (76%) create mode 100644 src/main/resources/explanations/challenge72_hint.adoc rename src/main/resources/explanations/{challenge71_reason.adoc => challenge72_reason.adoc} (100%) rename src/test/java/org/owasp/wrongsecrets/challenges/docker/{Challenge71ControllerTest.java => Challenge72ControllerTest.java} (74%) rename src/test/java/org/owasp/wrongsecrets/challenges/docker/{Challenge71Test.java => Challenge72Test.java} (85%) diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge72.java similarity index 89% rename from src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71.java rename to src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge72.java index b84c56298..38d46b436 100644 --- a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71.java +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge72.java @@ -19,15 +19,15 @@ */ @Slf4j @Component -public class Challenge71 extends FixedAnswerChallenge { +public class Challenge72 extends FixedAnswerChallenge { private static final Pattern DEPLOY_TOKEN_PATTERN = Pattern.compile("DEPLOY_TOKEN=([A-Za-z0-9_]+)"); private final Resource transcriptFile; - public Challenge71( - @Value("classpath:challenges/challenge-71/codex-session-transcript.md") + public Challenge72( + @Value("classpath:challenges/challenge-72/codex-session-transcript.md") Resource transcriptFile) { this.transcriptFile = transcriptFile; } @@ -39,12 +39,12 @@ public String getAnswer() { transcriptFile.getContentAsString(StandardCharsets.UTF_8); var matcher = DEPLOY_TOKEN_PATTERN.matcher(transcriptContent); if (!matcher.find()) { - log.warn("Could not find the deploy token in the Codex transcript of challenge 71"); + log.warn("Could not find the deploy token in the Codex transcript of challenge 72"); return FILE_MOUNT_ERROR; } return matcher.group(1); } catch (IOException e) { - log.warn("Exception while reading the Codex transcript of challenge 71", e); + log.warn("Exception while reading the Codex transcript of challenge 72", e); return FILE_MOUNT_ERROR; } } diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Controller.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge72Controller.java similarity index 79% rename from src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Controller.java rename to src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge72Controller.java index bfade683a..c549f9866 100644 --- a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Controller.java +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge72Controller.java @@ -12,33 +12,33 @@ import org.springframework.web.bind.annotation.RestController; /** - * Hosts the Codex session transcript of challenge 71 straight from the resource folder, so + * Hosts the Codex session transcript of challenge 72 straight from the resource folder, so * participants can read the transcript the same way an analyst would when reviewing agent output. */ @Slf4j @RestController -public class Challenge71Controller { +public class Challenge72Controller { private static final MediaType MARKDOWN = new MediaType("text", "markdown", StandardCharsets.UTF_8); private final Resource transcriptFile; - public Challenge71Controller( - @Value("classpath:challenges/challenge-71/codex-session-transcript.md") + public Challenge72Controller( + @Value("classpath:challenges/challenge-72/codex-session-transcript.md") Resource transcriptFile) { this.transcriptFile = transcriptFile; } - /** Returns the raw Codex session transcript for challenge 71. */ - @GetMapping("/challenges/challenge-71/codex-session-transcript.md") + /** Returns the raw Codex session transcript for challenge 72. */ + @GetMapping("/challenges/challenge-72/codex-session-transcript.md") public ResponseEntity codexTranscript() { try { return ResponseEntity.ok() .contentType(MARKDOWN) .body(transcriptFile.getContentAsString(StandardCharsets.UTF_8)); } catch (IOException e) { - log.warn("Unable to serve the Codex transcript of challenge 71", e); + log.warn("Unable to serve the Codex transcript of challenge 72", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } diff --git a/src/main/resources/challenges/challenge-71/challenge-71.snippet b/src/main/resources/challenges/challenge-72/challenge-72.snippet similarity index 90% rename from src/main/resources/challenges/challenge-71/challenge-71.snippet rename to src/main/resources/challenges/challenge-72/challenge-72.snippet index 184b35663..60294b021 100644 --- a/src/main/resources/challenges/challenge-71/challenge-71.snippet +++ b/src/main/resources/challenges/challenge-72/challenge-72.snippet @@ -6,11 +6,11 @@

Fetch the transcript:

curl -sO http://localhost:8080/challenges/challenge-71/codex-session-transcript.md
+ th:text="'curl -sO ' + ${httpServletRequest.scheme} + '://' + ${httpServletRequest.serverName} + (${httpServletRequest.serverPort == 80 || httpServletRequest.serverPort == 443 ? '' : ':' + ${httpServletRequest.serverPort}) + '/challenges/challenge-72/codex-session-transcript.md'">curl -sO http://localhost:8080/challenges/challenge-72/codex-session-transcript.md

…or read it right here:

- Open in new tab + Open in new tab

💡 The agent read a staging config file and echoed its contents — including a deploy token. Find that token in the transcript output.

@@ -30,7 +30,7 @@ function loadTranscript() { const out = document.getElementById('transcript-output'); out.style.display = 'block'; out.textContent = 'Loading…'; - fetch('/challenges/challenge-71/codex-session-transcript.md') + fetch('/challenges/challenge-72/codex-session-transcript.md') .then(function(r) { return r.text(); }) .then(function(text) { out.textContent = text; }) .catch(function(err) { out.textContent = 'Failed to load the transcript: ' + (err.message || err); }); diff --git a/src/main/resources/challenges/challenge-71/codex-session-transcript.md b/src/main/resources/challenges/challenge-72/codex-session-transcript.md similarity index 100% rename from src/main/resources/challenges/challenge-71/codex-session-transcript.md rename to src/main/resources/challenges/challenge-72/codex-session-transcript.md diff --git a/src/main/resources/explanations/challenge71_hint.adoc b/src/main/resources/explanations/challenge71_hint.adoc deleted file mode 100644 index 8d7b98656..000000000 --- a/src/main/resources/explanations/challenge71_hint.adoc +++ /dev/null @@ -1,3 +0,0 @@ -Open link:/challenges/challenge-71/codex-session-transcript.md[`/challenges/challenge-71/codex-session-transcript.md`] and search for the line containing `DEPLOY_TOKEN=`. The value after the equals sign is the answer. - -The same file is in the source tree at `src/main/resources/challenges/challenge-71/codex-session-transcript.md`. diff --git a/src/main/resources/explanations/challenge71.adoc b/src/main/resources/explanations/challenge72.adoc similarity index 76% rename from src/main/resources/explanations/challenge71.adoc rename to src/main/resources/explanations/challenge72.adoc index 8fb57cf5f..ecaff2ecf 100644 --- a/src/main/resources/explanations/challenge71.adoc +++ b/src/main/resources/explanations/challenge72.adoc @@ -1,12 +1,12 @@ -=== Challenge 71: Find the Secret in the AI Coding-Agent Transcript +=== Challenge 72: Find the Secret in the AI Coding-Agent Transcript AI coding agents like Codex, Claude Code, and Cursor read files, run commands, and produce detailed session transcripts. When a developer asks an agent to debug a deployment issue, the agent may read configuration files containing secrets and echo those values into its session output. This application ships a Codex session transcript where a developer asked the agent to investigate a failing staging deployment. The transcript captures the agent reading a `.env.staging` file and outputting its contents — including the deploy token — as part of normal debugging workflow. -Download the transcript from link:/challenges/challenge-71/codex-session-transcript.md[`/challenges/challenge-71/codex-session-transcript.md`] and find the staging deploy token that was exposed during the session. +Download the transcript from link:/challenges/challenge-72/codex-session-transcript.md[`/challenges/challenge-72/codex-session-transcript.md`] and find the staging deploy token that was exposed during the session. -The source is also available at link:https://github.com/OWASP/wrongsecrets/blob/master/src/main/resources/challenges/challenge-71/codex-session-transcript.md[`src/main/resources/challenges/challenge-71/codex-session-transcript.md`]. +The source is also available at link:https://github.com/OWASP/wrongsecrets/blob/master/src/main/resources/challenges/challenge-72/codex-session-transcript.md[`src/main/resources/challenges/challenge-72/codex-session-transcript.md`]. [NOTE] ==== diff --git a/src/main/resources/explanations/challenge72_hint.adoc b/src/main/resources/explanations/challenge72_hint.adoc new file mode 100644 index 000000000..f121dba9c --- /dev/null +++ b/src/main/resources/explanations/challenge72_hint.adoc @@ -0,0 +1,3 @@ +Open link:/challenges/challenge-72/codex-session-transcript.md[`/challenges/challenge-72/codex-session-transcript.md`] and search for the line containing `DEPLOY_TOKEN=`. The value after the equals sign is the answer. + +The same file is in the source tree at `src/main/resources/challenges/challenge-72/codex-session-transcript.md`. diff --git a/src/main/resources/explanations/challenge71_reason.adoc b/src/main/resources/explanations/challenge72_reason.adoc similarity index 100% rename from src/main/resources/explanations/challenge71_reason.adoc rename to src/main/resources/explanations/challenge72_reason.adoc diff --git a/src/main/resources/wrong-secrets-configuration.yaml b/src/main/resources/wrong-secrets-configuration.yaml index c924a236d..e57ebe26b 100644 --- a/src/main/resources/wrong-secrets-configuration.yaml +++ b/src/main/resources/wrong-secrets-configuration.yaml @@ -1053,14 +1053,14 @@ configurations: category: *ai ctf: enabled: true - - name: Challenge 71 - short-name: "challenge-71" - sources: - - class-name: "org.owasp.wrongsecrets.challenges.docker.Challenge71" - explanation: "explanations/challenge71.adoc" - hint: "explanations/challenge71_hint.adoc" - reason: "explanations/challenge71_reason.adoc" - ui-snippet: "challenges/challenge-71/challenge-71.snippet" + - name: Challenge 72 + short-name: "challenge-72" + sources: + - class-name: "org.owasp.wrongsecrets.challenges.docker.Challenge72" + explanation: "explanations/challenge72.adoc" + hint: "explanations/challenge72_hint.adoc" + reason: "explanations/challenge72_reason.adoc" + ui-snippet: "challenges/challenge-72/challenge-72.snippet" environments: *all_envs difficulty: *normal category: *ai diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71ControllerTest.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge72ControllerTest.java similarity index 74% rename from src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71ControllerTest.java rename to src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge72ControllerTest.java index d8b7e1372..6eb99024f 100644 --- a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71ControllerTest.java +++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge72ControllerTest.java @@ -6,14 +6,14 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.http.HttpStatus; -class Challenge71ControllerTest { +class Challenge72ControllerTest { private static final String TRANSCRIPT_LOCATION = - "challenges/challenge-71/codex-session-transcript.md"; + "challenges/challenge-72/codex-session-transcript.md"; @Test void shouldServeTheTranscriptAsMarkdown() { - var controller = new Challenge71Controller(new ClassPathResource(TRANSCRIPT_LOCATION)); + var controller = new Challenge72Controller(new ClassPathResource(TRANSCRIPT_LOCATION)); var response = controller.codexTranscript(); @@ -24,8 +24,8 @@ void shouldServeTheTranscriptAsMarkdown() { @Test void servedTranscriptShouldContainTheAnswerOfTheChallenge() { - var controller = new Challenge71Controller(new ClassPathResource(TRANSCRIPT_LOCATION)); - var challenge = new Challenge71(new ClassPathResource(TRANSCRIPT_LOCATION)); + var controller = new Challenge72Controller(new ClassPathResource(TRANSCRIPT_LOCATION)); + var challenge = new Challenge72(new ClassPathResource(TRANSCRIPT_LOCATION)); assertThat(controller.codexTranscript().getBody()).contains(challenge.spoiler().solution()); } @@ -33,7 +33,7 @@ void servedTranscriptShouldContainTheAnswerOfTheChallenge() { @Test void shouldReturnServerErrorWhenTheTranscriptIsMissing() { var controller = - new Challenge71Controller(new ClassPathResource("challenges/challenge-71/missing.md")); + new Challenge72Controller(new ClassPathResource("challenges/challenge-72/missing.md")); assertThat(controller.codexTranscript().getStatusCode()) .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Test.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge72Test.java similarity index 85% rename from src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Test.java rename to src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge72Test.java index 7f09ef158..7925a30f3 100644 --- a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Test.java +++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge72Test.java @@ -9,10 +9,10 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -class Challenge71Test { +class Challenge72Test { private static final String TRANSCRIPT_LOCATION = - "challenges/challenge-71/codex-session-transcript.md"; + "challenges/challenge-72/codex-session-transcript.md"; private static Resource transcriptContaining(String content) { return new ByteArrayResource(content.getBytes(StandardCharsets.UTF_8)); @@ -20,7 +20,7 @@ private static Resource transcriptContaining(String content) { @Test void spoilerShouldGiveTheTokenFromTheShippedTranscript() { - var challenge = new Challenge71(new ClassPathResource(TRANSCRIPT_LOCATION)); + var challenge = new Challenge72(new ClassPathResource(TRANSCRIPT_LOCATION)); assertThat(challenge.spoiler().solution()).isNotEmpty().isNotEqualTo(FILE_MOUNT_ERROR); assertThat(challenge.answerCorrect(challenge.spoiler().solution())).isTrue(); @@ -37,7 +37,7 @@ void shippedTranscriptShouldContainTheToken() throws Exception { @Test void shouldExtractTheTokenFromTheTranscript() { var challenge = - new Challenge71( + new Challenge72( transcriptContaining( """ succeeded in 438ms: @@ -55,7 +55,7 @@ void shouldExtractTheTokenFromTheTranscript() { @Test void incorrectAnswerShouldNotSolveChallenge() { - var challenge = new Challenge71(new ClassPathResource(TRANSCRIPT_LOCATION)); + var challenge = new Challenge72(new ClassPathResource(TRANSCRIPT_LOCATION)); assertThat(challenge.answerCorrect("wrong answer")).isFalse(); assertThat(challenge.answerCorrect("")).isFalse(); @@ -64,7 +64,7 @@ void incorrectAnswerShouldNotSolveChallenge() { @Test void shouldReportAnErrorWhenTheTranscriptHasNoToken() { var challenge = - new Challenge71(transcriptContaining("# Session transcript\n\nNo secrets here.\n")); + new Challenge72(transcriptContaining("# Session transcript\n\nNo secrets here.\n")); assertThat(challenge.spoiler().solution()).isEqualTo(FILE_MOUNT_ERROR); } @@ -72,7 +72,7 @@ void shouldReportAnErrorWhenTheTranscriptHasNoToken() { @Test void shouldReportAnErrorWhenTheTranscriptCannotBeRead() { var challenge = - new Challenge71(new ClassPathResource("challenges/challenge-71/does-not-exist.md")); + new Challenge72(new ClassPathResource("challenges/challenge-72/does-not-exist.md")); assertThat(challenge.spoiler().solution()).isEqualTo(FILE_MOUNT_ERROR); } From 775b2a58bfff5457740b1579e35538394fdbd455 Mon Sep 17 00:00:00 2001 From: Jeroen Willemsen Date: Fri, 18 Sep 2026 03:34:14 +0200 Subject: [PATCH 4/5] Update src/main/resources/wrong-secrets-configuration.yaml --- src/main/resources/wrong-secrets-configuration.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/wrong-secrets-configuration.yaml b/src/main/resources/wrong-secrets-configuration.yaml index a376d97f6..c7d187022 100644 --- a/src/main/resources/wrong-secrets-configuration.yaml +++ b/src/main/resources/wrong-secrets-configuration.yaml @@ -1078,7 +1078,7 @@ configurations: reason: "explanations/challenge72_reason.adoc" ui-snippet: "challenges/challenge-72/challenge-72.snippet" environments: *all_envs - difficulty: *normal + difficulty: *easy category: *ai ctf: enabled: true From 41e1de7cb7e038c83234dde0acf2817ba0eab7f1 Mon Sep 17 00:00:00 2001 From: kekubhai Date: Fri, 18 Sep 2026 09:29:26 +0530 Subject: [PATCH 5/5] fix(challenge-72): add dark-mode CSS, fix Thymeleaf expression, update lycheeignore - Add #codex-transcript-container to dark.css matching challenge-69/70 pattern - Fix nested variable in Thymeleaf th:text causing template parse error (CI failure) - Add staging.internal.wrongsecrets.example.com to .lycheeignore --- .lycheeignore | 3 +++ .../resources/challenges/challenge-72/challenge-72.snippet | 2 +- src/main/resources/static/css/dark.css | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.lycheeignore b/.lycheeignore index d8f4c98ac..444c774f4 100644 --- a/.lycheeignore +++ b/.lycheeignore @@ -43,5 +43,8 @@ https://www.jcchouinard.com/wget/ # Railway deploy template links return 404 to link checkers https://railway.com/deploy/* +# Staging internal URL from challenge 72 transcript is not reachable from CI +https://staging.internal.wrongsecrets.example.com/ + # Slack gives 403, but channels exists for a few years now https://owasp.slack.com/* diff --git a/src/main/resources/challenges/challenge-72/challenge-72.snippet b/src/main/resources/challenges/challenge-72/challenge-72.snippet index 60294b021..c334902cf 100644 --- a/src/main/resources/challenges/challenge-72/challenge-72.snippet +++ b/src/main/resources/challenges/challenge-72/challenge-72.snippet @@ -6,7 +6,7 @@

Fetch the transcript:

curl -sO http://localhost:8080/challenges/challenge-72/codex-session-transcript.md
+ th:text="'curl -sO ' + ${httpServletRequest.scheme} + '://' + ${httpServletRequest.serverName} + (${httpServletRequest.serverPort == 80 || httpServletRequest.serverPort == 443 ? '' : ':' + httpServletRequest.serverPort}) + '/challenges/challenge-72/codex-session-transcript.md'">curl -sO http://localhost:8080/challenges/challenge-72/codex-session-transcript.md

…or read it right here:

diff --git a/src/main/resources/static/css/dark.css b/src/main/resources/static/css/dark.css index 5bda2b51d..5486bfc55 100644 --- a/src/main/resources/static/css/dark.css +++ b/src/main/resources/static/css/dark.css @@ -154,6 +154,7 @@ .dark-mode #cursor-skill-container, .dark-mode #claude-skill-container, +.dark-mode #codex-transcript-container, .dark-mode #database-challenge-container { background-color: #1f1f1f !important; border-color: var(--bs-gray-700) !important; @@ -164,6 +165,8 @@ .dark-mode #cursor-skill-container p, .dark-mode #claude-skill-container h4, .dark-mode #claude-skill-container p, +.dark-mode #codex-transcript-container h4, +.dark-mode #codex-transcript-container p, .dark-mode #database-challenge-container h4, .dark-mode #database-challenge-container p { color: var(--bs-body-color);