Exam Prep · CDP · professional · mock exam · set 4

Mock Exam · Set 4 — the final dress rehearsal

This is the last paper before you book the real thing. Five challenges, one unbroken 6-hour sitting, a hundred points, an 80-point bar — and unlike the practice bank, nothing below has been drilled anywhere else on this site. If you've worked the study plan, the practice challenge bank, and sat Sets 13, this page is the only remaining question worth asking: not do you know the material, but can you produce it, cold, against a live environment, on a clock that doesn't pause and without a chatbot to lean on. Sit it exactly like exam day — same clock, same allowlist, same evidence discipline — and the score you get back is the most honest readiness signal this course can give you.

☺ Explain it like I'm 10

You've practiced the piano piece a hundred times, and you've even played it in front of a small audience twice before. Tonight is the dress rehearsal for the actual recital — full costume, full lighting, no stopping if you fumble a note, and someone in the back row timing you. If tonight goes well, you book the hall. If it doesn't, better to find that out tonight, in an empty room, than tomorrow in front of everyone who bought a ticket.

🐢🦊Your hosts for this topic: Timmy the Turtle & Foxy — Timmy runs the clock and refuses to sign off on anything without a verification command, and Foxy is the one who won't let a passing-looking score go unquestioned until it's actually been interrogated.

What makes Set 4 different from Sets 1–3

☺ Like you're 10: The earlier papers taught you the shape of the test. This one only tells you one thing: are you ready, yes or no.

Every mock exam on this site measures something slightly different, and using them out of order wastes the one that matters most. The earlier sets built the skill of sitting a timed, multi-challenge paper without flinching — pacing, triage, the discipline of moving on from a challenge that's fighting you. Set 4 assumes you already have that skill and spends its whole budget on a harder question: with the format no longer a surprise, does your actual technical output — under the clock, cold, with the real allowlist — clear 80 out of 100? A high score on an earlier set that you'd partly seen before proves less than it feels like it does. A high score here, on five challenges you are reading for the first time in this sitting, is close to the strongest evidence this course can hand you that you're ready to pay for the real attempt.

◆ Key idea

Treat this page as sealed the same way you'd treat the real exam: read the five briefs for the first time when the timer starts, not before. If you skim ahead "just to see how hard it is," you've spent your last cold measurement for nothing, and the readiness checklist at the bottom of this page will be lying to you when you read it.

Pre-flight — build the environment the night before

☺ Like you're 10: Set up every tool the day before, so the only thing you're doing once the clock starts is the actual work.

None of the five challenges below need a specific cloud account — a laptop with Docker, a throwaway kind cluster for Challenge 4, and the tools each challenge names is enough. Install everything the night before and confirm it runs cleanly; a broken tool install eats real minutes off a clock that doesn't pause for it.

# --- the night before ---------------------------------------------------
pip install semgrep gitleaks-cli 2>/dev/null; brew install gitleaks trufflehog 2>/dev/null
brew install trivy syft grype checkov kube-score conftest opa 2>/dev/null
docker pull zaproxy/zap-stable
docker pull owasp/juice-shop     # or any deliberately-vulnerable target you already run
kind create cluster --name cdp-mock4
kubectl get nodes                # Ready before you go to bed
kubectl create ns kyverno && kubectl apply -f https://github.com/kyverno/kyverno/releases/latest/download/install.yaml

# --- ninety seconds before the clock starts ------------------------------
mkdir -p ~/cdp-mock4/{c1,c2,c3,c4,c5,evidence} && cd ~/cdp-mock4
git init && git config user.email "you@example.com"   # C1 needs real git history to scan
kubectl config use-context kind-cdp-mock4
date   # write down the actual start time — you will need it for the report window
⚠ Run this exactly like exam day, including the parts that feel unnecessary

The CDP's own report window doesn't start with the challenges — it starts the moment the 6-hour challenge window ends, and it's a separate 24-hour clock. So treat this sitting the same way: the instant your 6 hours are up, stop touching the environment, and see whether you could actually assemble a report from what you captured. If you can't, that's data — see the CDP exam guide's standing point that a fix nobody can see proof of is a fix a report can't defend. Screenshot and note the done-when output for each task the moment you close it, not at hour six.

Exam conditions for this sitting

☺ Like you're 10: A dress rehearsal only tells you something true if you run it under the real rules — same clock, no shortcuts, no peeking at the answers.

