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/java/org/owasp/wrongsecrets/challenges/docker/Challenge72.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge72.java new file mode 100644 index 000000000..d875fd0c6 --- /dev/null +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge72.java @@ -0,0 +1,50 @@ +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 Challenge72 extends FixedAnswerChallenge { + + private static final Pattern DEPLOY_TOKEN_PATTERN = + Pattern.compile("DEPLOY_TOKEN=([A-Za-z0-9_]+)"); + + private final Resource transcriptFile; + + public Challenge72( + @Value("classpath:challenges/challenge-72/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 72"); + return FILE_MOUNT_ERROR; + } + return matcher.group(1); + } catch (IOException 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/Challenge72Controller.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge72Controller.java new file mode 100644 index 000000000..c549f9866 --- /dev/null +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge72Controller.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 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 Challenge72Controller { + + private static final MediaType MARKDOWN = + new MediaType("text", "markdown", StandardCharsets.UTF_8); + + private final Resource transcriptFile; + + public Challenge72Controller( + @Value("classpath:challenges/challenge-72/codex-session-transcript.md") + Resource transcriptFile) { + this.transcriptFile = transcriptFile; + } + + /** 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 72", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } +} diff --git a/src/main/resources/challenges/challenge-72/challenge-72.snippet b/src/main/resources/challenges/challenge-72/challenge-72.snippet new file mode 100644 index 000000000..c334902cf --- /dev/null +++ b/src/main/resources/challenges/challenge-72/challenge-72.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-72/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-72/codex-session-transcript.md b/src/main/resources/challenges/challenge-72/codex-session-transcript.md new file mode 100644 index 000000000..ed460ca42 --- /dev/null +++ b/src/main/resources/challenges/challenge-72/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/challenge72.adoc b/src/main/resources/explanations/challenge72.adoc new file mode 100644 index 000000000..ecaff2ecf --- /dev/null +++ b/src/main/resources/explanations/challenge72.adoc @@ -0,0 +1,14 @@ +=== 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-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-72/codex-session-transcript.md[`src/main/resources/challenges/challenge-72/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/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/challenge72_reason.adoc b/src/main/resources/explanations/challenge72_reason.adoc new file mode 100644 index 000000000..54bc8a7fc --- /dev/null +++ b/src/main/resources/explanations/challenge72_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/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); diff --git a/src/main/resources/wrong-secrets-configuration.yaml b/src/main/resources/wrong-secrets-configuration.yaml index 516adeb27..708593144 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 @@ -1067,3 +1067,17 @@ configurations: category: *docker_tech ctf: enabled: true + + - 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: *easy + category: *ai + ctf: + enabled: true diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge72ControllerTest.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge72ControllerTest.java new file mode 100644 index 000000000..6eb99024f --- /dev/null +++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge72ControllerTest.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 Challenge72ControllerTest { + + private static final String TRANSCRIPT_LOCATION = + "challenges/challenge-72/codex-session-transcript.md"; + + @Test + void shouldServeTheTranscriptAsMarkdown() { + var controller = new Challenge72Controller(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 Challenge72Controller(new ClassPathResource(TRANSCRIPT_LOCATION)); + var challenge = new Challenge72(new ClassPathResource(TRANSCRIPT_LOCATION)); + + assertThat(controller.codexTranscript().getBody()).contains(challenge.spoiler().solution()); + } + + @Test + void shouldReturnServerErrorWhenTheTranscriptIsMissing() { + var controller = + 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/Challenge72Test.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge72Test.java new file mode 100644 index 000000000..7925a30f3 --- /dev/null +++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge72Test.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 Challenge72Test { + + private static final String TRANSCRIPT_LOCATION = + "challenges/challenge-72/codex-session-transcript.md"; + + private static Resource transcriptContaining(String content) { + return new ByteArrayResource(content.getBytes(StandardCharsets.UTF_8)); + } + + @Test + void spoilerShouldGiveTheTokenFromTheShippedTranscript() { + var challenge = new Challenge72(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 Challenge72( + 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 Challenge72(new ClassPathResource(TRANSCRIPT_LOCATION)); + + assertThat(challenge.answerCorrect("wrong answer")).isFalse(); + assertThat(challenge.answerCorrect("")).isFalse(); + } + + @Test + void shouldReportAnErrorWhenTheTranscriptHasNoToken() { + var challenge = + new Challenge72(transcriptContaining("# Session transcript\n\nNo secrets here.\n")); + + assertThat(challenge.spoiler().solution()).isEqualTo(FILE_MOUNT_ERROR); + } + + @Test + void shouldReportAnErrorWhenTheTranscriptCannotBeRead() { + var challenge = + new Challenge72(new ClassPathResource("challenges/challenge-72/does-not-exist.md")); + + assertThat(challenge.spoiler().solution()).isEqualTo(FILE_MOUNT_ERROR); + } +}