The Practice Challenge Bank
The CDP hands you a broken environment and a task list, not four bubbles to pick between. So this page does the same: eight self-contained challenges, one per blueprint domain, each built the way the real exam is built — a scenario that explains why the environment is in the state it's in, a numbered task list, a "done when" check you run yourself, a worked solution behind a spoiler, and a points-based rubric so you can score your own attempt honestly without anyone standing over your shoulder. There is no multiple choice on this page, on purpose. Work each one cold, timed, and unaided first — the worked solution is for after you've genuinely tried, not instead of trying.
This isn't a quiz with four bubbles. It's eight little "here's a mess, go fix it" puzzles — a login page nobody scanned for weaknesses, a Terraform file that leaves a door wide open, a spreadsheet full of security findings when you only have time to fix five. Each puzzle comes with a checklist for what "fixed" actually looks like, and a scorecard so you can grade your own homework instead of needing a teacher to check it.
How to drill this bank
☺ Like you're 10: Try it yourself first, with a timer running. Only peek at the answer once you're stuck or finished — peeking first teaches your eyes to recognize, not your hands to build.
Reading a worked solution feels like progress and mostly isn't — the CDP doesn't ask you to recognize correct YAML or a correct sed one-liner, it asks you to produce one, against a live environment, with the clock running and no chatbot allowed. Four rules keep this bank honest:
- Attempt cold. Empty terminal, no half-finished file left over from last time. That's how the exam starts you.
- Time-box it. Give each challenge 45–70 minutes on a first pass — the real exam runs 5 challenges in 6 hours, so that's roughly its own per-challenge pace, and a couple of these (dynamic analysis, IaC) run heavier than a couple of the others (foundations, DSOMM).
- Verify, don't eyeball. Every challenge has a "done when" line — a command whose output either confirms the fix or doesn't. "I think that's right" isn't a verification step; a re-run scan showing zero findings is.
- Score yourself with the rubric, then re-drill anything under 70. A challenge you score honestly and fail teaches more than one you skim and assume you'd have gotten.
"I changed the config" and "I changed the config, and here is the scan proving it worked" are different claims — and only the second one is worth points on the real exam or on this page. Every rubric below rewards the proof step as heavily as the fix itself, because that's exactly how the CDP's own report window is graded.
The eight challenges at a glance
☺ Like you're 10: One puzzle per topic this course teaches — work them in any order, but don't skip the ones that sound boring, because those are usually the ones with the sneakiest traps.
The CDP itself doesn't publish a domain weighting the way, say, the CKS publishes percentages — see the CDP exam guide for why that matters on exam day. This bank instead maps one challenge onto each of the nine-chapter exam blueprint's eight content domains, so working the whole bank once is working the whole blueprint once.
| # | Challenge | Blueprint domain | Practice time |
|---|---|---|---|
| C1 | Wire the shift-left gates into a pipeline that has none | Foundations & the CDP Toolchain | ~45 min |
| C2 | Score a pipeline against DSOMM, then raise its level | Secure SDLC Gates & DSOMM | ~45 min |
| C3 | Find the SQLi, and the key that's still in history | Static Analysis & Secrets Detection | ~60 min |
| C4 | Trace a transitive CVE and decide if it's reachable | Software Composition Analysis | ~60 min |
| C5 | Run an authenticated scan and separate signal from noise | Dynamic Analysis in Practice | ~70 min |
| C6 | Fix the Terraform plan before anyone applies it | Infrastructure as Code Hardening | ~60 min |
| C7 | Turn a CIS checklist into automated, re-runnable evidence | Compliance as Code at Scale | ~60 min |
| C8 | 40 findings, one afternoon — which five do you fix | Vulnerability Management & Triage | ~50 min |
Shift-left foundations — pipeline and gates
☺ Like you're 10: Before you can practice finding bugs, you need a pipeline that actually stops a bad commit from getting through — these two challenges build that stopping power.
C1 · Wire the shift-left gates into a pipeline that has none
You've inherited a repo whose CI runs unit tests and nothing else. No secrets scan, no static analysis, no dependency check — and nobody would notice if any of the three landed in main tomorrow.
Given — .github/workflows/ci.yml:
name: CI
on: [push, pull_request]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm testYour task:
- Add a secrets-scan job that scans the full git history (not just the working tree) with gitleaks and fails the pipeline on any finding.
- Add a sast job that runs Semgrep against the repo and fails on any ERROR-severity finding.
- Add an sca job that scans the resolved dependency tree with Trivy for fixable HIGH/CRITICAL CVEs, and make all three new jobs required status checks alongside
build-and-test.
Done when: a branch that adds a hardcoded AWS_SECRET_ACCESS_KEY string and reintroduces lodash@4.17.15 (a known-vulnerable version) fails both the secrets-scan and sca jobs; a clean branch passes all four.
Show the worked solution
name: CI
on: [push, pull_request]
jobs:
secrets-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 } # full history, not a shallow clone
- uses: gitleaks/gitleaks-action@v2
env: { GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} }
# gitleaks-action exits non-zero on any finding by default
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: semgrep/semgrep-action@v1
with: { config: auto } # --error is the action's default behavior
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
sca:
needs: build-and-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aquasecurity/trivy-action@0.24.0
with:
scan-type: fs
severity: HIGH,CRITICAL
ignore-unfixed: true
exit-code: 1Why: a job that runs but doesn't fail the build is a signal, not a gate — the difference is whether its exit code can block a merge, which is why "required status checks" on branch protection is step three, not an afterthought. Order matters too: secrets and SAST run first because they're cheap and catch the worst outcomes (a leaked credential, an injectable query) before spending CI minutes on a build; SCA runs after build-and-test here because it needs the resolved lockfile, though trivy fs can also read manifests directly without an install step if you want it to run in parallel instead. fetch-depth: 0 is the detail people skip on gitleaks — a shallow clone only has the latest commit, so a secret added and removed two commits ago never gets scanned at all.
If this one exposed gaps in the vocabulary itself — gate vs. signal, shift-left, CALMS — that's exactly what chapters 1–2 of the blueprint are for. Tool references: gitleaks, Semgrep, Trivy.
C2 · Score a pipeline against DSOMM, then raise its level
The same repo's sast job (from C1, or an equivalent one already in place) runs on every push — but it's configured with continue-on-error: true, and no branch protection rule references it. It has been silently red for six weeks and nobody noticed.
Your task:
- Using the OWASP DevSecOps Maturity Model (DSOMM)'s general framing — activities move from ad hoc, to automated-but-advisory, to automated-and-enforced-with-tracked-metrics — classify this SAST activity's current maturity level and state in one sentence what's missing to reach the next one.
- Remove
continue-on-error: truefrom the job, and configure branch protection sosastis a required status check onmain. - Write a one-page maturity scorecard entry: activity, current level, evidence, target level, owner, target date.
Done when: a PR that deliberately reintroduces a Semgrep finding can no longer be merged (the required check is red and the merge button is disabled), and the scorecard entry exists as a committed file.
Show the worked solution
# remove continue-on-error from the job definition, then make it required:
gh api repos/OWNER/REPO/branches/main/protection \
-X PUT \
-H "Accept: application/vnd.github+json" \
-f required_status_checks[strict]=true \
-f 'required_status_checks[contexts][]=sast' \
-f enforce_admins=true \
-F required_pull_request_reviews='{"required_approving_review_count":1}' \
-F restrictions=null# Maturity scorecard — Static Application Security Testing | Field | Value | |---|---| | Activity | SAST (Semgrep) on every push | | Current level | Automated, advisory only — runs on every commit but cannot block a merge | | Evidence | Workflow run history shows repeated red `sast` jobs on `main` with no merge impact | | Target level | Automated + enforced — required status check, tracked over time | | Change made | Removed `continue-on-error: true`; added `sast` to required status checks | | Owner | Platform/AppSec | | Target date | This sprint |
Why: DSOMM's whole premise is that maturity isn't which tool you own, it's whether the practice is embedded and enforced. An advisory scan is easy to file away and ignore — a required, enforced check changes developer behavior because it's no longer optional. Treat the exact level names and numbering as a moving target: DSOMM has been revised more than once, so verify the current dimension and level definitions on dsomm.owasp.org rather than memorizing a specific numbering from any single source, this page included.
Background reading: Maturity Models: DSOMM, SAMM & BSIMM and the blueprint chapter. If the branch-protection step is what slowed you down, the policy-as-code drill is more reps on exactly that muscle.
Code and dependencies — what actually ships in the artifact
☺ Like you're 10: Two questions about anything you're about to ship: did you write something dangerous yourself, and did you drag in something dangerous that somebody else wrote?
C3 · Find the SQLi, and the key that's still in history
A small Flask service has a search endpoint built with string concatenation, and — three commits back — someone briefly committed a real-looking AWS access key before "fixing" it in the very next commit by deleting the line.
Given — app.py:
@app.route("/user")
def get_user():
uid = request.args.get("id")
query = "SELECT * FROM users WHERE id = " + uid
return db.execute(query).fetchone()Your task:
- Run Semgrep against the app, identify the SQL injection, and fix it with a parameterized query.
- Run gitleaks against the full git history, not just the working tree, and find the credential that was added and later deleted.
- Treat the finding as a confirmed compromise (rotate it — deleting the line does not undo the exposure), then add a pre-commit hook so a new attempt to commit a fake key is blocked before it ever reaches a remote.
Done when: semgrep reports zero SQL-injection findings on the fixed file; gitleaks detect reports the historical finding with its commit SHA; and git commit with a newly staged fake AKIA… string is rejected by the hook before it's committed.
Show the worked solution
@app.route("/user")
def get_user():
uid = request.args.get("id")
query = "SELECT * FROM users WHERE id = ?"
return db.execute(query, (uid,)).fetchone()# 1. SAST
semgrep --config p/python --config p/sql-injection app.py
# app.py:4 python.flask.security.injection.tainted-sql-string ...
# 2. secrets, full history (gitleaks scans the whole reachable history by default)
gitleaks detect --source . -v
# Finding: AKIA************FAKE
# Secret: aws-access-token
# Commit: a1b2c3d (3 commits back)
# File: app.py
# 3. pre-commit gate
cat >> .pre-commit-config.yaml <<'EOF'
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks
EOF
pre-commit install
echo 'aws_key = "AKIAIOSFAKEEXAMPLE12"' >> app.py
git add app.py && git commit -m "test" # blocked by the hook, exit non-zeroWhy: the trap in step 2 is scope — running a secrets scanner only against the current working tree misses anything that was ever committed and later deleted, and a key that was pushed to a shared remote even briefly must be treated as compromised regardless of whether it's still present in HEAD. Rewriting history (BFG, git filter-repo) and force-pushing cleans the repo going forward, but it does not un-expose a key that was already fetched by anyone, including automated scrapers — rotation in IAM is the only real fix. Step 1's fix matters for a specific reason too: parameterized queries work because the driver sends the value and the SQL text as two separate things to the database, so user input can never be interpreted as SQL syntax no matter what characters it contains.
Tool references: Semgrep, gitleaks, TruffleHog. More reps on the secrets half: the leaked-credential-triage drill.
Semgrep rule IDs, gitleaks' default detection rules, and the exact GitHub Actions step syntax for each tool below change between releases more often than the underlying concepts do. Treat every command in this bank as illustrative of the shape, and confirm current flags and rule packs against each tool's own docs — linked from every tool page in the tool landscape — before you rely on exact syntax on exam day.
C4 · Trace a transitive CVE and decide if it's reachable
A Java service's pom.xml pulls in log4j-core:2.14.1 transitively through an internal logging wrapper artifact — nobody on the team declared it directly, and nobody's looked at the dependency tree since the project started.
Given — relevant fragment of pom.xml:
<dependency> <groupId>com.acme.internal</groupId> <artifactId>logging-wrapper</artifactId> <version>1.3.0</version> <!-- transitively brings in log4j-core:2.14.1 --> </dependency>
Your task:
- Run an SCA scan and identify the CVE affecting the transitive dependency, its CVSS score, its EPSS score, and whether it's listed in CISA's Known Exploited Vulnerabilities (KEV) catalog.
- Decide whether the finding needs a same-day fix or can wait for the normal release cadence, and justify the decision in one sentence.
- Remediate by pinning
log4j-coreto a fixed version via a dependency-management override — without bumping the wholelogging-wrapperartifact — regenerate the SBOM, and re-scan to confirm zero criticals.
Done when: mvn dependency:tree shows log4j-core resolving to the pinned fixed version, and a re-run SCA scan shows no CRITICAL findings for this dependency.
Show the worked solution
trivy fs --scanners vuln --severity CRITICAL,HIGH . # log4j-core 2.14.1 CVE-2021-44228 (Log4Shell) CRITICAL CVSS 10.0 # fixed in: 2.17.1
<!-- force the transitive version without touching logging-wrapper -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.1</version>
</dependency>
</dependencies>
</dependencyManagement>mvn dependency:tree | grep log4j-core # confirm it now resolves to 2.17.1 syft . -o cyclonedx-json=sbom.json # regenerate the SBOM against the fixed tree grype sbom:sbom.json # re-scan; should show zero criticals for log4j-core
Why: CVE-2021-44228 is real-world evidence for why "is it reachable" and "is it in the KEV catalog" are both part of triage, not just CVSS. A pure-reachability argument ("we don't call the vulnerable class directly") is exactly the reasoning that failed teams during Log4Shell, because any code path that logs attacker-controlled input — a header, a username, a query string — reaches the vulnerable lookup, and new code paths get added constantly. Once a CVE is KEV-listed, treat "actively exploited in the wild" as overriding a reachability debate: fix now, don't schedule a debate about it. The Maven override matters mechanically too — bumping logging-wrapper to a version that hasn't been vetted risks breaking things the team doesn't control, while dependencyManagement pins the transitive version precisely.
Tool references: Trivy, Syft & Grype, Snyk, OWASP Dependency-Check. Background: Software Bills of Materials, Dependency & License Risk Management. More reps: the vulnerable-dependency fire drill.
Runtime and infrastructure — what the artifact touches once it's live
☺ Like you're 10: Scanning the code catches what you wrote wrong. These two challenges catch what happens once it's actually running somewhere, talking to the internet and sitting on real infrastructure.
C5 · Run an authenticated scan and separate signal from noise
A target application (OWASP Juice Shop or an equivalent deliberately-vulnerable app works well for practicing this one) sits behind a login form. An unauthenticated ZAP baseline scan only ever sees the login page itself — everything interesting is behind auth, and nobody's configured the scanner to get past it.
Your task:
- Configure an authenticated ZAP scan using the ZAP Automation Framework, so the spider and active scan can reach pages behind the login form.
- Run the full scan and triage the report: pick one genuine true-positive finding with real impact, and one low-value or informational finding, and explain in one sentence each why they land in different buckets.
- Fix the true positive and re-run the scan to confirm the alert no longer fires.
Done when: the automation run's spider report shows pages reachable only after login (proving auth worked), and the confirmed true-positive alert is absent from the re-scan report.
Show the worked solution
# zap-automation.yaml
env:
contexts:
- name: target
urls: ["http://localhost:3000"]
authentication:
method: form
parameters:
loginPageUrl: http://localhost:3000/#/login
loginRequestUrl: http://localhost:3000/rest/user/login
loginRequestBody: 'email={%username%}&password={%password%}'
users:
- name: tester
credentials: { username: tester@example.com, password: "Testpass1!" }
jobs:
- type: spider
parameters: { context: target, user: tester, url: http://localhost:3000 }
- type: activeScan
parameters: { context: target, user: tester }
- type: report
parameters: { template: traditional-html, reportFile: zap-report.html }docker run -v $(pwd):/zap/wrk/:rw -t zaproxy/zap-stable \ zap.sh -cmd -autorun /zap/wrk/zap-automation.yaml
Triage: true positive — a reflected XSS on the search endpoint, confirmed by manually replaying the flagged payload and watching it execute in a browser; the app returns unescaped user input straight into the page. Low-value — an informational "Timestamp Disclosure" alert on a Last-Modified header for a static asset, which discloses nothing an attacker couldn't already infer and carries no exploit path.
<!-- fix: encode output at render time instead of interpolating raw input -->
<p>No results for: {{ query | e }}</p>
<!-- plus a Content-Security-Policy header as defense in depth -->Why: the exam-relevant trap is that an unauthenticated DAST scan against an app with a login wall reports a false sense of security — the scanner genuinely finds nothing wrong because it never got anywhere real. The ZAP Automation Framework (the current recommended approach, replacing the older baked-in zap-baseline.py / zap-full-scan.py shell scripts for anything needing auth) expresses the context, the credentials, and the job sequence declaratively so the run is reproducible in CI. Triage is the actual skill being tested here, not running the scanner — a report full of unread informational alerts trains people to ignore DAST output entirely, which is worse than not scanning at all.
Tool references: OWASP ZAP, Burp Suite. Background: the blueprint chapter, API Security in Depth.
C6 · Fix the Terraform plan before anyone applies it
A junior engineer's first Terraform PR provisions an S3 bucket, a security group, and an EBS volume — and would, if merged, leave all three publicly exposed or unencrypted. Nobody's run a scanner against it yet.
Given — fragment of main.tf:
resource "aws_s3_bucket" "logs" {
bucket = "acme-app-logs"
}
resource "aws_security_group" "app" {
name = "app-sg"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_ebs_volume" "data" {
availability_zone = "eu-west-1a"
size = 40
}Your task:
- Run Checkov or tfsec against the plan and list the specific failing check IDs for all three resources.
- Fix each: block public access and enable default encryption on the bucket, restrict SSH ingress to a named CIDR instead of the whole internet, and enable EBS encryption.
- Add an OPA/Conftest policy as a CI gate that fails
terraform planin future if any new S3 bucket lacks a public-access block, so this class of finding can't regress silently.
Done when: a re-run of Checkov against the fixed file shows zero failures for these three resources, and a Conftest run against a deliberately-reintroduced public bucket in the plan JSON fails with your policy's message.
Show the worked solution
checkov -d . --compact # CKV_AWS_53/54/55/56 aws_s3_bucket.logs — public access block not configured # CKV_AWS_19 aws_s3_bucket.logs — bucket not encrypted # CKV_AWS_24 aws_security_group.app — 22 open to 0.0.0.0/0 # CKV_AWS_3 aws_ebs_volume.data — not encrypted
resource "aws_s3_bucket" "logs" {
bucket = "acme-app-logs"
}
resource "aws_s3_bucket_public_access_block" "logs" {
bucket = aws_s3_bucket.logs.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_server_side_encryption_configuration" "logs" {
bucket = aws_s3_bucket.logs.id
rule {
apply_server_side_encryption_by_default { sse_algorithm = "AES256" }
}
}
resource "aws_security_group" "app" {
name = "app-sg"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["203.0.113.0/24"] # bastion / office CIDR, not 0.0.0.0/0
}
}
resource "aws_ebs_volume" "data" {
availability_zone = "eu-west-1a"
size = 40
encrypted = true
}# policy/s3_public_block.rego
package terraform.s3
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket"
not has_public_access_block(resource.address)
msg := sprintf("S3 bucket %v has no aws_s3_bucket_public_access_block", [resource.address])
}
has_public_access_block(bucket_addr) {
pab := input.resource_changes[_]
pab.type == "aws_s3_bucket_public_access_block"
contains(pab.change.after.bucket, bucket_addr)
}terraform show -json tfplan > plan.json conftest test --policy policy/ plan.json
Why: all three findings share a root cause — AWS resources default to the less secure option unless a companion resource or attribute explicitly locks it down, so "I created the bucket" is never the same claim as "I created the bucket and blocked public access on it." Checkov and tfsec both catch this at plan time, which is the entire point of shifting IaC scanning left of apply — a public bucket caught in a PR review costs a diff comment; the same bucket caught after apply costs an incident. The Conftest policy is what keeps the fix from being a one-time cleanup instead of a permanent guarantee — without it, the next engineer who copies this pattern reintroduces the exact same gap.
Tool references: Checkov, tfsec, OPA & Conftest. More reps: the broken-Terraform-plan drill.
Proof and prioritization — closing the loop
☺ Like you're 10: Finding problems is only half the job. The other half is proving you fixed them to someone who wasn't watching, and deciding which ones actually deserve today's attention.
C7 · Turn a CIS checklist into automated, re-runnable evidence
An auditor's spreadsheet asks three questions about a fleet of Linux hosts: is root SSH login disabled, is auditd installed and running, and does the password policy meet CIS baseline requirements. Right now the answer comes from someone SSHing in once a quarter and eyeballing the config.
Your task:
- Write an InSpec profile with controls for all three checks.
- Run it against a target host and produce a machine-readable results artifact instead of a spreadsheet checkbox.
- For each failing control, apply the remediation, then re-run to show green — producing evidence an auditor can accept without ever needing shell access to the host themselves.
Done when: inspec exec against the target produces a JSON results file with all three controls passing, and the JSON is retained as the audit evidence artifact.
Show the worked solution
# controls/ssh_and_auditd.rb
control 'cis-5.2.10' do
impact 1.0
title 'Ensure SSH root login is disabled'
describe sshd_config do
its('PermitRootLogin') { should cmp 'no' }
end
end
control 'cis-4.1.1.1' do
impact 1.0
title 'Ensure auditd is installed and running'
describe package('auditd') do
it { should be_installed }
end
describe service('auditd') do
it { should be_enabled }
it { should be_running }
end
end
control 'cis-5.4.1' do
impact 0.7
title 'Ensure password minimum length meets CIS baseline'
describe file('/etc/security/pwquality.conf') do
its('content') { should match(/^\s*minlen\s*=\s*(1[4-9]|[2-9]\d)/) }
end
end# remediate the failures first, then run sudo sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config sudo systemctl restart sshd sudo apt-get install -y auditd && sudo systemctl enable --now auditd echo "minlen = 14" | sudo tee -a /etc/security/pwquality.conf inspec exec ./controls --target ssh://user@host \ --reporter cli json:results.json
Why: compliance-as-code turns a point-in-time manual attestation into a versioned, re-runnable check that produces the same evidence whether it runs once by hand or nightly across an entire fleet in a pipeline. That's the actual difference between "we're compliant" as a claim someone made in a meeting and "we're compliant" as a timestamped JSON artifact anyone can inspect. OpenSCAP with a CIS-aligned SCAP content stream (oscap xccdf eval --profile … --results-arf results.xml) is the equivalent approach when the fleet is standardized enough to use pre-built SCAP content instead of hand-written controls — know both, since which one a task expects depends entirely on what's already installed on the target.
Tool references: InSpec, OpenSCAP, Wazuh. Background: the blueprint chapter.
C8 · 40 findings, one afternoon — which five do you fix
DefectDojo just aggregated the week's scan output — SAST, SCA, DAST, and a container scan — into roughly 40 open findings. You have one afternoon before the sprint planning meeting where you have to defend a short list. A sample of the pile:
| Source | Finding | CVSS | EPSS | KEV? | Exposure | Fix available? |
|---|---|---|---|---|---|---|
| SCA | Struts2 RCE in a vendored dep | 9.8 | 0.94 | Yes | Internet-facing | Yes — bump minor version |
| DAST | Reflected XSS on internal admin tool | 6.1 | 0.02 | No | Internal-only, SSO-gated | Yes — output encoding |
| SAST | Hardcoded API key in config.py | — | — | No | Internal repo, private | Yes — rotate + move to Vault |
| Secrets scan | Same key, same file, different tool | — | — | No | Internal repo, private | Duplicate of above |
| Container | Base image CVE, no upstream patch yet | 7.5 | 0.03 | No | Internet-facing | No — vendor hasn't shipped a fix |
| SCA | Prototype-pollution CVE in a build-time-only dev dependency | 9.1 | 0.01 | No | Never ships to production | Yes — bump version |
| DAST | Missing Strict-Transport-Security header | 3.1 | 0.01 | No | Internet-facing | Yes — one header |
| SAST | Path traversal in an internal batch job | 7.2 | 0.05 | No | Internal-only, no untrusted input path found | Yes — sanitize input |
Your task:
- Deduplicate: identify the pair of findings that are the same underlying root cause reported by two different tools, and count it once.
- Rank the deduplicated list and pick your top 5, using severity, EPSS, KEV status, and exposure together — not raw CVSS alone — with a one-line justification per item.
- Assign a remediation SLA to each of your top 5, and for the one finding with no vendor fix available, propose a compensating control instead of an indefinite "accepted risk."
Done when: your top-5 list and SLAs are written down, and every item's justification references at least one factor beyond its raw CVSS score.
Show the worked solution
| Rank | Finding | Justification | SLA |
|---|---|---|---|
| 1 | Struts2 RCE | KEV-listed, high EPSS, internet-facing, fix available — every risk factor stacks the same direction | 24–48h |
| 2 | Hardcoded API key | Confirmed by two independent tools; a live credential is a standing compromise regardless of CVSS | Immediate rotation, fix this sprint |
| 3 | Base image CVE, no fix | Internet-facing and CVSS 7.5, but no patch exists — needs a compensating control now, not a wait | Compensating control in 48h; monitor for vendor patch |
| 4 | Path traversal, internal batch job | High CVSS but no confirmed untrusted-input path — fix on normal cadence, don't treat as urgent without reachability | Next sprint |
| 5 | Reflected XSS, admin tool | Low CVSS and low EPSS, but SSO-gated internal exposure lowers real risk further — still worth a cheap fix | Next sprint |
Dropped, correctly: the missing HSTS header (real but low severity, cheap to batch with other work, not urgent on its own); the prototype-pollution CVE in a dev-only dependency (never reaches production, so its CVSS is nearly irrelevant to actual risk).
Compensating control for the no-fix base image CVE: a WAF/virtual-patch rule blocking the specific exploit pattern until the vendor ships a fix, plus a tracked follow-up item — not silence, and not an indefinite "risk accepted" with no re-check date.
Why: the whole exercise turns on refusing to let CVSS be the only input. The Struts2 finding and the path-traversal finding have comparable severity scores, but KEV membership plus a high EPSS score means one is being actively exploited right now across the internet while the other is a theoretical risk with no confirmed reachable path — those aren't the same priority no matter what the severity label says. The duplicate-finding trap matters for a mundane reason: an unscrubbed count of "40 findings" makes a backlog look twice as bad as it is and burns credibility with the people you're asking to prioritize your fixes over theirs.
Tool references: DefectDojo. Background: the blueprint chapter.
Score a full sitting
☺ Like you're 10: Once you've done all eight puzzles, add up how you did — a bad score on one puzzle tells you exactly what to go re-study, which is more useful than one big number.
Each challenge's rubric scores out of 100, the same scale the real CDP report is graded on, with an 80/100 pass mark per the vendor's own published figures — see the CDP exam guide's standing caveat that you should re-verify that number on Practical DevSecOps' own page before you rely on it. Score each challenge honestly, then read the pattern rather than the total:
| Band | What it looks like | What to do about it |
|---|---|---|
| 90–100 | Every task done inside the time-box, "done when" check passed on the first try, docs used only to confirm a detail | Nothing — move to the next challenge |
| 80–89 | Done inside the time-box, check passed, but you needed the worked solution for one field or one flag | Note the specific gap; it'll usually recur, so flashcard it |
| 60–79 | Ran out of time, or the fix was half-right — the scanner still shows one finding, or the verification step wasn't actually run | Re-drill this exact challenge cold in 48 hours |
| Under 60 | Didn't recognize the tool, the vulnerability class, or the resource shape at all | Stop drilling — go read the blueprint chapter properly first |
A single low score on one challenge is a study plan, not a failure — it tells you precisely which of the eight blueprint domains needs another pass before you book the real exam, which is a far more useful signal than an aggregate percentage across all eight would be.
Dot: I read all eight worked solutions on the train this morning. Feeling pretty good about this bank!
Timmy: How many of the eight "done when" checks did you actually run?
Dot: …none? But the YAML all looked correct to me.
Ellie: That's recognition, not recall. C3's key is still sitting in the git history whether you read about gitleaks detect or not — the only thing that proves you'd have found it is actually running the scan.
Foxy: Speaking of C8 — the Struts2 finding and the path-traversal one had almost the same CVSS. What actually separated them?
Nutty: KEV listing and EPSS, filed right there in the table. One's being exploited in the wild today; the other's a theoretical path nobody's confirmed is reachable. Same severity number, very different afternoon.
Timmy: Cold, timed, verified, scored honestly — every time, all eight. Dot, go do C1 again. For real this time.
The rest of exam prep: the CDP study plan maps the nine-chapter blueprint onto a week-by-week schedule; the CDP exam guide covers format, registration, and exam-day logistics; Know It Cold and the command & tool reference are the from-memory drills worth doing alongside this bank; the mock exam sets string challenges like these into a full timed sitting; and the troubleshooting triage playbook is what to reach for when a challenge isn't behaving the way you expected. For hands-on reps that chain several of these domains into one continuous pipeline rather than eight separate scenarios, work the capstone lab track.
1. What does every challenge in this bank have instead of multiple-choice options, and why does that match the real exam? 2. In C1, why does a job with no impact on branch protection count as a "signal" rather than a "gate"? 3. In C2, what's the actual difference between an advisory SAST check and an enforced one, and why does DSOMM care about that distinction more than which tool is installed? 4. In C4, why does a CVE's KEV-catalog membership override a pure reachability argument? 5. In C8, name the two findings that got correctly dropped from the top 5, and why. 6. What score should you treat as this bank's pass mark, and what should you do with a single low score rather than an aggregate one?
Check your answers
- A broken environment, a task list, and a "done when" verification check — no bubbles to guess between. That mirrors the CDP directly: it's 100% practical and task-based, with zero multiple-choice questions anywhere in it.
- Because its exit code can't block a merge —
continue-on-error: true(C1) or the absence of a required-status-check entry (C2) means the job can run, fail, and be ignored indefinitely. A gate is defined by its ability to stop a bad change, not by whether it runs at all. - Advisory means the check runs and reports but nothing stops a violating change from merging; enforced means it's a required status check and a violation is physically blocked. DSOMM cares about that distinction because maturity is about whether a practice is actually embedded in how work gets merged, not which scanner brand happens to be installed — an advisory Semgrep job and an enforced one represent very different maturity levels even though it's the exact same tool.
- KEV listing means CISA has confirmed active, real-world exploitation of that specific CVE — that's stronger evidence than a reachability argument, which is easy to get wrong (a new code path can make a previously "unreachable" sink reachable overnight) and doesn't account for attackers finding paths a defender didn't anticipate.
- The missing HSTS header (real but low-severity and cheap to batch in later, not urgent alone) and the prototype-pollution CVE in a dev-only dependency (never reaches production, so its CVSS score is nearly irrelevant to actual risk).
- 80/100, matching the CDP's own published pass mark. A single low score identifies exactly which blueprint domain needs another pass — treat it as a targeted study list, not as a verdict on the whole attempt.