One timer, started once, six hours, no pausing. Not for a broken container, not for a coffee, not for a laptop update. If your own tooling eats twenty minutes, that's a real finding about your setup discipline, not an excuse to reset the clock — the real exam won't reset it either.

Nothing open except what the real exam permits. Practical DevSecOps doesn't publish a fixed allowlist the way a vendor like Linux Foundation does for its own exams, but the CDP's own stated policy is explicit on one point: no AI assistants or chatbots during the challenge window — see the exam guide. Hold yourself to that here too. Official docs for the tools named in each challenge are fair game; this course's own pages, your notes from Sets 1–3, and any assistant are not.

No peeking at the worked solutions until you're done or truly stuck past the flag-and-move line. Each challenge's answer key is folded behind a summary specifically so opening it mid-attempt is a deliberate act, not an accident.

Read all five briefs first, five minutes, before touching a terminal. Mark each one cheap, medium, or expensive in your head. This paper does not arrive in difficulty order — the challenge you find easiest might be the one you're reading last.

Your 6-hour time budget

☺ Like you're 10: Give each puzzle its own slice of the six hours, and when a slice runs out, write down where you got stuck and move to the next one.

Five challenges in 360 minutes averages 67 minutes each once the 15 minutes of reading and the 10-minute evidence sweep at the end are carved out — but the challenges aren't equal, so the budget isn't either. Challenges 2 and 3 (dependency triage across a real transitive tree, and configuring authenticated DAST from scratch) run heavier than 1, 4, and 5.

BlockWhatBudgetRunning total
ReadAll five briefs, no terminal yet15 min15
Challenge 1Static analysis & secrets detection65 min80
Challenge 2Software composition analysis70 min150
Challenge 3Dynamic analysis in practice70 min220
Challenge 4Infrastructure-as-code hardening65 min285
Challenge 5Vulnerability triage & compliance evidence65 min350
Verify & evidence sweepRe-run every done-when, confirm your screenshots exist10 min360

The flag-and-move rule. If a challenge passes roughly 15 minutes past its budget with no passing done-when check, stop. Write one line about where you stalled, leave your partial work in place, and move to the next challenge. Twenty points sitting untouched at Challenge 5 because you spent ninety extra minutes chasing a clean result on Challenge 2 is the single most common way a candidate who knows the material still fails the clock.

360 minutes · 5 challenges · 100 points read 15m C1 SAST + secrets · 65m C2 SCA · 70m C3 DAST · 70m C4 IaC · 65m C5 Triage + evidence · 65m verify 10m This is the challenge window only — the real CDP adds a separate 24-hour report window after it closes Passing score: 80 / 100 · Flag-and-move at roughly 15 minutes past a block's budget

The paper — 5 challenges, cold

☺ Like you're 10: Five brand-new puzzles, one per topic this course cares about most. Start the clock, read them all once, then go.

Every scenario below is new — none of it reuses the practice bank's C1–C8. Work each one in your own environment, verify with the stated done-when check before you consider it closed, and only open a worked solution after you've genuinely attempted the challenge or the flag-and-move threshold has passed.

Challenge 1 · The convert job and the token that got "removed" (20 pts)

A small Node/Express file-processing service, media-api, resizes uploaded images by shelling out to ImageMagick. Three commits back, someone briefly hardcoded a live-looking Twilio auth token into a .env.local file while testing an SMS-notification feature, then "cleaned it up" in the very next commit by deleting the line.

Givenroutes/convert.js:

const { exec } = require("child_process");

app.post("/convert", (req, res) => {
  const { filename, width } = req.body;
  exec(`convert uploads/${filename} -resize ${width} out/${filename}`, (err, stdout) => {
    if (err) return res.status(500).send(err.message);
    res.send({ ok: true });
  });
});

Your task:

  1. Run Semgrep against the service, identify the command-injection sink, and rewrite it so a filename like x.jpg; rm -rf / or a width like 100; curl evil.tld cannot execute arbitrary shell.
  2. Run gitleaks or TruffleHog against the full git history and locate the deleted Twilio token, with its commit SHA.
  3. Treat the token as compromised — deleting the line does not undo the exposure — and add a pre-commit hook plus a required, merge-blocking CI job so a new secret can't reach a shared branch again.

Done when: semgrep reports zero command-injection findings on the fixed route; gitleaks detect --source . -v reports the historical Twilio finding with its commit SHA; and a test commit adding a fake SK…-style token is rejected by the pre-commit hook before it's committed.

Show the worked solution
const { execFile } = require("child_process");
const path = require("path");

