-
-
Notifications
You must be signed in to change notification settings - Fork 618
feat(challenges): add challenges 70 and 71 with AI agent skills #2647
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
b5bd55b
5f154b5
92125f1
91076eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 Challenge70 extends FixedAnswerChallenge { | ||
|
|
||
| private static final Pattern DEPLOY_TOKEN_PATTERN = | ||
| Pattern.compile("STAGING_DEPLOY_TOKEN=\"([^\"]+)\""); | ||
|
|
||
| private final Resource skillFile; | ||
|
|
||
| public Challenge70( | ||
| @Value("classpath:challenges/challenge-70/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 70"); | ||
| return FILE_MOUNT_ERROR; | ||
| } | ||
| return matcher.group(1); | ||
| } catch (IOException e) { | ||
| log.warn("Exception while reading the Cursor skill of challenge 70", e); | ||
| return FILE_MOUNT_ERROR; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 70 straight from the resource folder, so participants can | ||
| * read the skill the same way an agent would. | ||
| */ | ||
| @Slf4j | ||
| @RestController | ||
| public class Challenge70Controller { | ||
|
|
||
| private static final MediaType MARKDOWN = | ||
| new MediaType("text", "markdown", StandardCharsets.UTF_8); | ||
|
|
||
| private final Resource skillFile; | ||
|
|
||
| public Challenge70Controller( | ||
| @Value("classpath:challenges/challenge-70/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<String> 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 70", e); | ||
| return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 Challenge71 extends FixedAnswerChallenge { | ||
|
|
||
| private static final Pattern UPLOAD_TOKEN_PATTERN = | ||
| Pattern.compile("UPLOAD_TOKEN_B64\\s*=\\s*\"([^\"]+)\""); | ||
|
|
||
| private final Resource uploaderScript; | ||
|
|
||
| public Challenge71( | ||
| @Value( | ||
| "classpath:challenges/challenge-71/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 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 71", e); | ||
| return FILE_MOUNT_ERROR; | ||
| } catch (IllegalArgumentException e) { | ||
| log.warn("The upload token in the Claude skill of challenge 71 is not valid base64", e); | ||
| return FILE_MOUNT_ERROR; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 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 Challenge71Controller { | ||
|
|
||
| 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. | ||
| 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<String> 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<byte[]> 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 71", 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(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,36 @@ | ||||||||||||
| <div id="cursor-skill-container" style="border: 1px solid #ccc; border-radius: 8px; padding: 20px; margin: 20px; background-color: #f9f9f9;"> | ||||||||||||
| <h4>📄 Cursor skill: <code>deploy-preview</code></h4> | ||||||||||||
| <p>This application ships a Cursor skill in its resource folder. The backend hosts it at <code>/skills/cursor/deploy-preview/SKILL.md</code>, which is exactly what an agent would read when the skill is installed.</p> | ||||||||||||
|
|
||||||||||||
| <div class="skill-warning" style="border: 1px solid #ffeaa7; border-radius: 6px; padding: 15px; margin: 15px 0;"> | ||||||||||||
| <p>Fetch the skill from the command line:</p> | ||||||||||||
| <pre class="skill-code" style="padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 13px;">curl -s http://localhost:8080/skills/cursor/deploy-preview/SKILL.md</pre> | ||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would this work?
Suggested change
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, that makes sense. I’ll use the request’s scheme, host, and port so the curl command works regardless of where the application is running. |
||||||||||||
|
|
||||||||||||
| <p style="margin-top: 12px;">…or read it right here:</p> | ||||||||||||
| <button class="btn btn-warning btn-sm skill-btn" onclick="loadCursorSkill()">▶ Show SKILL.md</button> | ||||||||||||
| <a class="btn btn-secondary btn-sm skill-btn" href="/skills/cursor/deploy-preview/SKILL.md" target="_blank" rel="noopener">Open in new tab</a> | ||||||||||||
| <pre id="cursor-skill-output" class="skill-output" style="display:none; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 12px; margin-top: 8px; white-space: pre-wrap; word-break: break-word;"></pre> | ||||||||||||
|
|
||||||||||||
| <p style="margin-top: 12px;"><small class="skill-tip">💡 The author inlined the shared staging deploy token so the skill would be "self-contained". Submit that token.</small></p> | ||||||||||||
| </div> | ||||||||||||
| </div> | ||||||||||||
|
|
||||||||||||
| <style> | ||||||||||||
| .skill-output { | ||||||||||||
| background-color: #f8f9fa !important; | ||||||||||||
| color: #212529 !important; | ||||||||||||
| border: 1px solid #dee2e6 !important; | ||||||||||||
| } | ||||||||||||
| </style> | ||||||||||||
|
|
||||||||||||
| <script> | ||||||||||||
| function loadCursorSkill() { | ||||||||||||
| const out = document.getElementById('cursor-skill-output'); | ||||||||||||
| out.style.display = 'block'; | ||||||||||||
| out.textContent = 'Loading…'; | ||||||||||||
| fetch('/skills/cursor/deploy-preview/SKILL.md') | ||||||||||||
| .then(function(r) { return r.text(); }) | ||||||||||||
| .then(function(text) { out.textContent = text; }) | ||||||||||||
| .catch(function(err) { out.textContent = 'Failed to load the skill file: ' + (err.message || err); }); | ||||||||||||
| } | ||||||||||||
| </script> | ||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| <div id="claude-skill-container" style="border: 1px solid #ccc; border-radius: 8px; padding: 20px; margin: 20px; background-color: #f9f9f9;"> | ||
| <h4>📦 Claude skill bundle: <code>incident-reporter</code></h4> | ||
| <p>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.</p> | ||
|
|
||
| <div class="skill-warning" style="border: 1px solid #ffeaa7; border-radius: 6px; padding: 15px; margin: 15px 0;"> | ||
| <p>Step 1 — download and unpack the bundle:</p> | ||
| <pre class="skill-code" style="padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 13px;">curl -sO http://localhost:8080/skills/claude/incident-reporter.zip | ||
| unzip incident-reporter.zip | ||
| find incident-reporter -type f</pre> | ||
| <a class="btn btn-warning btn-sm skill-btn" href="/skills/claude/incident-reporter.zip">⬇ Download incident-reporter.zip</a> | ||
|
|
||
| <p style="margin-top: 12px;">Step 2 — the <code>SKILL.md</code> is clean. Look at what it tells the agent to run:</p> | ||
| <pre class="skill-code" style="padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 13px;">grep -r TOKEN incident-reporter/</pre> | ||
|
|
||
| <p style="margin-top: 12px;"><small class="skill-tip">💡 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.</small></p> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is super clever :D , it now behaves like a skill. but given the markdown files are readable/searchable on source it makes it a little too easy. Please export the skill and replace it.