Exam Prep · CDP · professional · practice challenge bank

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.

☺ Explain it like I'm 10

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.

🐢🐿️Your hosts for this topic: Timmy the Turtle & Nutty the Squirrel — Timmy refuses to accept "it looks fixed," and won't sign off on anything without a verification command; Nutty is the one who insists the rubric and the evidence get filed away properly, so grading yourself doesn't turn into wishful thinking.

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:

◆ Key idea

"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 drill loop Attempt cold timed · no notes Verify run "done when" Self-score against the rubric ≥ 80 / 100? honestly scored Yes → next challenge No → back onto tomorrow's cold re-drill list

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.

#ChallengeBlueprint domainPractice time
C1Wire the shift-left gates into a pipeline that has noneFoundations & the CDP Toolchain~45 min
C2Score a pipeline against DSOMM, then raise its levelSecure SDLC Gates & DSOMM~45 min
C3Find the SQLi, and the key that's still in historyStatic Analysis & Secrets Detection~60 min
C4Trace a transitive CVE and decide if it's reachableSoftware Composition Analysis~60 min
C5Run an authenticated scan and separate signal from noiseDynamic Analysis in Practice~70 min
C6Fix the Terraform plan before anyone applies itInfrastructure as Code Hardening~60 min
C7Turn a CIS checklist into automated, re-runnable evidenceCompliance as Code at Scale~60 min
C840 findings, one afternoon — which five do you fixVulnerability 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 test

Your task:

  1. 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.
  2. Add a sast job that runs Semgrep against the repo and fails on any ERROR-severity finding.
  3. 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: 1

Why: 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:

  1. 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.
  2. Remove continue-on-error: true from the job, and configure branch protection so sast is a required status check on main.
  3. 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.

Givenapp.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:

  1. Run Semgrep against the app, identify the SQL injection, and fix it with a parameterized query.
  2. Run gitleaks against the full git history, not just the working tree, and find the credential that was added and later deleted.
  3. 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-zero

Why: 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.

⚠ Commands and check IDs drift

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:

  1. 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.
  2. Decide whether the finding needs a same-day fix or can wait for the normal release cadence, and justify the decision in one sentence.
  3. Remediate by pinning log4j-core to a fixed version via a dependency-management override — without bumping the whole logging-wrapper artifact — 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:

  1. Configure an authenticated ZAP scan using the ZAP Automation Framework, so the spider and active scan can reach pages behind the login form.
  2. 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.
  3. 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:

  1. Run Checkov or tfsec against the plan and list the specific failing check IDs for all three resources.
  2. 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.
  3. Add an OPA/Conftest policy as a CI gate that fails terraform plan in 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:

  1. Write an InSpec profile with controls for all three checks.
  2. Run it against a target host and produce a machine-readable results artifact instead of a spreadsheet checkbox.
  3. 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:

SourceFindingCVSSEPSSKEV?ExposureFix available?
SCAStruts2 RCE in a vendored dep9.80.94YesInternet-facingYes — bump minor version
DASTReflected XSS on internal admin tool6.10.02NoInternal-only, SSO-gatedYes — output encoding
SASTHardcoded API key in config.pyNoInternal repo, privateYes — rotate + move to Vault
Secrets scanSame key, same file, different toolNoInternal repo, privateDuplicate of above
ContainerBase image CVE, no upstream patch yet7.50.03NoInternet-facingNo — vendor hasn't shipped a fix
SCAPrototype-pollution CVE in a build-time-only dev dependency9.10.01NoNever ships to productionYes — bump version
DASTMissing Strict-Transport-Security header3.10.01NoInternet-facingYes — one header
SASTPath traversal in an internal batch job7.20.05NoInternal-only, no untrusted input path foundYes — sanitize input

Your task:

  1. Deduplicate: identify the pair of findings that are the same underlying root cause reported by two different tools, and count it once.
  2. 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.
  3. 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
RankFindingJustificationSLA
1Struts2 RCEKEV-listed, high EPSS, internet-facing, fix available — every risk factor stacks the same direction24–48h
2Hardcoded API keyConfirmed by two independent tools; a live credential is a standing compromise regardless of CVSSImmediate rotation, fix this sprint
3Base image CVE, no fixInternet-facing and CVSS 7.5, but no patch exists — needs a compensating control now, not a waitCompensating control in 48h; monitor for vendor patch
4Path traversal, internal batch jobHigh CVSS but no confirmed untrusted-input path — fix on normal cadence, don't treat as urgent without reachabilityNext sprint
5Reflected XSS, admin toolLow CVSS and low EPSS, but SSO-gated internal exposure lowers real risk further — still worth a cheap fixNext 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:

BandWhat it looks likeWhat to do about it
90–100Every task done inside the time-box, "done when" check passed on the first try, docs used only to confirm a detailNothing — move to the next challenge
80–89Done inside the time-box, check passed, but you needed the worked solution for one field or one flagNote the specific gap; it'll usually recur, so flashcard it
60–79Ran out of time, or the fix was half-right — the scanner still shows one finding, or the verification step wasn't actually runRe-drill this exact challenge cold in 48 hours
Under 60Didn't recognize the tool, the vulnerability class, or the resource shape at allStop 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.

🎬 At the Shift-Left Squad
🦆

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.

✓ Checkpoint

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
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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).
  6. 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.