app.post("/convert", (req, res) => {
  const { filename, width } = req.body;
  if (!/^[\w.-]+\.(jpe?g|png|gif)$/i.test(filename)) return res.status(400).send("bad filename");
  const w = Number(width);
  if (!Number.isInteger(w) || w < 1 || w > 4000) return res.status(400).send("bad width");

  const src = path.join("uploads", filename);
  const dst = path.join("out", filename);
  // execFile with an argv array — no shell is invoked, so there is no string to inject into
  execFile("convert", [src, "-resize", String(w), dst], (err) => {
    if (err) return res.status(500).send("conversion failed");
    res.send({ ok: true });
  });
});
# 2. secrets, full history
gitleaks detect --source . -v
#   Finding:  SK********************FAKE
#   Secret:   twilio-api-key
#   Commit:   4e91a2c  (3 commits back)
#   File:     .env.local

# 3. pre-commit + CI 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 'TWILIO_TOKEN=SKfakefakefakefakefakefakefakefake' >> .env.local
git add .env.local && git commit -m "test"     # rejected, non-zero exit

Why: the injection sink isn't the shell command itself, it's exec()'s decision to hand a single interpolated string to /bin/sh -c — anything the attacker controls that reaches that string is executable shell, including a filename. execFile with an argv array removes the shell entirely, so ;, |, and backticks are just literal characters passed as one argument; the input validation on top is defense in depth, not the actual fix. On the secret: a shallow clone or a scan of only the working tree would report zero findings here, which is exactly the trap — the token is gone from HEAD but was live and pushed for one full commit, which is enough time for it to be fetched by anything watching public or shared history, including automated scrapers. Rotation in Twilio's own console is the only real remediation; deleting the line is not.

Tool references: Semgrep, gitleaks, TruffleHog. Background: the blueprint chapter, Secure Coding Patterns.

Challenge 2 · A transitive CVE with a CISA KEV listing (20 pts)

A Java service, reporting-engine, depends on an internal wrapper artifact, report-formatting-lib:2.1.0, which transitively pulls in commons-text:1.9 — nobody declared it directly, and nobody's looked at the resolved tree since the wrapper was added eighteen months ago.

Given — the wrapper's templating utility, called from several report endpoints with user-editable template strings:

import org.apache.commons.text.StringSubstitutor;

public class TemplateRenderer {
    public String render(String template, Map<String, String> values) {
        return StringSubstitutor.createInterpolator()
            .setVariableResolver(new MapLookup(values))
            .replace(template);   // template can contain user-editable ${...} expressions
    }
}

Your task:

  1. Run an SCA scan, identify the CVE affecting the transitive commons-text dependency, and record its CVSS score, its EPSS score, and whether it appears in CISA's KEV catalog.
  2. Decide whether the finding is reachable, based on the snippet above, and justify the decision in one sentence.
  3. Remediate by pinning commons-text to a fixed version via a dependency-management override — without bumping the whole report-formatting-lib artifact, which the reporting team hasn't vetted this sprint — regenerate the SBOM, and re-scan to confirm zero criticals.

Done when: mvn dependency:tree shows commons-text resolving to a 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 .
#   commons-text 1.9   CVE-2022-42889 (Text4Shell)   CRITICAL   CVSS 9.8
#   fixed in: 1.10.0

Reachability: yes. StringSubstitutor.createInterpolator() registers the default interpolator lookups, which in the vulnerable versions include script:, dns:, and url: — and the code above feeds it a template string that's explicitly user-editable. That's the exact shape CVE-2022-42889 needs: attacker-controlled text reaching an interpolator with the dangerous lookups still enabled. A pure "we don't call eval anywhere" argument would miss this entirely, the same trap this course keeps coming back to with Log4Shell.

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-text</artifactId>
      <version>1.10.0</version>
    </dependency>
  </dependencies>
</dependencyManagement>
mvn dependency:tree | grep commons-text     # confirm resolves to 1.10.0
syft . -o cyclonedx-json=sbom.json          # regenerate SBOM against the fixed tree
grype sbom:sbom.json                         # re-scan; zero criticals for commons-text

Why: commons-text 1.10.0 disabled the dangerous lookups (script, dns, url) by default rather than requiring every caller to remember to opt out of them — the fix is a safer default, not a removed feature most code depended on. CVE-2022-42889 was added to CISA's KEV catalog, which is the strongest available evidence that "is this being exploited" isn't a hypothetical for this one; a KEV listing should override any temptation to schedule the fix for a normal release cadence. The dependencyManagement override matters mechanically for the same reason it did with Log4Shell: bumping the whole wrapper artifact risks pulling in unrelated, unvetted changes, while pinning the transitive dependency directly fixes exactly the one CVE in scope.

