From b5bd55b5187800f83509b4100ce4c89870a44fe8 Mon Sep 17 00:00:00 2001 From: kekubhai Date: Fri, 4 Sep 2026 12:21:34 +0530 Subject: [PATCH 1/3] feat(challenges): add challenges 67 and 68 with AI agent skills - Add Challenge 67: Extract hardcoded secret from Cursor skill deployment preview file - Add Challenge 67 Controller: Serve Cursor SKILL.md file for challenge interaction - Add Challenge 68: Decode base64-encoded secret from Claude incident reporter skill - Add Challenge 68 Controller: Serve Claude skill files and handle incident report submission - Add comprehensive test suites for Challenge 67 and 68 (unit and controller tests) - Add challenge resources: skill definitions, documentation, and supporting scripts - Add challenge explanations: descriptions, hints, and reasoning for both challenges - Update README.md: increment total challenge count to 69 and add challenge links - Update challenge configuration: register new challenges in wrong-secrets-configuration.yaml - Demonstrates secrets exposure through AI agent skill files shipped in plain text format --- README.md | 6 +- .../challenges/docker/Challenge67.java | 48 +++++++++++ .../docker/Challenge67Controller.java | 45 ++++++++++ .../challenges/docker/Challenge68.java | 54 ++++++++++++ .../docker/Challenge68Controller.java | 72 ++++++++++++++++ .../challenge-67/challenge-67.snippet | 36 ++++++++ .../cursor-skill/deploy-preview/SKILL.md | 62 ++++++++++++++ .../challenge-68/challenge-68.snippet | 17 ++++ .../claude-skill/incident-reporter/SKILL.md | 41 ++++++++++ .../incident-reporter/references/runbook.md | 29 +++++++ .../scripts/upload_report.py | 46 +++++++++++ .../resources/explanations/challenge67.adoc | 12 +++ .../explanations/challenge67_hint.adoc | 3 + .../explanations/challenge67_reason.adoc | 24 ++++++ .../resources/explanations/challenge68.adoc | 14 ++++ .../explanations/challenge68_hint.adoc | 10 +++ .../explanations/challenge68_reason.adoc | 25 ++++++ .../wrong-secrets-configuration.yaml | 28 +++++++ .../docker/Challenge67ControllerTest.java | 41 ++++++++++ .../challenges/docker/Challenge67Test.java | 76 +++++++++++++++++ .../docker/Challenge68ControllerTest.java | 72 ++++++++++++++++ .../challenges/docker/Challenge68Test.java | 82 +++++++++++++++++++ 22 files changed, 841 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge67.java create mode 100644 src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge67Controller.java create mode 100644 src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge68.java create mode 100644 src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge68Controller.java create mode 100644 src/main/resources/challenges/challenge-67/challenge-67.snippet create mode 100644 src/main/resources/challenges/challenge-67/cursor-skill/deploy-preview/SKILL.md create mode 100644 src/main/resources/challenges/challenge-68/challenge-68.snippet create mode 100644 src/main/resources/challenges/challenge-68/claude-skill/incident-reporter/SKILL.md create mode 100644 src/main/resources/challenges/challenge-68/claude-skill/incident-reporter/references/runbook.md create mode 100644 src/main/resources/challenges/challenge-68/claude-skill/incident-reporter/scripts/upload_report.py create mode 100644 src/main/resources/explanations/challenge67.adoc create mode 100644 src/main/resources/explanations/challenge67_hint.adoc create mode 100644 src/main/resources/explanations/challenge67_reason.adoc create mode 100644 src/main/resources/explanations/challenge68.adoc create mode 100644 src/main/resources/explanations/challenge68_hint.adoc create mode 100644 src/main/resources/explanations/challenge68_reason.adoc create mode 100644 src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge67ControllerTest.java create mode 100644 src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge67Test.java create mode 100644 src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge68ControllerTest.java create mode 100644 src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge68Test.java diff --git a/README.md b/README.md index c95f9c2f76..56305d7cec 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Welcome to the OWASP WrongSecrets game! The game is packed with real life examples of how to _not_ store secrets in your software. Each of these examples is captured in a challenge, which you need to solve using various tools and techniques. Solving these challenges will help you recognize common mistakes & can help you to reflect on your own secrets management strategy. -Can you solve all the 67 challenges? +Can you solve all the 69 challenges? Try some of them on [our Heroku demo environment](https://wrongsecrets.herokuapp.com/). @@ -161,7 +161,7 @@ docker run -p 8080:8080 -p 8090:8090 ghcr.io/owasp/wrongsecrets/wrongsecrets-mas ⚠️ **Warning**: This is a development version built from the latest master branch and may contain experimental features or instabilities. **πŸ“ Note on Ports:** -- Port **8080**: Main application (challenges 0-66) +- Port **8080**: Main application (challenges 0-68) - Port **8090**: MCP server (required for Challenge 60) **πŸ“ Note on Challenge 62 (Google Drive MCP):** @@ -226,6 +226,8 @@ Now you can try to find the secrets by means of solving the challenge offered at - [localhost:8080/challenge/challenge-64](http://localhost:8080/challenge/challenge-64) - [localhost:8080/challenge/challenge-65](http://localhost:8080/challenge/challenge-65) - [localhost:8080/challenge/challenge-66](http://localhost:8080/challenge/challenge-66) +- [localhost:8080/challenge/challenge-67](http://localhost:8080/challenge/challenge-67) +- [localhost:8080/challenge/challenge-68](http://localhost:8080/challenge/challenge-68) Note that these challenges are still very basic, and so are their explanations. Feel free to file a PR to make them look diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge67.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge67.java new file mode 100644 index 0000000000..20f2727a26 --- /dev/null +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge67.java @@ -0,0 +1,48 @@ +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 that is hardcoded in a Cursor skill. The skill is shipped as a plain + * {@code SKILL.md} file, so the secret is readable for anybody who receives the skill. + */ +@Slf4j +@Component +public class Challenge67 extends FixedAnswerChallenge { + + private static final Pattern DEPLOY_TOKEN_PATTERN = + Pattern.compile("STAGING_DEPLOY_TOKEN=\"([^\"]+)\""); + + private final Resource skillFile; + + public Challenge67( + @Value("classpath:challenges/challenge-67/cursor-skill/deploy-preview/SKILL.md") + Resource skillFile) { + this.skillFile = skillFile; + } + + @Override + public String getAnswer() { + try { + var skillContent = skillFile.getContentAsString(StandardCharsets.UTF_8); + var matcher = DEPLOY_TOKEN_PATTERN.matcher(skillContent); + if (!matcher.find()) { + log.warn("Could not find the deploy token in the Cursor skill of challenge 67"); + return FILE_MOUNT_ERROR; + } + return matcher.group(1); + } catch (IOException e) { + log.warn("Exception while reading the Cursor skill of challenge 67", e); + return FILE_MOUNT_ERROR; + } + } +} diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge67Controller.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge67Controller.java new file mode 100644 index 0000000000..2c74ef0f1f --- /dev/null +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge67Controller.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 Cursor skill of challenge 67 straight from the resource folder, so participants can + * read the skill the same way an agent would. + */ +@Slf4j +@RestController +public class Challenge67Controller { + + private static final MediaType MARKDOWN = + new MediaType("text", "markdown", StandardCharsets.UTF_8); + + private final Resource skillFile; + + public Challenge67Controller( + @Value("classpath:challenges/challenge-67/cursor-skill/deploy-preview/SKILL.md") + Resource skillFile) { + this.skillFile = skillFile; + } + + /** Returns the raw {@code SKILL.md} of the {@code deploy-preview} Cursor skill. */ + @GetMapping("/skills/cursor/deploy-preview/SKILL.md") + public ResponseEntity cursorSkill() { + try { + return ResponseEntity.ok() + .contentType(MARKDOWN) + .body(skillFile.getContentAsString(StandardCharsets.UTF_8)); + } catch (IOException e) { + log.warn("Unable to serve the Cursor skill of challenge 67", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } +} diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge68.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge68.java new file mode 100644 index 0000000000..f0bc719bba --- /dev/null +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge68.java @@ -0,0 +1,54 @@ +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.Base64; +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 that is hardcoded in a Claude skill. The skill is distributed as a + * zip bundle, and the token does not sit in the {@code SKILL.md} itself but in one of the bundled + * scripts, base64 encoded to keep secret scanners quiet. + */ +@Slf4j +@Component +public class Challenge68 extends FixedAnswerChallenge { + + private static final Pattern UPLOAD_TOKEN_PATTERN = + Pattern.compile("UPLOAD_TOKEN_B64\\s*=\\s*\"([^\"]+)\""); + + private final Resource uploaderScript; + + public Challenge68( + @Value( + "classpath:challenges/challenge-68/claude-skill/incident-reporter/scripts/upload_report.py") + Resource uploaderScript) { + this.uploaderScript = uploaderScript; + } + + @Override + public String getAnswer() { + try { + var scriptContent = uploaderScript.getContentAsString(StandardCharsets.UTF_8); + var matcher = UPLOAD_TOKEN_PATTERN.matcher(scriptContent); + if (!matcher.find()) { + log.warn("Could not find the upload token in the Claude skill of challenge 68"); + return FILE_MOUNT_ERROR; + } + return new String(Base64.getDecoder().decode(matcher.group(1)), StandardCharsets.UTF_8); + } catch (IOException e) { + log.warn("Exception while reading the Claude skill of challenge 68", e); + return FILE_MOUNT_ERROR; + } catch (IllegalArgumentException e) { + log.warn("The upload token in the Claude skill of challenge 68 is not valid base64", e); + return FILE_MOUNT_ERROR; + } + } +} diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge68Controller.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge68Controller.java new file mode 100644 index 0000000000..c7862a8eb3 --- /dev/null +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge68Controller.java @@ -0,0 +1,72 @@ +package org.owasp.wrongsecrets.challenges.docker; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.ContentDisposition; +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 Claude skill of challenge 68. The skill files live in the resource folder and are + * zipped on request, so participants download the same kind of bundle that is passed around when a + * skill is shared. + */ +@Slf4j +@RestController +public class Challenge68Controller { + + static final String SKILL_ROOT = "challenges/challenge-68/claude-skill/"; + private static final String BUNDLE_NAME = "incident-reporter.zip"; + private static final MediaType ZIP = new MediaType("application", "zip"); + // Fixed timestamp (2024-01-01T00:00:00Z) so the generated bundle is reproducible. + private static final long FIXED_ENTRY_TIME = 1704067200000L; + + /** The files that make up the skill, named as they appear inside the bundle. */ + private static final List SKILL_FILES = + List.of( + "incident-reporter/SKILL.md", + "incident-reporter/references/runbook.md", + "incident-reporter/scripts/upload_report.py"); + + /** Returns the {@code incident-reporter} Claude skill as a downloadable zip bundle. */ + @GetMapping("/skills/claude/incident-reporter.zip") + public ResponseEntity claudeSkillBundle() { + try { + return ResponseEntity.ok() + .contentType(ZIP) + .headers( + headers -> + headers.setContentDisposition( + ContentDisposition.attachment().filename(BUNDLE_NAME).build())) + .body(zipSkill()); + } catch (IOException e) { + log.warn("Unable to package the Claude skill of challenge 68", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + /** Zips the skill files from the resource folder, keeping their paths inside the bundle. */ + byte[] zipSkill() throws IOException { + var bundle = new ByteArrayOutputStream(); + try (var zip = new ZipOutputStream(bundle)) { + for (String name : SKILL_FILES) { + var entry = new ZipEntry(name); + entry.setTime(FIXED_ENTRY_TIME); + zip.putNextEntry(entry); + try (var content = new ClassPathResource(SKILL_ROOT + name).getInputStream()) { + content.transferTo(zip); + } + zip.closeEntry(); + } + } + return bundle.toByteArray(); + } +} diff --git a/src/main/resources/challenges/challenge-67/challenge-67.snippet b/src/main/resources/challenges/challenge-67/challenge-67.snippet new file mode 100644 index 0000000000..5a6402e34d --- /dev/null +++ b/src/main/resources/challenges/challenge-67/challenge-67.snippet @@ -0,0 +1,36 @@ +
+

πŸ“„ Cursor skill: deploy-preview

+

This application ships a Cursor skill in its resource folder. The backend hosts it at /skills/cursor/deploy-preview/SKILL.md, which is exactly what an agent would read when the skill is installed.

+ +
+

Fetch the skill from the command line:

+
curl -s http://localhost:8080/skills/cursor/deploy-preview/SKILL.md
+ +

…or read it right here:

+ + Open in new tab + + +

πŸ’‘ The author inlined the shared staging deploy token so the skill would be "self-contained". Submit that token.

+
+
+ + + + diff --git a/src/main/resources/challenges/challenge-67/cursor-skill/deploy-preview/SKILL.md b/src/main/resources/challenges/challenge-67/cursor-skill/deploy-preview/SKILL.md new file mode 100644 index 0000000000..9ece562193 --- /dev/null +++ b/src/main/resources/challenges/challenge-67/cursor-skill/deploy-preview/SKILL.md @@ -0,0 +1,62 @@ +--- +name: deploy-preview +description: Deploy the branch that is currently checked out to the shared staging preview environment and report the preview URL back to the user. +--- + +# Deploy Preview + +Use this skill when the user asks you to "deploy a preview", "push this to +staging", or "get me a preview URL" for the branch that is currently checked +out. + +## When to use this skill + +- The user wants a throwaway preview environment for the current branch. +- The user wants to refresh a preview environment that already exists. + +Do **not** use this skill for production releases. Production deployments go +through the release pipeline and require a second approver. + +## Prerequisites + +The staging deploy API authenticates with a shared team token. Previously +everybody had to look this token up in the password manager, which slowed the +agent down, so the token is inlined here to keep the skill self-contained: + +```bash +export STAGING_DEPLOY_URL="https://staging.wrongsecrets.example.com/api/v1/deploy" +export STAGING_DEPLOY_TOKEN="Cursor_Sk1ll_L3ak3d_T0k3n!" +``` + +## Steps + +1. Verify the working tree is clean and the branch is pushed: + + ```bash + git status --porcelain + git push --set-upstream origin "$(git rev-parse --abbrev-ref HEAD)" + ``` + +2. Build the application: + + ```bash + ./mvnw --batch-mode --no-transfer-progress package -DskipTests + ``` + +3. Trigger the preview deployment: + + ```bash + curl --silent --show-error --request POST "$STAGING_DEPLOY_URL" \ + --header "Authorization: Bearer $STAGING_DEPLOY_TOKEN" \ + --header "Content-Type: application/json" \ + --data "{\"branch\": \"$(git rev-parse --abbrev-ref HEAD)\"}" + ``` + +4. Report the `preview_url` field from the response back to the user. + +## Troubleshooting + +- `401 Unauthorized`: the shared token was rotated. Ask the platform team for + the new value and update the `STAGING_DEPLOY_TOKEN` line above. +- `409 Conflict`: a preview for this branch is already being built. Wait for the + running deployment to finish and try again. diff --git a/src/main/resources/challenges/challenge-68/challenge-68.snippet b/src/main/resources/challenges/challenge-68/challenge-68.snippet new file mode 100644 index 0000000000..b036d28850 --- /dev/null +++ b/src/main/resources/challenges/challenge-68/challenge-68.snippet @@ -0,0 +1,17 @@ +
+

πŸ“¦ Claude skill bundle: incident-reporter

+

The skill files live in this application's resource folder. The backend packages them into a zip bundle on request, which is how a Claude skill is shared and installed.

+ +
+

Step 1 β€” download and unpack the bundle:

+
curl -sO http://localhost:8080/skills/claude/incident-reporter.zip
+unzip incident-reporter.zip
+find incident-reporter -type f
+ ⬇ Download incident-reporter.zip + +

Step 2 β€” the SKILL.md is clean. Look at what it tells the agent to run:

+
grep -r TOKEN incident-reporter/
+ +

πŸ’‘ The token you find is not the answer yet β€” the author "hid" it from the secret scanner. Submit the value the uploader actually authenticates with.

+
+
diff --git a/src/main/resources/challenges/challenge-68/claude-skill/incident-reporter/SKILL.md b/src/main/resources/challenges/challenge-68/claude-skill/incident-reporter/SKILL.md new file mode 100644 index 0000000000..8113ac0c76 --- /dev/null +++ b/src/main/resources/challenges/challenge-68/claude-skill/incident-reporter/SKILL.md @@ -0,0 +1,41 @@ +--- +name: incident-reporter +description: Assemble a structured incident report from the on-call runbook and upload it to the incident tracker. Use when the user asks to file, report, or close out an incident. +allowed-tools: Bash(python3:*), Read, Grep +--- + +# Incident Reporter + +Use this skill when the user asks you to file an incident, write up a postmortem +stub, or close out an incident that has just been mitigated. + +## Workflow + +1. Read `references/runbook.md` to work out which severity applies and which + fields the tracker expects for that severity. + +2. Assemble the report as a JSON object with the following keys: + + - `title` β€” one line, imperative, no incident number. + - `severity` β€” one of `sev1`, `sev2`, `sev3`. + - `started_at` / `mitigated_at` β€” RFC 3339 timestamps in UTC. + - `summary` β€” two or three sentences of plain prose. + - `contributing_factors` β€” a list of strings. + +3. Upload the report with the bundled uploader. It reads the JSON object from + standard input and prints the created record: + + ```bash + python3 scripts/upload_report.py < report.json + ``` + +4. Report the `id` and `url` from the uploader output back to the user. + +## Notes + +- The uploader handles authentication against the tracker on its own, so you do + not have to ask the user for credentials. See `scripts/upload_report.py` if you + need to know which endpoint is used. +- Never invent timestamps. If the user has not provided them, ask. +- `sev1` incidents additionally require a named incident commander. The runbook + explains how to look that up. diff --git a/src/main/resources/challenges/challenge-68/claude-skill/incident-reporter/references/runbook.md b/src/main/resources/challenges/challenge-68/claude-skill/incident-reporter/references/runbook.md new file mode 100644 index 0000000000..05e6529ef6 --- /dev/null +++ b/src/main/resources/challenges/challenge-68/claude-skill/incident-reporter/references/runbook.md @@ -0,0 +1,29 @@ +# On-call runbook (excerpt) + +This excerpt is bundled with the incident-reporter skill so the agent can decide +on a severity without needing access to the internal wiki. + +## Severity levels + +| Severity | Criteria | Required fields | +| -------- | ----------------------------------------------------------- | -------------------------------------------- | +| `sev1` | Customer-visible outage, or any confirmed data exposure. | All base fields plus `incident_commander`. | +| `sev2` | Degraded service, elevated error rates, no data exposure. | All base fields. | +| `sev3` | Internal-only impact, or a near miss worth writing down. | `title`, `severity`, `summary`. | + +## Base fields + +`title`, `severity`, `started_at`, `mitigated_at`, `summary`, +`contributing_factors`. + +## Finding the incident commander + +For `sev1` the incident commander is whoever acknowledged the page first. Look it +up in the paging tool timeline; do not guess. + +## Credentials + +The tracker credential is managed by the platform team. It is supposed to be +provided through the environment, but the current version of the uploader ships +with a copy of the token bundled in `scripts/upload_report.py` so the skill keeps +working on machines where the environment variable was never set. diff --git a/src/main/resources/challenges/challenge-68/claude-skill/incident-reporter/scripts/upload_report.py b/src/main/resources/challenges/challenge-68/claude-skill/incident-reporter/scripts/upload_report.py new file mode 100644 index 0000000000..9870be5157 --- /dev/null +++ b/src/main/resources/challenges/challenge-68/claude-skill/incident-reporter/scripts/upload_report.py @@ -0,0 +1,46 @@ +"""Upload a structured incident report to the incident tracker. + +This uploader is bundled with the incident-reporter skill. It reads a single +JSON object from standard input and POSTs it to the tracker. +""" + +import base64 +import json +import sys +import urllib.request + +TRACKER_URL = "https://incidents.wrongsecrets.example.com/api/v2/reports" + +# The tracker token is stored base64 encoded so that the secret scanner in CI +# stops flagging this file on every commit. +# TODO(on-call): read this from the vault instead, before the next audit. +UPLOAD_TOKEN_B64 = "Q2w0dWRlX1NrMWxsX1oxcF9TM2NyM3Qh" + + +def upload_token(): + """Return the plaintext tracker token used to authenticate the upload.""" + return base64.b64decode(UPLOAD_TOKEN_B64).decode("utf-8") + + +def upload(report): + """POST a single incident report to the tracker and return the created record.""" + request = urllib.request.Request( + TRACKER_URL, + data=json.dumps(report).encode("utf-8"), + headers={ + "Authorization": f"Bearer {upload_token()}", + "Content-Type": "application/json", + }, + method="POST", + ) + with urllib.request.urlopen(request) as response: + return json.load(response) + + +def main(): + report = json.load(sys.stdin) + print(json.dumps(upload(report), indent=2)) + + +if __name__ == "__main__": + main() diff --git a/src/main/resources/explanations/challenge67.adoc b/src/main/resources/explanations/challenge67.adoc new file mode 100644 index 0000000000..b991957d4d --- /dev/null +++ b/src/main/resources/explanations/challenge67.adoc @@ -0,0 +1,12 @@ +=== Challenge 67: Find the Secret in the Cursor Skill + +Agent skills are becoming a popular way to share automation with a team: you drop a folder with a `SKILL.md` in it, the agent picks it up, and everybody gets the same workflow. Unfortunately that also makes them a very convenient place to "temporarily" park a shared credential. + +This application ships a Cursor skill called `deploy-preview`. The skill lives in the resource folder and is hosted by the backend at link:/skills/cursor/deploy-preview/SKILL.md[`/skills/cursor/deploy-preview/SKILL.md`]. + +Read the skill and submit the staging deploy token that the author inlined "to keep the skill self-contained". + +[NOTE] +==== +Nothing is encoded or encrypted here: this is exactly what a leaked skill looks like in the wild. A skill is documentation, and documentation gets copied into wikis, tickets, pull requests and chat messages. +==== diff --git a/src/main/resources/explanations/challenge67_hint.adoc b/src/main/resources/explanations/challenge67_hint.adoc new file mode 100644 index 0000000000..c4043fcab3 --- /dev/null +++ b/src/main/resources/explanations/challenge67_hint.adoc @@ -0,0 +1,3 @@ +Open link:/skills/cursor/deploy-preview/SKILL.md[`/skills/cursor/deploy-preview/SKILL.md`] and look at the *Prerequisites* section. The value of `STAGING_DEPLOY_TOKEN` is the answer. + +The same file is in the source tree at `src/main/resources/challenges/challenge-67/cursor-skill/deploy-preview/SKILL.md`. diff --git a/src/main/resources/explanations/challenge67_reason.adoc b/src/main/resources/explanations/challenge67_reason.adoc new file mode 100644 index 0000000000..8511bf344b --- /dev/null +++ b/src/main/resources/explanations/challenge67_reason.adoc @@ -0,0 +1,24 @@ +*Why you should never put a secret in an agent skill* + +A skill is built to be shared. That is the whole point of it: you write the workflow once and every developer, agent and CI job reuses it. This means a secret in a skill has the worst possible properties: + +- It is copied to every machine that installs the skill, including personal laptops. +- It ends up in the repository, so it is in the git history forever, even after you "remove" it. +- It is published to skill marketplaces and registries, where it is indexed and searchable. +- It is read by the agent and can therefore be echoed into chat transcripts, logs and telemetry belonging to third parties. +- It is a *shared* credential, so it is almost never rotated and almost never scoped down. + +---- +What to do instead: + +- Keep credentials out of the skill and let the skill read them from the environment, for example `$STAGING_DEPLOY_TOKEN`. +- Document *which* variable is required and *where* to obtain it, never the value itself. +- Give every consumer their own short-lived, least-privilege credential instead of one shared team token. +- Scan skill files with a secret scanner in pre-commit and CI, just like you scan source code. +- Treat a secret that ever appeared in a skill as compromised and rotate it. +---- + +[NOTE] +==== +Skills, rules, prompts, MCP server configurations and agent instruction files are all code as far as your secret management is concerned. If you would not hardcode a token in a `.java` file, do not hardcode it in a `SKILL.md` either. +==== diff --git a/src/main/resources/explanations/challenge68.adoc b/src/main/resources/explanations/challenge68.adoc new file mode 100644 index 0000000000..aad64564db --- /dev/null +++ b/src/main/resources/explanations/challenge68.adoc @@ -0,0 +1,14 @@ +=== Challenge 68: Find the Secret in the Claude Skill Bundle + +Claude skills are distributed as a zip: a folder with a `SKILL.md` that describes when to use the skill, plus any scripts and reference material the skill needs. The whole bundle is unpacked on the machine that installs it. + +That makes a skill bundle a great hiding place, because reviewers tend to read the `SKILL.md` and stop there. The interesting part is usually in the files next to it. + +This application ships a Claude skill called `incident-reporter`. The skill files live in the resource folder and the backend serves them as a bundle at link:/skills/claude/incident-reporter.zip[`/skills/claude/incident-reporter.zip`]. + +Download the bundle, unpack it, and work out the token the uploader authenticates with. + +[NOTE] +==== +The `SKILL.md` is clean. Reading only the entry point of a skill is not a review. +==== diff --git a/src/main/resources/explanations/challenge68_hint.adoc b/src/main/resources/explanations/challenge68_hint.adoc new file mode 100644 index 0000000000..461053be11 --- /dev/null +++ b/src/main/resources/explanations/challenge68_hint.adoc @@ -0,0 +1,10 @@ +Download and unpack the bundle: + +---- +curl -sO http://localhost:8080/skills/claude/incident-reporter.zip +unzip incident-reporter.zip +---- + +The `SKILL.md` only points at the bundled uploader. Open `incident-reporter/scripts/upload_report.py` and look at `UPLOAD_TOKEN_B64`. + +That value is not the answer yet: the author base64 encoded it to stop the secret scanner from complaining. Decode it, for example with `base64 -d`, and submit the decoded token. diff --git a/src/main/resources/explanations/challenge68_reason.adoc b/src/main/resources/explanations/challenge68_reason.adoc new file mode 100644 index 0000000000..5c7fa77e0f --- /dev/null +++ b/src/main/resources/explanations/challenge68_reason.adoc @@ -0,0 +1,25 @@ +*Why a skill bundle needs the same review as your source code* + +A skill bundle is shipped software. It contains executable scripts, it runs on developer machines and in CI, and it is installed by people who did not write it. Treating it as "just some markdown" is how secrets end up being distributed at scale. + +Two failures stack up in this challenge: + +- The token is committed inside the bundle, so everyone who installs the skill receives a working shared credential. +- The token is base64 encoded, which is *encoding*, not encryption. It stops a naive secret scanner from firing, and it stops nobody else. Anyone with the bundle can decode it in one command. + +The second point is the more dangerous of the two, because it converts a detectable problem into an undetectable one. The scanner goes quiet, the finding disappears from the backlog, and the credential stays valid for years. + +---- +What to do instead: + +- Read credentials from the environment or a secret manager at runtime, and fail loudly with a clear message when they are absent. +- Never "fix" a secret scanner finding by encoding, splitting or obfuscating the value. Fix it by removing the secret and rotating it. +- Review every file in a skill bundle, not just the `SKILL.md`. Scripts, reference documents and sample configuration are all part of the attack surface. +- Verify the bundle in CI: unpack it and run your secret scanner over the extracted contents, including a base64-aware ruleset. +- Publish bundles from a build pipeline that has no access to production credentials in the first place. +---- + +[NOTE] +==== +Encoding is not a security control. If the only thing standing between an attacker and your credential is a `base64 -d`, the credential is public. +==== diff --git a/src/main/resources/wrong-secrets-configuration.yaml b/src/main/resources/wrong-secrets-configuration.yaml index 6871933e11..fa77208912 100644 --- a/src/main/resources/wrong-secrets-configuration.yaml +++ b/src/main/resources/wrong-secrets-configuration.yaml @@ -1013,3 +1013,31 @@ configurations: category: *bin ctf: enabled: true + + - name: Challenge 67 + short-name: "challenge-67" + sources: + - class-name: "org.owasp.wrongsecrets.challenges.docker.Challenge67" + explanation: "explanations/challenge67.adoc" + hint: "explanations/challenge67_hint.adoc" + reason: "explanations/challenge67_reason.adoc" + ui-snippet: "challenges/challenge-67/challenge-67.snippet" + environments: *all_envs + difficulty: *easy + category: *ai + ctf: + enabled: true + + - name: Challenge 68 + short-name: "challenge-68" + sources: + - class-name: "org.owasp.wrongsecrets.challenges.docker.Challenge68" + explanation: "explanations/challenge68.adoc" + hint: "explanations/challenge68_hint.adoc" + reason: "explanations/challenge68_reason.adoc" + ui-snippet: "challenges/challenge-68/challenge-68.snippet" + environments: *all_envs + difficulty: *normal + category: *ai + ctf: + enabled: true diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge67ControllerTest.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge67ControllerTest.java new file mode 100644 index 0000000000..60fc193a3c --- /dev/null +++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge67ControllerTest.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 Challenge67ControllerTest { + + private static final String SKILL_LOCATION = + "challenges/challenge-67/cursor-skill/deploy-preview/SKILL.md"; + + @Test + void shouldServeTheCursorSkillAsMarkdown() { + var controller = new Challenge67Controller(new ClassPathResource(SKILL_LOCATION)); + + var response = controller.cursorSkill(); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getHeaders().getContentType()).hasToString("text/markdown;charset=UTF-8"); + assertThat(response.getBody()).contains("name: deploy-preview", "STAGING_DEPLOY_TOKEN=\""); + } + + @Test + void servedSkillShouldContainTheAnswerOfTheChallenge() { + var controller = new Challenge67Controller(new ClassPathResource(SKILL_LOCATION)); + var challenge = new Challenge67(new ClassPathResource(SKILL_LOCATION)); + + assertThat(controller.cursorSkill().getBody()).contains(challenge.spoiler().solution()); + } + + @Test + void shouldReturnServerErrorWhenTheSkillIsMissing() { + var controller = + new Challenge67Controller(new ClassPathResource("challenges/challenge-67/missing.md")); + + assertThat(controller.cursorSkill().getStatusCode()) + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge67Test.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge67Test.java new file mode 100644 index 0000000000..e7b0d8fc83 --- /dev/null +++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge67Test.java @@ -0,0 +1,76 @@ +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 Challenge67Test { + + private static final String SKILL_LOCATION = + "challenges/challenge-67/cursor-skill/deploy-preview/SKILL.md"; + + private static Resource skillContaining(String content) { + return new ByteArrayResource(content.getBytes(StandardCharsets.UTF_8)); + } + + @Test + void spoilerShouldGiveTheTokenFromTheShippedSkill() { + var challenge = new Challenge67(new ClassPathResource(SKILL_LOCATION)); + + assertThat(challenge.spoiler().solution()).isNotEmpty().isNotEqualTo(FILE_MOUNT_ERROR); + assertThat(challenge.answerCorrect(challenge.spoiler().solution())).isTrue(); + } + + @Test + void shippedSkillShouldInlineTheTokenInsteadOfReadingItFromTheEnvironment() throws Exception { + var skill = new ClassPathResource(SKILL_LOCATION).getContentAsString(StandardCharsets.UTF_8); + + assertThat(skill).contains("STAGING_DEPLOY_TOKEN=\""); + } + + @Test + void shouldExtractTheTokenFromTheSkillFile() { + var challenge = + new Challenge67( + skillContaining( + """ + ## Prerequisites + + ```bash + export STAGING_DEPLOY_URL="https://staging.example.com/api/v1/deploy" + export STAGING_DEPLOY_TOKEN="t0k3n-from-the-skill" + ``` + """)); + + assertThat(challenge.spoiler().solution()).isEqualTo("t0k3n-from-the-skill"); + assertThat(challenge.answerCorrect("t0k3n-from-the-skill")).isTrue(); + } + + @Test + void incorrectAnswerShouldNotSolveChallenge() { + var challenge = new Challenge67(new ClassPathResource(SKILL_LOCATION)); + + assertThat(challenge.answerCorrect("wrong answer")).isFalse(); + assertThat(challenge.answerCorrect("")).isFalse(); + } + + @Test + void shouldReportAnErrorWhenTheSkillHasNoToken() { + var challenge = new Challenge67(skillContaining("# Deploy Preview\n\nNo secrets here.\n")); + + assertThat(challenge.spoiler().solution()).isEqualTo(FILE_MOUNT_ERROR); + } + + @Test + void shouldReportAnErrorWhenTheSkillCannotBeRead() { + var challenge = + new Challenge67(new ClassPathResource("challenges/challenge-67/does-not-exist.md")); + + assertThat(challenge.spoiler().solution()).isEqualTo(FILE_MOUNT_ERROR); + } +} diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge68ControllerTest.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge68ControllerTest.java new file mode 100644 index 0000000000..b83f97d333 --- /dev/null +++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge68ControllerTest.java @@ -0,0 +1,72 @@ +package org.owasp.wrongsecrets.challenges.docker; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipInputStream; +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.HttpStatus; + +class Challenge68ControllerTest { + + private static final String SKILL_MD = "incident-reporter/SKILL.md"; + private static final String UPLOADER = "incident-reporter/scripts/upload_report.py"; + private static final String RUNBOOK = "incident-reporter/references/runbook.md"; + + private static Map unzip(byte[] bundle) throws IOException { + var entries = new HashMap(); + try (var zip = new ZipInputStream(new ByteArrayInputStream(bundle))) { + for (var entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) { + entries.put(entry.getName(), new String(zip.readAllBytes(), StandardCharsets.UTF_8)); + } + } + return entries; + } + + @Test + void bundleShouldContainAllSkillFilesRelativeToTheSkillFolder() throws IOException { + var entries = unzip(new Challenge68Controller().zipSkill()); + + assertThat(entries).containsOnlyKeys(SKILL_MD, UPLOADER, RUNBOOK); + } + + @Test + void bundledSkillFileShouldNotContainTheSecret() throws IOException { + var entries = unzip(new Challenge68Controller().zipSkill()); + + assertThat(entries.get(SKILL_MD)).contains("name: incident-reporter"); + assertThat(entries.get(SKILL_MD)).doesNotContain("UPLOAD_TOKEN_B64"); + } + + @Test + void bundledUploaderShouldCarryTheEncodedTokenAndNotThePlaintextOne() throws IOException { + var entries = unzip(new Challenge68Controller().zipSkill()); + var challenge = + new Challenge68(new ClassPathResource(Challenge68Controller.SKILL_ROOT + UPLOADER)); + + assertThat(entries.get(UPLOADER)).contains("UPLOAD_TOKEN_B64 = \""); + assertThat(entries.get(UPLOADER)).doesNotContain(challenge.spoiler().solution()); + } + + @Test + void bundleShouldBeReproducible() throws IOException { + assertThat(new Challenge68Controller().zipSkill()) + .isEqualTo(new Challenge68Controller().zipSkill()); + } + + @Test + void shouldServeTheBundleAsAZipDownload() { + var response = new Challenge68Controller().claudeSkillBundle(); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getHeaders().getContentType()).hasToString("application/zip"); + assertThat(response.getHeaders().getContentDisposition().getFilename()) + .isEqualTo("incident-reporter.zip"); + assertThat(response.getBody()).isNotEmpty(); + } +} diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge68Test.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge68Test.java new file mode 100644 index 0000000000..a2e1d9215b --- /dev/null +++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge68Test.java @@ -0,0 +1,82 @@ +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 java.util.Base64; +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 Challenge68Test { + + private static final String UPLOADER_LOCATION = + "challenges/challenge-68/claude-skill/incident-reporter/scripts/upload_report.py"; + + private static Resource uploaderContaining(String content) { + return new ByteArrayResource(content.getBytes(StandardCharsets.UTF_8)); + } + + private static Resource uploaderWithToken(String token) { + var encoded = Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8)); + return uploaderContaining("UPLOAD_TOKEN_B64 = \"" + encoded + "\"\n"); + } + + @Test + void spoilerShouldGiveTheDecodedTokenFromTheShippedSkill() { + var challenge = new Challenge68(new ClassPathResource(UPLOADER_LOCATION)); + + assertThat(challenge.spoiler().solution()).isNotEmpty().isNotEqualTo(FILE_MOUNT_ERROR); + assertThat(challenge.answerCorrect(challenge.spoiler().solution())).isTrue(); + } + + @Test + void answerShouldNotBeTheEncodedValueThatIsInTheBundle() throws Exception { + var uploader = + new ClassPathResource(UPLOADER_LOCATION).getContentAsString(StandardCharsets.UTF_8); + var challenge = new Challenge68(new ClassPathResource(UPLOADER_LOCATION)); + + assertThat(uploader).contains("UPLOAD_TOKEN_B64 = \""); + assertThat(uploader).doesNotContain(challenge.spoiler().solution()); + } + + @Test + void shouldDecodeTheTokenFromTheUploaderScript() { + var challenge = new Challenge68(uploaderWithToken("t0k3n-from-the-bundle")); + + assertThat(challenge.spoiler().solution()).isEqualTo("t0k3n-from-the-bundle"); + assertThat(challenge.answerCorrect("t0k3n-from-the-bundle")).isTrue(); + } + + @Test + void incorrectAnswerShouldNotSolveChallenge() { + var challenge = new Challenge68(new ClassPathResource(UPLOADER_LOCATION)); + + assertThat(challenge.answerCorrect("wrong answer")).isFalse(); + assertThat(challenge.answerCorrect("")).isFalse(); + } + + @Test + void shouldReportAnErrorWhenTheUploaderHasNoToken() { + var challenge = new Challenge68(uploaderContaining("TRACKER_URL = \"https://example.com\"\n")); + + assertThat(challenge.spoiler().solution()).isEqualTo(FILE_MOUNT_ERROR); + } + + @Test + void shouldReportAnErrorWhenTheTokenIsNotValidBase64() { + var challenge = new Challenge68(uploaderContaining("UPLOAD_TOKEN_B64 = \"not base64 %%\"\n")); + + assertThat(challenge.spoiler().solution()).isEqualTo(FILE_MOUNT_ERROR); + } + + @Test + void shouldReportAnErrorWhenTheUploaderCannotBeRead() { + var challenge = + new Challenge68(new ClassPathResource("challenges/challenge-68/does-not-exist.py")); + + assertThat(challenge.spoiler().solution()).isEqualTo(FILE_MOUNT_ERROR); + } +} From 5f154b5e9e7466070e8a74ef3df59ac567d0fae4 Mon Sep 17 00:00:00 2001 From: kekubhai Date: Sun, 6 Sep 2026 10:29:16 +0530 Subject: [PATCH 2/3] feat(challenges): add challenges 70 and 71 with AI agent skills - Rename Challenge67 to Challenge70 with updated Cursor skill for deploy-preview - Rename Challenge68 to Challenge71 with updated Claude skill for incident-reporter - Update all controller classes and tests to reference new challenge numbers - Add challenge explanations, hints, and reasons documentation for challenges 70 and 71 - Update wrong-secrets-configuration.yaml with new challenge configurations - Update README.md to reference challenges 70 and 71 instead of 67 and 68 - Restructure resource files to match new challenge numbering scheme --- README.md | 4 +-- .../{Challenge67.java => Challenge70.java} | 10 +++---- ...roller.java => Challenge70Controller.java} | 10 +++---- .../{Challenge68.java => Challenge71.java} | 12 ++++---- ...roller.java => Challenge71Controller.java} | 8 ++--- .../challenge-70.snippet} | 0 .../cursor-skill/deploy-preview/SKILL.md | 0 .../challenge-71.snippet} | 0 .../claude-skill/incident-reporter/SKILL.md | 0 .../incident-reporter/references/runbook.md | 0 .../scripts/upload_report.py | 0 .../{challenge67.adoc => challenge70.adoc} | 2 +- ...enge67_hint.adoc => challenge70_hint.adoc} | 2 +- ...67_reason.adoc => challenge70_reason.adoc} | 0 .../{challenge68.adoc => challenge71.adoc} | 2 +- ...enge68_hint.adoc => challenge71_hint.adoc} | 0 ...68_reason.adoc => challenge71_reason.adoc} | 0 .../wrong-secrets-configuration.yaml | 30 +++++++++---------- ...st.java => Challenge70ControllerTest.java} | 12 ++++---- ...llenge67Test.java => Challenge70Test.java} | 14 ++++----- ...st.java => Challenge71ControllerTest.java} | 16 +++++----- ...llenge68Test.java => Challenge71Test.java} | 18 +++++------ 22 files changed, 70 insertions(+), 70 deletions(-) rename src/main/java/org/owasp/wrongsecrets/challenges/docker/{Challenge67.java => Challenge70.java} (88%) rename src/main/java/org/owasp/wrongsecrets/challenges/docker/{Challenge67Controller.java => Challenge70Controller.java} (83%) rename src/main/java/org/owasp/wrongsecrets/challenges/docker/{Challenge68.java => Challenge71.java} (89%) rename src/main/java/org/owasp/wrongsecrets/challenges/docker/{Challenge68Controller.java => Challenge71Controller.java} (93%) rename src/main/resources/challenges/{challenge-67/challenge-67.snippet => challenge-70/challenge-70.snippet} (100%) rename src/main/resources/challenges/{challenge-67 => challenge-70}/cursor-skill/deploy-preview/SKILL.md (100%) rename src/main/resources/challenges/{challenge-68/challenge-68.snippet => challenge-71/challenge-71.snippet} (100%) rename src/main/resources/challenges/{challenge-68 => challenge-71}/claude-skill/incident-reporter/SKILL.md (100%) rename src/main/resources/challenges/{challenge-68 => challenge-71}/claude-skill/incident-reporter/references/runbook.md (100%) rename src/main/resources/challenges/{challenge-68 => challenge-71}/claude-skill/incident-reporter/scripts/upload_report.py (100%) rename src/main/resources/explanations/{challenge67.adoc => challenge70.adoc} (93%) rename src/main/resources/explanations/{challenge67_hint.adoc => challenge70_hint.adoc} (81%) rename src/main/resources/explanations/{challenge67_reason.adoc => challenge70_reason.adoc} (100%) rename src/main/resources/explanations/{challenge68.adoc => challenge71.adoc} (93%) rename src/main/resources/explanations/{challenge68_hint.adoc => challenge71_hint.adoc} (100%) rename src/main/resources/explanations/{challenge68_reason.adoc => challenge71_reason.adoc} (100%) rename src/test/java/org/owasp/wrongsecrets/challenges/docker/{Challenge67ControllerTest.java => Challenge70ControllerTest.java} (73%) rename src/test/java/org/owasp/wrongsecrets/challenges/docker/{Challenge67Test.java => Challenge70Test.java} (84%) rename src/test/java/org/owasp/wrongsecrets/challenges/docker/{Challenge68ControllerTest.java => Challenge71ControllerTest.java} (82%) rename src/test/java/org/owasp/wrongsecrets/challenges/docker/{Challenge68Test.java => Challenge71Test.java} (81%) diff --git a/README.md b/README.md index 56305d7cec..9f0932ecf1 100644 --- a/README.md +++ b/README.md @@ -226,8 +226,8 @@ Now you can try to find the secrets by means of solving the challenge offered at - [localhost:8080/challenge/challenge-64](http://localhost:8080/challenge/challenge-64) - [localhost:8080/challenge/challenge-65](http://localhost:8080/challenge/challenge-65) - [localhost:8080/challenge/challenge-66](http://localhost:8080/challenge/challenge-66) -- [localhost:8080/challenge/challenge-67](http://localhost:8080/challenge/challenge-67) -- [localhost:8080/challenge/challenge-68](http://localhost:8080/challenge/challenge-68) +- [localhost:8080/challenge/challenge-70](http://localhost:8080/challenge/challenge-70) +- [localhost:8080/challenge/challenge-71](http://localhost:8080/challenge/challenge-71) Note that these challenges are still very basic, and so are their explanations. Feel free to file a PR to make them look diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge67.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge70.java similarity index 88% rename from src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge67.java rename to src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge70.java index 20f2727a26..b09a8b3059 100644 --- a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge67.java +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge70.java @@ -17,15 +17,15 @@ */ @Slf4j @Component -public class Challenge67 extends FixedAnswerChallenge { +public class Challenge70 extends FixedAnswerChallenge { private static final Pattern DEPLOY_TOKEN_PATTERN = Pattern.compile("STAGING_DEPLOY_TOKEN=\"([^\"]+)\""); private final Resource skillFile; - public Challenge67( - @Value("classpath:challenges/challenge-67/cursor-skill/deploy-preview/SKILL.md") + public Challenge70( + @Value("classpath:challenges/challenge-70/cursor-skill/deploy-preview/SKILL.md") Resource skillFile) { this.skillFile = skillFile; } @@ -36,12 +36,12 @@ public String getAnswer() { var skillContent = skillFile.getContentAsString(StandardCharsets.UTF_8); var matcher = DEPLOY_TOKEN_PATTERN.matcher(skillContent); if (!matcher.find()) { - log.warn("Could not find the deploy token in the Cursor skill of challenge 67"); + log.warn("Could not find the deploy token in the Cursor skill of challenge 70"); return FILE_MOUNT_ERROR; } return matcher.group(1); } catch (IOException e) { - log.warn("Exception while reading the Cursor skill of challenge 67", e); + log.warn("Exception while reading the Cursor skill of challenge 70", e); return FILE_MOUNT_ERROR; } } diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge67Controller.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge70Controller.java similarity index 83% rename from src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge67Controller.java rename to src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge70Controller.java index 2c74ef0f1f..25aa7a6e94 100644 --- a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge67Controller.java +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge70Controller.java @@ -12,20 +12,20 @@ import org.springframework.web.bind.annotation.RestController; /** - * Hosts the Cursor skill of challenge 67 straight from the resource folder, so participants can + * Hosts the Cursor skill of challenge 70 straight from the resource folder, so participants can * read the skill the same way an agent would. */ @Slf4j @RestController -public class Challenge67Controller { +public class Challenge70Controller { private static final MediaType MARKDOWN = new MediaType("text", "markdown", StandardCharsets.UTF_8); private final Resource skillFile; - public Challenge67Controller( - @Value("classpath:challenges/challenge-67/cursor-skill/deploy-preview/SKILL.md") + public Challenge70Controller( + @Value("classpath:challenges/challenge-70/cursor-skill/deploy-preview/SKILL.md") Resource skillFile) { this.skillFile = skillFile; } @@ -38,7 +38,7 @@ public ResponseEntity cursorSkill() { .contentType(MARKDOWN) .body(skillFile.getContentAsString(StandardCharsets.UTF_8)); } catch (IOException e) { - log.warn("Unable to serve the Cursor skill of challenge 67", e); + log.warn("Unable to serve the Cursor skill of challenge 70", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge68.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71.java similarity index 89% rename from src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge68.java rename to src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71.java index f0bc719bba..922b5c06e8 100644 --- a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge68.java +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71.java @@ -19,16 +19,16 @@ */ @Slf4j @Component -public class Challenge68 extends FixedAnswerChallenge { +public class Challenge71 extends FixedAnswerChallenge { private static final Pattern UPLOAD_TOKEN_PATTERN = Pattern.compile("UPLOAD_TOKEN_B64\\s*=\\s*\"([^\"]+)\""); private final Resource uploaderScript; - public Challenge68( + public Challenge71( @Value( - "classpath:challenges/challenge-68/claude-skill/incident-reporter/scripts/upload_report.py") + "classpath:challenges/challenge-71/claude-skill/incident-reporter/scripts/upload_report.py") Resource uploaderScript) { this.uploaderScript = uploaderScript; } @@ -39,15 +39,15 @@ public String getAnswer() { var scriptContent = uploaderScript.getContentAsString(StandardCharsets.UTF_8); var matcher = UPLOAD_TOKEN_PATTERN.matcher(scriptContent); if (!matcher.find()) { - log.warn("Could not find the upload token in the Claude skill of challenge 68"); + log.warn("Could not find the upload token in the Claude skill of challenge 71"); return FILE_MOUNT_ERROR; } return new String(Base64.getDecoder().decode(matcher.group(1)), StandardCharsets.UTF_8); } catch (IOException e) { - log.warn("Exception while reading the Claude skill of challenge 68", e); + log.warn("Exception while reading the Claude skill of challenge 71", e); return FILE_MOUNT_ERROR; } catch (IllegalArgumentException e) { - log.warn("The upload token in the Claude skill of challenge 68 is not valid base64", e); + log.warn("The upload token in the Claude skill of challenge 71 is not valid base64", e); return FILE_MOUNT_ERROR; } } diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge68Controller.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Controller.java similarity index 93% rename from src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge68Controller.java rename to src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Controller.java index c7862a8eb3..186f005f5b 100644 --- a/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge68Controller.java +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Controller.java @@ -15,15 +15,15 @@ import org.springframework.web.bind.annotation.RestController; /** - * Hosts the Claude skill of challenge 68. The skill files live in the resource folder and are + * Hosts the Claude skill of challenge 71. The skill files live in the resource folder and are * zipped on request, so participants download the same kind of bundle that is passed around when a * skill is shared. */ @Slf4j @RestController -public class Challenge68Controller { +public class Challenge71Controller { - static final String SKILL_ROOT = "challenges/challenge-68/claude-skill/"; + static final String SKILL_ROOT = "challenges/challenge-71/claude-skill/"; private static final String BUNDLE_NAME = "incident-reporter.zip"; private static final MediaType ZIP = new MediaType("application", "zip"); // Fixed timestamp (2024-01-01T00:00:00Z) so the generated bundle is reproducible. @@ -48,7 +48,7 @@ public ResponseEntity claudeSkillBundle() { ContentDisposition.attachment().filename(BUNDLE_NAME).build())) .body(zipSkill()); } catch (IOException e) { - log.warn("Unable to package the Claude skill of challenge 68", e); + log.warn("Unable to package the Claude skill of challenge 71", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } diff --git a/src/main/resources/challenges/challenge-67/challenge-67.snippet b/src/main/resources/challenges/challenge-70/challenge-70.snippet similarity index 100% rename from src/main/resources/challenges/challenge-67/challenge-67.snippet rename to src/main/resources/challenges/challenge-70/challenge-70.snippet diff --git a/src/main/resources/challenges/challenge-67/cursor-skill/deploy-preview/SKILL.md b/src/main/resources/challenges/challenge-70/cursor-skill/deploy-preview/SKILL.md similarity index 100% rename from src/main/resources/challenges/challenge-67/cursor-skill/deploy-preview/SKILL.md rename to src/main/resources/challenges/challenge-70/cursor-skill/deploy-preview/SKILL.md diff --git a/src/main/resources/challenges/challenge-68/challenge-68.snippet b/src/main/resources/challenges/challenge-71/challenge-71.snippet similarity index 100% rename from src/main/resources/challenges/challenge-68/challenge-68.snippet rename to src/main/resources/challenges/challenge-71/challenge-71.snippet diff --git a/src/main/resources/challenges/challenge-68/claude-skill/incident-reporter/SKILL.md b/src/main/resources/challenges/challenge-71/claude-skill/incident-reporter/SKILL.md similarity index 100% rename from src/main/resources/challenges/challenge-68/claude-skill/incident-reporter/SKILL.md rename to src/main/resources/challenges/challenge-71/claude-skill/incident-reporter/SKILL.md diff --git a/src/main/resources/challenges/challenge-68/claude-skill/incident-reporter/references/runbook.md b/src/main/resources/challenges/challenge-71/claude-skill/incident-reporter/references/runbook.md similarity index 100% rename from src/main/resources/challenges/challenge-68/claude-skill/incident-reporter/references/runbook.md rename to src/main/resources/challenges/challenge-71/claude-skill/incident-reporter/references/runbook.md diff --git a/src/main/resources/challenges/challenge-68/claude-skill/incident-reporter/scripts/upload_report.py b/src/main/resources/challenges/challenge-71/claude-skill/incident-reporter/scripts/upload_report.py similarity index 100% rename from src/main/resources/challenges/challenge-68/claude-skill/incident-reporter/scripts/upload_report.py rename to src/main/resources/challenges/challenge-71/claude-skill/incident-reporter/scripts/upload_report.py diff --git a/src/main/resources/explanations/challenge67.adoc b/src/main/resources/explanations/challenge70.adoc similarity index 93% rename from src/main/resources/explanations/challenge67.adoc rename to src/main/resources/explanations/challenge70.adoc index b991957d4d..d28a7a2461 100644 --- a/src/main/resources/explanations/challenge67.adoc +++ b/src/main/resources/explanations/challenge70.adoc @@ -1,4 +1,4 @@ -=== Challenge 67: Find the Secret in the Cursor Skill +=== Challenge 70: Find the Secret in the Cursor Skill Agent skills are becoming a popular way to share automation with a team: you drop a folder with a `SKILL.md` in it, the agent picks it up, and everybody gets the same workflow. Unfortunately that also makes them a very convenient place to "temporarily" park a shared credential. diff --git a/src/main/resources/explanations/challenge67_hint.adoc b/src/main/resources/explanations/challenge70_hint.adoc similarity index 81% rename from src/main/resources/explanations/challenge67_hint.adoc rename to src/main/resources/explanations/challenge70_hint.adoc index c4043fcab3..9b5ef8ec2c 100644 --- a/src/main/resources/explanations/challenge67_hint.adoc +++ b/src/main/resources/explanations/challenge70_hint.adoc @@ -1,3 +1,3 @@ Open link:/skills/cursor/deploy-preview/SKILL.md[`/skills/cursor/deploy-preview/SKILL.md`] and look at the *Prerequisites* section. The value of `STAGING_DEPLOY_TOKEN` is the answer. -The same file is in the source tree at `src/main/resources/challenges/challenge-67/cursor-skill/deploy-preview/SKILL.md`. +The same file is in the source tree at `src/main/resources/challenges/challenge-70/cursor-skill/deploy-preview/SKILL.md`. diff --git a/src/main/resources/explanations/challenge67_reason.adoc b/src/main/resources/explanations/challenge70_reason.adoc similarity index 100% rename from src/main/resources/explanations/challenge67_reason.adoc rename to src/main/resources/explanations/challenge70_reason.adoc diff --git a/src/main/resources/explanations/challenge68.adoc b/src/main/resources/explanations/challenge71.adoc similarity index 93% rename from src/main/resources/explanations/challenge68.adoc rename to src/main/resources/explanations/challenge71.adoc index aad64564db..c6b1505a89 100644 --- a/src/main/resources/explanations/challenge68.adoc +++ b/src/main/resources/explanations/challenge71.adoc @@ -1,4 +1,4 @@ -=== Challenge 68: Find the Secret in the Claude Skill Bundle +=== Challenge 71: Find the Secret in the Claude Skill Bundle Claude skills are distributed as a zip: a folder with a `SKILL.md` that describes when to use the skill, plus any scripts and reference material the skill needs. The whole bundle is unpacked on the machine that installs it. diff --git a/src/main/resources/explanations/challenge68_hint.adoc b/src/main/resources/explanations/challenge71_hint.adoc similarity index 100% rename from src/main/resources/explanations/challenge68_hint.adoc rename to src/main/resources/explanations/challenge71_hint.adoc diff --git a/src/main/resources/explanations/challenge68_reason.adoc b/src/main/resources/explanations/challenge71_reason.adoc similarity index 100% rename from src/main/resources/explanations/challenge68_reason.adoc rename to src/main/resources/explanations/challenge71_reason.adoc diff --git a/src/main/resources/wrong-secrets-configuration.yaml b/src/main/resources/wrong-secrets-configuration.yaml index fa77208912..071bafcb78 100644 --- a/src/main/resources/wrong-secrets-configuration.yaml +++ b/src/main/resources/wrong-secrets-configuration.yaml @@ -1014,28 +1014,28 @@ configurations: ctf: enabled: true - - name: Challenge 67 - short-name: "challenge-67" - sources: - - class-name: "org.owasp.wrongsecrets.challenges.docker.Challenge67" - explanation: "explanations/challenge67.adoc" - hint: "explanations/challenge67_hint.adoc" - reason: "explanations/challenge67_reason.adoc" - ui-snippet: "challenges/challenge-67/challenge-67.snippet" + - name: Challenge 70 + short-name: "challenge-70" + sources: + - class-name: "org.owasp.wrongsecrets.challenges.docker.Challenge70" + explanation: "explanations/challenge70.adoc" + hint: "explanations/challenge70_hint.adoc" + reason: "explanations/challenge70_reason.adoc" + ui-snippet: "challenges/challenge-70/challenge-70.snippet" environments: *all_envs difficulty: *easy category: *ai ctf: enabled: true - - name: Challenge 68 - short-name: "challenge-68" + - name: Challenge 71 + short-name: "challenge-71" sources: - - class-name: "org.owasp.wrongsecrets.challenges.docker.Challenge68" - explanation: "explanations/challenge68.adoc" - hint: "explanations/challenge68_hint.adoc" - reason: "explanations/challenge68_reason.adoc" - ui-snippet: "challenges/challenge-68/challenge-68.snippet" + - 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 diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge67ControllerTest.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge70ControllerTest.java similarity index 73% rename from src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge67ControllerTest.java rename to src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge70ControllerTest.java index 60fc193a3c..d22d4bd062 100644 --- a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge67ControllerTest.java +++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge70ControllerTest.java @@ -6,14 +6,14 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.http.HttpStatus; -class Challenge67ControllerTest { +class Challenge70ControllerTest { private static final String SKILL_LOCATION = - "challenges/challenge-67/cursor-skill/deploy-preview/SKILL.md"; + "challenges/challenge-70/cursor-skill/deploy-preview/SKILL.md"; @Test void shouldServeTheCursorSkillAsMarkdown() { - var controller = new Challenge67Controller(new ClassPathResource(SKILL_LOCATION)); + var controller = new Challenge70Controller(new ClassPathResource(SKILL_LOCATION)); var response = controller.cursorSkill(); @@ -24,8 +24,8 @@ void shouldServeTheCursorSkillAsMarkdown() { @Test void servedSkillShouldContainTheAnswerOfTheChallenge() { - var controller = new Challenge67Controller(new ClassPathResource(SKILL_LOCATION)); - var challenge = new Challenge67(new ClassPathResource(SKILL_LOCATION)); + var controller = new Challenge70Controller(new ClassPathResource(SKILL_LOCATION)); + var challenge = new Challenge70(new ClassPathResource(SKILL_LOCATION)); assertThat(controller.cursorSkill().getBody()).contains(challenge.spoiler().solution()); } @@ -33,7 +33,7 @@ void servedSkillShouldContainTheAnswerOfTheChallenge() { @Test void shouldReturnServerErrorWhenTheSkillIsMissing() { var controller = - new Challenge67Controller(new ClassPathResource("challenges/challenge-67/missing.md")); + new Challenge70Controller(new ClassPathResource("challenges/challenge-70/missing.md")); assertThat(controller.cursorSkill().getStatusCode()) .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge67Test.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge70Test.java similarity index 84% rename from src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge67Test.java rename to src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge70Test.java index e7b0d8fc83..7fc831c68b 100644 --- a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge67Test.java +++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge70Test.java @@ -9,10 +9,10 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -class Challenge67Test { +class Challenge70Test { private static final String SKILL_LOCATION = - "challenges/challenge-67/cursor-skill/deploy-preview/SKILL.md"; + "challenges/challenge-70/cursor-skill/deploy-preview/SKILL.md"; private static Resource skillContaining(String content) { return new ByteArrayResource(content.getBytes(StandardCharsets.UTF_8)); @@ -20,7 +20,7 @@ private static Resource skillContaining(String content) { @Test void spoilerShouldGiveTheTokenFromTheShippedSkill() { - var challenge = new Challenge67(new ClassPathResource(SKILL_LOCATION)); + var challenge = new Challenge70(new ClassPathResource(SKILL_LOCATION)); assertThat(challenge.spoiler().solution()).isNotEmpty().isNotEqualTo(FILE_MOUNT_ERROR); assertThat(challenge.answerCorrect(challenge.spoiler().solution())).isTrue(); @@ -36,7 +36,7 @@ void shippedSkillShouldInlineTheTokenInsteadOfReadingItFromTheEnvironment() thro @Test void shouldExtractTheTokenFromTheSkillFile() { var challenge = - new Challenge67( + new Challenge70( skillContaining( """ ## Prerequisites @@ -53,7 +53,7 @@ void shouldExtractTheTokenFromTheSkillFile() { @Test void incorrectAnswerShouldNotSolveChallenge() { - var challenge = new Challenge67(new ClassPathResource(SKILL_LOCATION)); + var challenge = new Challenge70(new ClassPathResource(SKILL_LOCATION)); assertThat(challenge.answerCorrect("wrong answer")).isFalse(); assertThat(challenge.answerCorrect("")).isFalse(); @@ -61,7 +61,7 @@ void incorrectAnswerShouldNotSolveChallenge() { @Test void shouldReportAnErrorWhenTheSkillHasNoToken() { - var challenge = new Challenge67(skillContaining("# Deploy Preview\n\nNo secrets here.\n")); + var challenge = new Challenge70(skillContaining("# Deploy Preview\n\nNo secrets here.\n")); assertThat(challenge.spoiler().solution()).isEqualTo(FILE_MOUNT_ERROR); } @@ -69,7 +69,7 @@ void shouldReportAnErrorWhenTheSkillHasNoToken() { @Test void shouldReportAnErrorWhenTheSkillCannotBeRead() { var challenge = - new Challenge67(new ClassPathResource("challenges/challenge-67/does-not-exist.md")); + new Challenge70(new ClassPathResource("challenges/challenge-70/does-not-exist.md")); assertThat(challenge.spoiler().solution()).isEqualTo(FILE_MOUNT_ERROR); } diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge68ControllerTest.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71ControllerTest.java similarity index 82% rename from src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge68ControllerTest.java rename to src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71ControllerTest.java index b83f97d333..648e5c8af4 100644 --- a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge68ControllerTest.java +++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71ControllerTest.java @@ -12,7 +12,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.http.HttpStatus; -class Challenge68ControllerTest { +class Challenge71ControllerTest { private static final String SKILL_MD = "incident-reporter/SKILL.md"; private static final String UPLOADER = "incident-reporter/scripts/upload_report.py"; @@ -30,14 +30,14 @@ private static Map unzip(byte[] bundle) throws IOException { @Test void bundleShouldContainAllSkillFilesRelativeToTheSkillFolder() throws IOException { - var entries = unzip(new Challenge68Controller().zipSkill()); + var entries = unzip(new Challenge71Controller().zipSkill()); assertThat(entries).containsOnlyKeys(SKILL_MD, UPLOADER, RUNBOOK); } @Test void bundledSkillFileShouldNotContainTheSecret() throws IOException { - var entries = unzip(new Challenge68Controller().zipSkill()); + var entries = unzip(new Challenge71Controller().zipSkill()); assertThat(entries.get(SKILL_MD)).contains("name: incident-reporter"); assertThat(entries.get(SKILL_MD)).doesNotContain("UPLOAD_TOKEN_B64"); @@ -45,9 +45,9 @@ void bundledSkillFileShouldNotContainTheSecret() throws IOException { @Test void bundledUploaderShouldCarryTheEncodedTokenAndNotThePlaintextOne() throws IOException { - var entries = unzip(new Challenge68Controller().zipSkill()); + var entries = unzip(new Challenge71Controller().zipSkill()); var challenge = - new Challenge68(new ClassPathResource(Challenge68Controller.SKILL_ROOT + UPLOADER)); + new Challenge71(new ClassPathResource(Challenge71Controller.SKILL_ROOT + UPLOADER)); assertThat(entries.get(UPLOADER)).contains("UPLOAD_TOKEN_B64 = \""); assertThat(entries.get(UPLOADER)).doesNotContain(challenge.spoiler().solution()); @@ -55,13 +55,13 @@ void bundledUploaderShouldCarryTheEncodedTokenAndNotThePlaintextOne() throws IOE @Test void bundleShouldBeReproducible() throws IOException { - assertThat(new Challenge68Controller().zipSkill()) - .isEqualTo(new Challenge68Controller().zipSkill()); + assertThat(new Challenge71Controller().zipSkill()) + .isEqualTo(new Challenge71Controller().zipSkill()); } @Test void shouldServeTheBundleAsAZipDownload() { - var response = new Challenge68Controller().claudeSkillBundle(); + var response = new Challenge71Controller().claudeSkillBundle(); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getHeaders().getContentType()).hasToString("application/zip"); diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge68Test.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Test.java similarity index 81% rename from src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge68Test.java rename to src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Test.java index a2e1d9215b..f03bfaee89 100644 --- a/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge68Test.java +++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/Challenge71Test.java @@ -10,10 +10,10 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -class Challenge68Test { +class Challenge71Test { private static final String UPLOADER_LOCATION = - "challenges/challenge-68/claude-skill/incident-reporter/scripts/upload_report.py"; + "challenges/challenge-71/claude-skill/incident-reporter/scripts/upload_report.py"; private static Resource uploaderContaining(String content) { return new ByteArrayResource(content.getBytes(StandardCharsets.UTF_8)); @@ -26,7 +26,7 @@ private static Resource uploaderWithToken(String token) { @Test void spoilerShouldGiveTheDecodedTokenFromTheShippedSkill() { - var challenge = new Challenge68(new ClassPathResource(UPLOADER_LOCATION)); + var challenge = new Challenge71(new ClassPathResource(UPLOADER_LOCATION)); assertThat(challenge.spoiler().solution()).isNotEmpty().isNotEqualTo(FILE_MOUNT_ERROR); assertThat(challenge.answerCorrect(challenge.spoiler().solution())).isTrue(); @@ -36,7 +36,7 @@ void spoilerShouldGiveTheDecodedTokenFromTheShippedSkill() { void answerShouldNotBeTheEncodedValueThatIsInTheBundle() throws Exception { var uploader = new ClassPathResource(UPLOADER_LOCATION).getContentAsString(StandardCharsets.UTF_8); - var challenge = new Challenge68(new ClassPathResource(UPLOADER_LOCATION)); + var challenge = new Challenge71(new ClassPathResource(UPLOADER_LOCATION)); assertThat(uploader).contains("UPLOAD_TOKEN_B64 = \""); assertThat(uploader).doesNotContain(challenge.spoiler().solution()); @@ -44,7 +44,7 @@ void answerShouldNotBeTheEncodedValueThatIsInTheBundle() throws Exception { @Test void shouldDecodeTheTokenFromTheUploaderScript() { - var challenge = new Challenge68(uploaderWithToken("t0k3n-from-the-bundle")); + var challenge = new Challenge71(uploaderWithToken("t0k3n-from-the-bundle")); assertThat(challenge.spoiler().solution()).isEqualTo("t0k3n-from-the-bundle"); assertThat(challenge.answerCorrect("t0k3n-from-the-bundle")).isTrue(); @@ -52,7 +52,7 @@ void shouldDecodeTheTokenFromTheUploaderScript() { @Test void incorrectAnswerShouldNotSolveChallenge() { - var challenge = new Challenge68(new ClassPathResource(UPLOADER_LOCATION)); + var challenge = new Challenge71(new ClassPathResource(UPLOADER_LOCATION)); assertThat(challenge.answerCorrect("wrong answer")).isFalse(); assertThat(challenge.answerCorrect("")).isFalse(); @@ -60,14 +60,14 @@ void incorrectAnswerShouldNotSolveChallenge() { @Test void shouldReportAnErrorWhenTheUploaderHasNoToken() { - var challenge = new Challenge68(uploaderContaining("TRACKER_URL = \"https://example.com\"\n")); + var challenge = new Challenge71(uploaderContaining("TRACKER_URL = \"https://example.com\"\n")); assertThat(challenge.spoiler().solution()).isEqualTo(FILE_MOUNT_ERROR); } @Test void shouldReportAnErrorWhenTheTokenIsNotValidBase64() { - var challenge = new Challenge68(uploaderContaining("UPLOAD_TOKEN_B64 = \"not base64 %%\"\n")); + var challenge = new Challenge71(uploaderContaining("UPLOAD_TOKEN_B64 = \"not base64 %%\"\n")); assertThat(challenge.spoiler().solution()).isEqualTo(FILE_MOUNT_ERROR); } @@ -75,7 +75,7 @@ void shouldReportAnErrorWhenTheTokenIsNotValidBase64() { @Test void shouldReportAnErrorWhenTheUploaderCannotBeRead() { var challenge = - new Challenge68(new ClassPathResource("challenges/challenge-68/does-not-exist.py")); + new Challenge71(new ClassPathResource("challenges/challenge-71/does-not-exist.py")); assertThat(challenge.spoiler().solution()).isEqualTo(FILE_MOUNT_ERROR); } From 92125f1e7258cfa5505ccf34bd956afd25302606 Mon Sep 17 00:00:00 2001 From: Jeroen Willemsen Date: Sun, 6 Sep 2026 08:28:26 +0200 Subject: [PATCH 3/3] added claude skill --- .../claude-skill/incident-reporter.skill | Bin 0 -> 3364 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/main/resources/challenges/challenge-71/claude-skill/incident-reporter.skill diff --git a/src/main/resources/challenges/challenge-71/claude-skill/incident-reporter.skill b/src/main/resources/challenges/challenge-71/claude-skill/incident-reporter.skill new file mode 100644 index 0000000000000000000000000000000000000000..83cc2f46c297c00f2ee375b58aeeddc8e7f45fc6 GIT binary patch literal 3364 zcmbtWc{r47A0FAWWt6fd*$P8LXtG6?jAiUg_Fa}Sh8c|I7(!tG{FkZy1nw}Afxns;C502kynMBu;0Wdbn$tpxkyn(FE5>r4B(9#jgw-%Eiq z>rI;1Jkq=?X+mbnt<`Djj~TOf6dgpKD4ewO=f6#%))!GEOw-^aimFjE^LjU=)Ee;< zsURgmJf8E;QpjT8Bk}6WS~jH~+GJIxxbl})j7stE*EH2SyDyK*@x%j*t`d8NNJ^gP*B%~=-bYVzwW3w$mBdl4R*E3VLwjZn{%>RO%T z+o{hBCNq$Z#s(^wDBG}ZsNJZHAREnmR2$?kKZT%!6(q}gJ`qZgHFwa#k+ntP~QQn;OIS`mz^tHf@R@ZL9{9YRFangh~9ofarL*r8GI}{yvi}HoOFUO zqn?}zqOSQ^q7=#&>LvF<8tVt8AgDq5f|d*c$09OrHg zvra!2R&c)z<=H^D;k2^b{79lxlIS4BeSfG)9@5()W1&bmNr;iI@q@&)Gz;AHWg5Mw zQFf}8qEIr*y`qMBnc)TrZ-K2O0%=AKZlImL3xeYNSeBDF1tD_}o;-{|Gsdf0*KI_; zZq8h5Cj*OGAhswDdo#=!^*_0=3FNl?VQ6&)_h>w;m@#GbQ*&mrogAjhpkC|@yV4T1 z7L!D*{a?T3>qZt>Fc+o5$Y)&2&3Gq&iLK6BX0-KTkt&k+~h3!lfJ@o#n@Ct zP?0wWXHy}cTL6*o5_@kMLKt-^i5a6p?%5r&a{YS9NRN>z`GvPEh!y}4V*mhtnzDYNzE9ZCz1Z>#fl3(s;%!JWj4uH5puU@Y!57kJ5vXuw^=FY3Oq z2&C#f^YRrYsVA%IcrD$lyvW1AhZ(EAQy@LDF1AW)HlON1xvJE&=O#8zTo0b zesrGsZEIE5lAL|j3)sZRAnyBa?m?FIMw*Y${! zDEW=$JEdstdW_fCLS*Hu-M|H}J~e)k1hHjHg|tz?bOo`}<0H#RzLf6F#pBLpZ1oA9 z=;5twmKvT2KN*bFb#M^V=IO}d`?s`J)(QtTbSTmML)A7%3ATbl>~wixc`6~WNlz^K zt?Vxro}92AwpF`Xjw}4o49~2gyIfs1ROWNpAeF7iI9)7V^V&vk6+qA?a=f^wsqUKa zMI&dW^MMXJ&^}=^;|b=9$wUdpam#9n5G4a1cE(~LL$JLg1EF!!Z4{{3p2!}crGQdO zw)g4Lm~H@PUM!VLk;_@+)BV7IQitcXpGz@Zf1{s@mhZ22SFU--pA}2aX$U?#b6bQ4 zSvAAXBeCMXpjJBE=6oV8N14oIo{rHp#BI}m@Ai4B!pD$906+l!KXtprzTN)A+7Lc| zPOc8WxwAAPqv26zQ-*Wrj)ape6RTw^%!UaGgK&Z>!saIZ)iYXexf?bLdBDT-yI?N07^g?i^0e1>b|vYhbF{pg69Tw&f9 zj7t_ zd(v;kquDeIj;J!Hi7%xlSupaXRx6c@z=2WudEJtd1aY6G6+4c_46cgTxb|bEwwsYu zM5qb`0`o{MOYp})!*o-d+NOfOTu6h(xrDx}N5bMTX`jEyH}NEnE2^!fKf^o*`?2CJ za6ye7Ov`!O`uJ7CJ*c*9Uf7ws`z9+_IC(*mXQ3f@u!CutQ0lpbNO$j&t501c!XAT5 zCbyq9W}TOLHN3>AHn(=N!%(Ff)G=F$6Z{&)cD1uD2|WcgNa~I)(4Vp^N^8B+65b&? zveRm+730wrN$eN$$iYQLbJ=ru)+DBqRkCVpne|vLhj6M2;^uzhA|ODH&(~YBk@9nno z>3nPQUiG)I^x=I5JQoo4Sf9emhn}&PaS!D{-6j=NnKyCh_LbM-0|9wnr#n+N^@YkJph027!%)f{z7a9-G2CcO7ss#eE`|s9+3DGSuEpzWIu}a azvko*KKgIj0{~$7c3XYxwc5kqzy1sAfh|7( literal 0 HcmV?d00001