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 1–3, 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.
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.
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.
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 windowThe 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.
| Block | What | Budget | Running total |
|---|---|---|---|
| Read | All five briefs, no terminal yet | 15 min | 15 |
| Challenge 1 | Static analysis & secrets detection | 65 min | 80 |
| Challenge 2 | Software composition analysis | 70 min | 150 |
| Challenge 3 | Dynamic analysis in practice | 70 min | 220 |
| Challenge 4 | Infrastructure-as-code hardening | 65 min | 285 |
| Challenge 5 | Vulnerability triage & compliance evidence | 65 min | 350 |
| Verify & evidence sweep | Re-run every done-when, confirm your screenshots exist | 10 min | 360 |
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.
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.
Given — routes/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:
- Run Semgrep against the service, identify the command-injection sink, and rewrite it so a filename like
x.jpg; rm -rf /or a width like100; curl evil.tldcannot execute arbitrary shell. - Run gitleaks or TruffleHog against the full git history and locate the deleted Twilio token, with its commit SHA.
- 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 exitWhy: 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:
- Run an SCA scan, identify the CVE affecting the transitive
commons-textdependency, and record its CVSS score, its EPSS score, and whether it appears in CISA's KEV catalog. - Decide whether the finding is reachable, based on the snippet above, and justify the decision in one sentence.
- Remediate by pinning
commons-textto a fixed version via a dependency-management override — without bumping the wholereport-formatting-libartifact, 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:
- Obtain a token via a scripted call to
/auth/token, and configure the ZAP Automation Framework to attach it as anAuthorization: Bearer …header on every request in the scan — form-based authentication doesn't apply here, since there's no HTML login form to submit. - 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. - 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: trueYour task:
- 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).
- Fix the manifest: remove
privilegedandhostNetwork, pin the image to a digest instead of:latest, add a hardenedsecurityContext(non-root, read-only root filesystem, all capabilities dropped), and add resource requests/limits. - Add a Kyverno
ClusterPolicyinenforcemode that blocks privileged pods and mutable:latesttags 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.
| Source | Finding | CVSS | EPSS | KEV? | Exposure | Fix available? |
|---|---|---|---|---|---|---|
| Container | Redis 6.0.9 running with no requirepass, bound to 0.0.0.0 in a dev namespace left running | 9.8* | — | No | Internet-facing (dev env) | Yes — enable auth, bind internal |
| IaC / RBAC | ClusterRoleBinding grants cluster-admin to the CI pipeline's service account | — | — | No | Internal, but CI runs against prod | Yes — scope to least privilege |
| SAST | Admin-only /internal/refund endpoint missing a role check | 8.8 | — | No | Internet-facing | Yes — add authorization check |
| SCA | OpenSSL X.509 parsing CVE in the base image | 8.1 | 0.15 | No | Internet-facing | Yes — bump base image |
| DAST | Missing X-Frame-Options on a public marketing page (clickjacking) | 4.3 | 0.01 | No | Internet-facing, public | Yes — one header |
| SCA | Prototype-pollution CVE in a frontend build-time-only devDependency | 9.1 | 0.01 | No | Never ships to production | Yes — bump version |
| Container | Same unauthenticated Redis instance, reported again by a second scanner | 9.8* | — | No | duplicate | duplicate |
* 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:
- 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.
- Assign each of your top 3 a remediation SLA.
- Write a Rego/Conftest policy that continuously proves the RBAC finding stays fixed — no
ClusterRoleBindinggrantscluster-adminto 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
| Rank | Finding | Justification | SLA |
|---|---|---|---|
| 1 | Unauthenticated Redis, internet-facing | Trivially 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 gets | Immediate — pull it off the internet today |
| 2 | CI service account holds cluster-admin | No 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 one | This sprint — scope to least privilege |
| 3 | Missing authz on /internal/refund | Internet-facing business-logic access-control gap with direct financial impact; high CVSS and no mitigating control in front of it | This 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:
| Band | What it looks like |
|---|---|
| 18–20 | All tasks done inside the time-box, done-when check passed on the first try, docs used only to confirm one detail |
| 14–17 | Done inside the time-box and the check passed, but you needed the worked solution for one field, flag, or command |
| 8–13 | Ran 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–7 | Didn't recognize the vulnerability class, the tool, or the resource shape at all |
| # | Challenge | Domain | Points | Your score |
|---|---|---|---|---|
| 1 | The convert job and the token that got "removed" | Static analysis & secrets | 20 | |
| 2 | A transitive CVE with a KEV listing | Software composition analysis | 20 | |
| 3 | Authenticated DAST against a JSON API | Dynamic analysis | 20 | |
| 4 | A Kubernetes manifest, and a policy so it can't regress | Infrastructure as code | 20 | |
| 5 | Twenty-five findings, and one control an auditor is waiting on | Vuln triage & compliance evidence | 20 | |
| Total | 100 |
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.
| Domain | Available | Yours | If under 14, go here |
|---|---|---|---|
| Static Analysis & Secrets Detection | 20 | Static Analysis & Secrets Detection, then re-drill C3 in the practice bank and the leaked-credential-triage drill. | |
| Software Composition Analysis | 20 | Software Composition Analysis, then the vulnerable-dependency fire drill. | |
| Dynamic Analysis | 20 | Dynamic Analysis in Practice, then API Security in Depth. | |
| Infrastructure as Code Hardening | 20 | Infrastructure-as-Code Hardening, then the broken-Terraform drill and the policy-as-code drill. | |
| Vuln Triage & Compliance Evidence | 20 | Vulnerability 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.
| Check | Why it's non-negotiable |
|---|---|
| Total score is 80 or higher | It'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 14 | The 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 entirely | An 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 fact | Try 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 assistant | A 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 weak | If this is the first time you're seeing a gap Set 1 already surfaced, this sitting was scored too early. |
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.
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.
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
- 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.
- 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.
- 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 anAuthorization: Bearer …header on every subsequent request. StringSubstitutor.createInterpolator()registers the default lookups — includingscript:,dns:, andurl:— that are vulnerable in commons-text before 1.10.0, and the code feeds it atemplatestring 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.- 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.