Tool references: Trivy, Syft & Grype, Snyk. Background: the blueprint chapter, Software Bills of Materials.

Challenge 3 · Authenticated DAST against a JSON API, not a login form (20 pts)

Unlike a browser app with a login page, orders-api is a pure JSON REST API secured with short-lived Bearer JWTs issued by a separate /auth/token endpoint. An out-of-the-box ZAP baseline scan reaches /health and nothing else — there's no HTML for the spider to follow links from, and every real endpoint returns 401 without a token.

Your task:

  1. Obtain a token via a scripted call to /auth/token, and configure the ZAP Automation Framework to attach it as an Authorization: Bearer … header on every request in the scan — form-based authentication doesn't apply here, since there's no HTML login form to submit.
  2. Seed the scan from the API's OpenAPI spec (openapi.yaml) rather than the spider alone, since a JSON API has no crawlable links for a spider to discover on its own.
  3. Run the scan and triage the report: identify one genuine true positive with real impact, and one low-value/informational finding, then fix the true positive and re-run to confirm it's gone.

Given — the relevant endpoint, which builds a dynamic sort clause from a JSON body field:

@app.route("/orders/search", methods=["POST"])
def search_orders():
    body = request.get_json()
    sort_field = body.get("sort", "created_at")
    query = f"SELECT * FROM orders ORDER BY {sort_field}"
    return jsonify(db.execute(query).fetchall())

Done when: the automation run's report shows the OpenAPI-seeded endpoints were reached with a 200/normal response (proving the bearer-token auth worked, not just a wall of 401s), and the confirmed true-positive alert is absent from the re-scan.

Show the worked solution
# zap-automation.yaml
env:
  contexts:
    - name: orders-api
      urls: ["http://localhost:8080"]
  parameters:
    failOnError: true
jobs:
  - type: script
    parameters:
      action: add
      type: httpsender
      engine: "Oracle Nashorn"
      name: bearer-auth
      file: /zap/wrk/bearer-auth.js       # fetches a fresh token and adds the header

  - type: openapi
    parameters:
      apiUrl: http://localhost:8080/openapi.yaml
      targetUrl: http://localhost:8080
      context: orders-api

  - type: activeScan
    parameters: { context: orders-api }

  - type: report
    parameters: { template: traditional-json, reportFile: zap-report.json }
// bearer-auth.js — ZAP HttpSender script, runs before every request in scope
function sendingRequest(msg, initiator, helper) {
    var token = TOKEN_CACHE.get("orders-api");
    if (!token) {
        // one real login call, cached for the rest of the scan
        token = doLogin();
        TOKEN_CACHE.put("orders-api", token);
    }
    msg.getRequestHeader().setHeader("Authorization", "Bearer " + token);
}
function responseReceived(msg, initiator, helper) {}

Triage: true positive — SQL injection via the sort field in the POST body, confirmed by sending {"sort": "created_at; DROP TABLE orders--"} against a disposable copy of the database and observing the query execute unmodified. Low-value — an informational "Timestamp Disclosure" alert on a response header for the /health endpoint, which discloses nothing an attacker couldn't already infer from the API's own uptime.

ALLOWED_SORT_FIELDS = {"created_at", "total", "status"}

@app.route("/orders/search", methods=["POST"])
def search_orders():
    body = request.get_json()
    sort_field = body.get("sort", "created_at")
    if sort_field not in ALLOWED_SORT_FIELDS:
        return jsonify({"error": "invalid sort field"}), 400
    query = f"SELECT * FROM orders ORDER BY {sort_field}"   # safe: sort_field is now from a closed allow-list
    return jsonify(db.execute(query).fetchall())

Why: the exam-relevant trap is assuming DAST auth means "fill in a login form." A pure JSON API has no form to submit — the ZAP Automation Framework's script-based authentication (or a Replacer rule, for a simpler static token) is what actually applies, and it's a different job type than the form authentication method used against browser apps. The second trap is scope: without seeding from the OpenAPI spec, the spider literally cannot discover POST-only JSON endpoints, since there's nothing in an HTML response for it to crawl — a scan that "found nothing" here would mean the auth or the seeding was broken, not that the API is clean. The SQL injection itself can't be parameterized the usual way, because ORDER BY column names can't be bound as query parameters in most drivers — a closed allow-list is the correct fix pattern specifically for this class of "dynamic identifier" injection, not string escaping.

Tool references: OWASP ZAP, Burp Suite. Background: the blueprint chapter, API Security in Depth.

Challenge 4 · A Kubernetes manifest, and a policy so it can't regress (20 pts)

A junior engineer's first Kubernetes PR ships the checkout Deployment as a privileged, host-networked container with no resource limits and a mutable tag. Nobody's run a scanner against it, and there's no admission policy in the cluster that would have stopped it anyway.

Given — fragment of checkout-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata: { name: checkout }
spec:
  template:
    spec:
      hostNetwork: true
      containers:
        - name: checkout
          image: acme/checkout:latest
          securityContext:
            privileged: true

Your task:

  1. Scan the manifest with Checkov's Kubernetes framework or kube-score, and list the specific failing checks (privileged container, host networking, mutable tag, missing resource limits, missing non-root/read-only-filesystem settings).
  2. Fix the manifest: remove privileged and hostNetwork, pin the image to a digest instead of :latest, add a hardened securityContext (non-root, read-only root filesystem, all capabilities dropped), and add resource requests/limits.
  3. Add a Kyverno ClusterPolicy in enforce mode that blocks privileged pods and mutable :latest tags cluster-wide, then prove it by deploying a deliberately bad pod and showing the admission webhook rejects it.

Done when: Checkov reports zero failures for the fixed manifest, and kubectl apply of a test pod with privileged: true is rejected with an admission-webhook error referencing your policy.

Show the worked solution
checkov -f checkout-deployment.yaml --framework kubernetes --compact
#   CKV_K8S_16   Container should not be privileged
#   CKV_K8S_9    hostNetwork should not be set
#   CKV_K8S_43   Image should use a digest, not a mutable tag
#   CKV_K8S_10/11/12/13   CPU/memory requests and limits not set
#   CKV_K8S_23   Should not run as root
apiVersion: apps/v1
kind: Deployment
metadata: { name: checkout }
spec:
  template:
    spec:
      containers:
        - name: checkout
          image: acme/checkout@sha256:9f1a2b...        # digest, not :latest
          securityContext:
            runAsNonRoot: true
            runAsUser: 10001
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            capabilities: { drop: ["ALL"] }
          resources:
            requests: { cpu: "100m", memory: "128Mi" }
            limits:   { cpu: "500m", memory: "256Mi" }
# kyverno-policy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: { name: disallow-privileged-and-latest }
spec:
  validationFailureAction: Enforce
  rules:
    - name: no-privileged
      match: { any: [{ resources: { kinds: ["Pod"] } }] }
      validate:
        message: "Privileged containers are not allowed."
        pattern:
          spec:
            containers:
              - =(securityContext): { =(privileged): "false" }
    - name: no-latest-tag
      match: { any: [{ resources: { kinds: ["Pod"] } }] }
      validate:
        message: "Images must not use the :latest tag."
        pattern:
          spec:
            containers:
              - image: "!*:latest"
kubectl apply -f kyverno-policy.yaml
kubectl run bad-pod --image=nginx:latest --overrides='{"spec":{"containers":[{"name":"bad-pod","image":"nginx:latest","securityContext":{"privileged":true}}]}}' --dry-run=server
#   Error from server: admission webhook "validate.kyverno.svc-fail" denied the request:
#   Privileged containers are not allowed.

Why: the manifest's problems share a root cause with the bank's Terraform challenge — Kubernetes, like AWS, defaults containers to the more permissive option unless a manifest explicitly locks them down, so "the Deployment applies cleanly" is never the same claim as "the Deployment is hardened." Checkov catches this at review time, before kubectl apply ever runs — that's shifting left applied to Kubernetes manifests the same way it applies to Terraform plans. The Kyverno policy is what turns a one-time cleanup into a standing guarantee: without an enforced admission rule, the next engineer who copies this Deployment as a template reintroduces the exact same gap, and nothing in the review process is guaranteed to catch it a second time.

Tool references: Checkov, Kyverno, OPA & Conftest. Background: the blueprint chapter, Kubernetes Security Deep Dive.

Challenge 5 · Twenty-five findings, and one control an auditor is waiting on (20 pts)

DefectDojo just aggregated this week's scan output — container, SCA, SAST, DAST, and an IaC pass over the cluster's RBAC — into roughly 25 open findings. You have the rest of the afternoon before sprint planning, where you need to defend a short list, and an auditor is waiting on evidence that no service account outside an approved list holds cluster-admin.

SourceFindingCVSSEPSSKEV?ExposureFix available?
ContainerRedis 6.0.9 running with no requirepass, bound to 0.0.0.0 in a dev namespace left running9.8*NoInternet-facing (dev env)Yes — enable auth, bind internal
IaC / RBACClusterRoleBinding grants cluster-admin to the CI pipeline's service accountNoInternal, but CI runs against prodYes — scope to least privilege
SASTAdmin-only /internal/refund endpoint missing a role check8.8NoInternet-facingYes — add authorization check
SCAOpenSSL X.509 parsing CVE in the base image8.10.15NoInternet-facingYes — bump base image
DASTMissing X-Frame-Options on a public marketing page (clickjacking)4.30.01NoInternet-facing, publicYes — one header
SCAPrototype-pollution CVE in a frontend build-time-only devDependency9.10.01NoNever ships to productionYes — bump version
ContainerSame unauthenticated Redis instance, reported again by a second scanner9.8*Noduplicateduplicate

* The scanner assigns this misconfiguration a generic severity score, not a CVSS score pulled from an actual CVE record — Redis running with no auth isn't a vulnerability with a CVE ID, it's a deployment choice. EPSS, which predicts exploitation probability for known CVEs specifically, doesn't apply to it at all.

Your task:

  1. Deduplicate the pile, then rank the rest and pick your top 3, weighing severity, EPSS, KEV status, and exposure together rather than raw CVSS alone — one-line justification per pick.
  2. Assign each of your top 3 a remediation SLA.
  3. Write a Rego/Conftest policy that continuously proves the RBAC finding stays fixed — no ClusterRoleBinding grants cluster-admin to a subject outside an approved allowlist — run it against the live cluster's bindings, and produce the JSON result as the auditor's evidence artifact.

Done when: your top-3 list and SLAs are written down with a justification that references at least one factor beyond raw CVSS for each item, and conftest test -o json against the cluster's current ClusterRoleBindings produces a results file with your RBAC control passing.

Show the worked solution
RankFindingJustificationSLA
1Unauthenticated Redis, internet-facingTrivially exploitable with zero skill required — no CVE, no EPSS to consult, but full read/write on a live datastore reachable from the open internet is about as bad as exposure getsImmediate — pull it off the internet today
2CI service account holds cluster-adminNo CVSS score attached, but the blast radius is the whole cluster: a single compromised CI job (a very common real-world entry point) becomes a full cluster compromise, not a contained oneThis sprint — scope to least privilege
3Missing authz on /internal/refundInternet-facing business-logic access-control gap with direct financial impact; high CVSS and no mitigating control in front of itThis sprint

Dropped, correctly: the clickjacking finding (real but low severity, cheap to batch with other header fixes later); the prototype-pollution CVE (never reaches production, so its 9.1 CVSS is nearly irrelevant to actual risk); and the OpenSSL CVE is de-prioritized, not dropped — the specific vulnerable X.509 parsing path only triggers on certain punycode-encoded certificate names during client-certificate verification, which this service's traffic pattern doesn't exercise, and the OpenSSL project itself downgraded this exact CVE's severity after further analysis. Batch it on the normal patch cycle rather than treating the 8.1 CVSS number as the whole story — but write down why it was downgraded, because "we decided it didn't matter" without that reasoning on record is indistinguishable from skipping it.

# policy/rbac_cluster_admin.rego
package kubernetes.rbac

allowlist := {"eks-admin-group", "platform-team-sa"}

deny[msg] {
  crb := input.items[_]
  crb.roleRef.name == "cluster-admin"
  subj := crb.subjects[_]
  not allowlist[subj.name]
  msg := sprintf("ClusterRoleBinding %v grants cluster-admin to unapproved subject %v", [crb.metadata.name, subj.name])
}
# fix the finding first, then generate evidence against the corrected state
kubectl delete clusterrolebinding ci-cluster-admin-binding
kubectl create clusterrolebinding ci-deploy-scoped --clusterrole=deploy-only --serviceaccount=ci:ci-runner

kubectl get clusterrolebindings -o json > evidence/crbs.json
conftest test --policy policy/ evidence/crbs.json -o json > evidence/rbac-control-result.json
cat evidence/rbac-control-result.json   # 0 failures — this is the auditor's evidence artifact

Why: the whole exercise turns on refusing to let a single number — CVSS, or its absence — do all the deciding. The unauthenticated Redis instance has no CVE and therefore no CVSS or EPSS in the strict sense, yet it outranks findings with a formal 8+ score, because exposure and ease of exploitation matter independently of whether NVD assigned a number to it. The RBAC finding is the same shape from the other direction: no CVSS at all, ranked #2 anyway, because blast radius is a real risk dimension a vulnerability scanner's scoring model doesn't capture. The compliance half closes the loop the way this course keeps insisting it should: the fix isn't "we deleted one bad binding," it's "we deleted the binding and there's now a re-runnable, machine-readable check proving it stays deleted" — that's the difference between a cleanup and a control.

Tool references: DefectDojo, OPA & Conftest. Background: the blueprint chapter, Compliance as Code at Scale.

Score yourself

☺ Like you're 10: Grade honestly after a break, add up the five numbers, and look hardest at whichever one is lowest — that's the one still telling you something useful.

Mark after stepping away for at least fifteen minutes — scoring your own work while still adrenalised from the clock produces generous nonsense. For each challenge, use the same rubric bands this course's practice bank uses, since it's the same 100-point scale the real CDP report is graded against:

BandWhat it looks like
18–20All tasks done inside the time-box, done-when check passed on the first try, docs used only to confirm one detail
14–17Done inside the time-box and the check passed, but you needed the worked solution for one field, flag, or command
8–13Ran out of time, or the fix was half-right — the scanner still shows a finding, or you never actually re-ran the verification step
0–7Didn't recognize the vulnerability class, the tool, or the resource shape at all
#ChallengeDomainPointsYour score
1The convert job and the token that got "removed"Static analysis & secrets20
2A transitive CVE with a KEV listingSoftware composition analysis20
3Authenticated DAST against a JSON APIDynamic analysis20
4A Kubernetes manifest, and a policy so it can't regressInfrastructure as code20
5Twenty-five findings, and one control an auditor is waiting onVuln triage & compliance evidence20
Total100

The bar is 80. Because the paper totals 100, your raw score is your percentage. If a single domain scored under 14, that's not noise the total can hide — go back to the specific blueprint chapter and deep-dive linked under that challenge above, then re-run that exact scenario cold in 48 hours before you touch anything else.

DomainAvailableYoursIf under 14, go here
Static Analysis & Secrets Detection20Static Analysis & Secrets Detection, then re-drill C3 in the practice bank and the leaked-credential-triage drill.
Software Composition Analysis20Software Composition Analysis, then the vulnerable-dependency fire drill.
Dynamic Analysis20Dynamic Analysis in Practice, then API Security in Depth.
Infrastructure as Code Hardening20Infrastructure-as-Code Hardening, then the broken-Terraform drill and the policy-as-code drill.
Vuln Triage & Compliance Evidence20Vulnerability Management & Triage and Compliance as Code at Scale.

The readiness checklist — book it, or one more pass?

☺ Like you're 10: A close-enough total isn't the same as being ready. Check every box honestly before you spend real money on the real exam.

A score by itself can lie to you — a 78 built from four strong domains and one near-zero is a completely different candidate from a 78 spread evenly across all five, and only one of them is one unlucky challenge draw away from failing the real thing. Work through every row below honestly. Any single "no" means the honest answer is one more study pass, not the real exam.

CheckWhy it's non-negotiable
Total score is 80 or higherIt's the actual pass mark — see the exam guide's standing note to re-verify this figure on the vendor's own page before you rely on it.
No single challenge scored under 14The real exam draws five specific challenges, not an average of many. A near-zero domain here is a near-zero domain there too, if it happens to be the one drawn.
All five challenges were attempted, none skipped entirelyAn untouched challenge is a guaranteed zero on the real thing. If more than one got skipped, the diagnosis is pacing, not knowledge — revisit the time budget above, not the content.
You captured evidence the moment each task closed, not after the factTry assembling a one-paragraph-per-task report from what you captured, right now. If you can't reconstruct what you did, the real report window will go the same way.
You ran this cold — no lookups beyond official tool docs, no notes from Sets 1–3, no assistantA score earned with the guardrails down measures a candidate who doesn't exist on the real exam day.
You've already re-drilled whatever Sets 1–3 flagged as weakIf this is the first time you're seeing a gap Set 1 already surfaced, this sitting was scored too early.
◆ The actual verdict

All six boxes checked, honestly: book it. ↗ Practical DevSecOps official page — confirm the current price, scheduling process, and passing score there before you pay, since those figures move. Any box unchecked: pick the domain from the table above with the lowest score, work its blueprint chapter and drill for a few days, then come back and re-sit this exact page cold in a week — not Set 1 again, since it's no longer a cold measurement for you. Sit Set 4 as many times as it takes to check every box; the real exam doesn't award partial credit for having felt ready.

🎬 At the Shift-Left Squad
🦫

Benny the Beaver: Sat Set 4. Seventy-eight out of a hundred. Booking the real exam this afternoon — we're basically there.

🐢

Timmy the Turtle: Seventy-eight is under eighty. What was the domain breakdown, not just the total?

🦫

Benny: I... didn't check per challenge. Does that matter if the total's close?

🦊

Foxy: It matters if the seventy-eight is hiding a zero somewhere. Which challenge did you flag and skip?

🦫

Benny: Challenge 3 — the authenticated DAST one. Ran out of time wiring up the bearer-token script and never got the scan running properly.

🐿️

Nutty the Squirrel: So if the real exam's fifth challenge draws on exactly that skill, you don't have a seventy-eight attempt — you have a near-zero on a fifth of the paper. Did you at least write down where you stalled?

🦫

Benny: ...no. I moved on and forgot about it.

🦉

Professor Owl: Then the honest read isn't "close, book it" — it's one more pass on authenticated DAST, specifically, and a cold re-sit of this exact page afterward. The checklist exists so the total doesn't get to lie to you.

✓ Checkpoint

1. What's the one thing Set 4 measures that Sets 1–3 deliberately don't, and why does "reading the briefs cold" matter for that measurement to mean anything? 2. Why doesn't EPSS apply to the unauthenticated Redis finding in Challenge 5, even though the scanner gave it a 9.8? 3. In Challenge 3, why doesn't ZAP's form-based authentication apply to orders-api, and what has to happen instead? 4. In Challenge 2, what makes the Text4Shell finding reachable rather than theoretical? 5. Per the readiness checklist, is an 82/100 total with one challenge scored 4/20 enough to book the real exam? Why or why not?

Check your answers
  1. Whether you can produce a working fix against a live environment, cold, under the real clock — not whether you recognize the material, which is what Sets 1–3 and the practice bank measure. Reading the briefs before the timer starts turns this into a recognition exercise like any other page on the site, and the resulting score stops meaning anything about exam-day readiness.
  2. Because EPSS predicts the probability of exploitation for a specific, known CVE — the unauthenticated Redis instance isn't a CVE at all, it's a deployment misconfiguration, so there's no CVE record for EPSS to score. The scanner's 9.8 is a generic severity estimate, not a value pulled from an actual vulnerability database entry, which is exactly why exposure and ease of exploitation had to be argued directly instead of citing a score.
  3. Form-based authentication submits credentials through an HTML <form>, and a pure JSON API has no form to submit — there's nothing for that job type to fill in. Instead, a script-based authentication job (or a Replacer rule for a static token) has to call the token endpoint directly and attach the result as an Authorization: Bearer … header on every subsequent request.
  4. StringSubstitutor.createInterpolator() registers the default lookups — including script:, dns:, and url: — that are vulnerable in commons-text before 1.10.0, and the code feeds it a template string that's explicitly user-editable. Attacker-controlled input reaching an interpolator with those lookups still enabled is the exact condition CVE-2022-42889 needs; a "we don't call eval" argument would have missed it.
  5. No. The readiness checklist requires no single challenge under 14, specifically because the real exam draws five fixed challenges rather than averaging many — a near-zero on one domain here is a near-zero on that domain on exam day too, if the exam happens to test it. An 82 total with a 4/20 buried inside it is a candidate one unlucky challenge draw away from failing, which is precisely the pattern a raw total can't reveal on its own.

That's the final paper. Set the timer, close every tab except the tools' own docs, and let the five scenarios above tell you something the earlier sets couldn't — because this time, you're reading them for the first time. Score it honestly, run every row of the readiness checklist, and let the answer — not the total alone — decide whether today is the day you book the real CDP.

📄 The four papers

Set 1 · Set 2 · Set 3 · Set 4 (you are here). Work them in order — each one is sealed until you sit it, so skimming ahead spends a cold measurement you don't get back. See the practice challenge bank for untimed, per-domain reps, and the CDP exam guide for what happens after you book.