Mock Exam · Set 2 — reweighted, sealed, worked
This is the second full sitting: five challenges, a hundred points, and the same 80-point bar the real Certified DevSecOps Professional exam uses, in the same shape the exam guide describes it — five live tasks against an environment, no multiple choice anywhere. What makes this Set 2 rather than a rerun of Set 1 isn't the format, it's the content and the weighting. Every scenario below is new — none of it is copied from the practice challenge bank or from a first sitting — and fifty of the hundred points sit in exactly two domains: container & supply-chain hardening and vulnerability-management triage, instead of the five-way even split this course's own study plan uses as its default weighting heuristic. That shift is deliberate. A candidate who only ever drilled an even split can still pattern-match a first sitting's specific fixes; Practical DevSecOps has never published a domain-weight table promising the real exam's five challenges divide themselves evenly either, so a paper that leans hard into two domains is closer to what an unlucky draw on the actual day could look like. Unlike the real exam, every challenge below folds its own worked solution directly beneath it — read that as a teaching document once you've genuinely attempted the task, not as an answer key to open first.
A first practice recital had you play five songs, equal time each. This one tells you, right before you sit down, that two of the five songs are now worth half the points between them — so you'd better be excellent at those two and still solid on the other three. It isn't a harder recital because the notes changed. It's harder because the scoring changed, and nobody let you pick which parts would matter more.
How Set 2 differs from Set 1 — and why the weighting shifted
☺ Like you're 10: Set 1 practiced five things equally. Set 2 tells you two of those five now count for more — so being merely fine at them isn't enough anymore.
The exam guide's own domain list — static analysis & secrets, software composition analysis, dynamic analysis, IaC hardening, and vulnerability management & compliance — is presented there as a study heuristic, not a published spec, precisely because Practical DevSecOps has never released a percentage breakdown for its five live challenges. The fairest default for a first sitting is to spread points evenly across that heuristic; this paper deliberately doesn't. Fifty points sit in the two domains most directly tied to the CDP's own grading mechanism — the written report proving a fix actually happened — because container/supply-chain hardening and vulnerability triage are where "I found it" and "I proved it's actually handled, and I can defend the priority I gave it" diverge the hardest.
| Domain | Even-split baseline | This paper (Set 2) | Delta |
|---|---|---|---|
| Container & supply-chain hardening | 20 | 25 | +5 |
| Vulnerability management & triage | 20 | 25 | +5 |
| Static analysis & secrets detection | 20 | 15 | −5 |
| Infrastructure-as-code hardening | 20 | 15 | −5 |
| Dynamic analysis & compliance evidence | 20 | 20 | — |
Reweighting doesn't add a sixth domain or drop one of the five — it changes how much a weak showing in any single one costs you. A candidate who's shaky on container hardening loses more here than on a balanced paper, and a candidate who's excellent at triage has more room to bank points there than an even split would give them. That's closer to what an unpublished, unweighted real exam can actually do to you on a given day.
Before the clock starts — target, rules, and how to use the worked solutions
☺ Like you're 10: Set up your practice target the night before, agree to the same rules the real exam uses, and don't open the answer key until you've honestly tried.
Work this paper against Vulnerly, the deliberately vulnerable Node.js/Postgres app the capstone lab track builds and hardens across its seven parts, or an equivalent throwaway target of your own — every challenge below assumes a small Express service backed by Postgres, containerized with Docker, deployed behind CI. If you haven't built the capstone yet, that's fine; each challenge is self-contained and gives you the exact starting code or config it needs.
- One clock, one sitting. Block a genuine six-hour window, the same length as the real challenge window, and don't split it across two evenings — a mock sat in two sessions measures a candidate who won't exist on the real day.
- No AI assistant, no chatbot. The real CDP explicitly disallows both during the challenge window — see the exam guide — so drilling with an assistant open to check syntax measures someone who won't be sitting the actual exam.
- Read all five briefs before you commit to Challenge 1. Five minutes spent scanning the whole paper is the highest-return five minutes of the sitting — you'll spot which challenge is going to eat the most of your budget before you're already inside it.
- Fold the solutions and leave them folded. Every worked solution below sits behind a
<details>summary specifically so it doesn't spoil the page for a genuine attempt. Time yourself, write down where you got stuck, and only then open the fold — reading a solution before an honest attempt turns a diagnostic into a reading exercise and tells you nothing about your own pacing. - Draft your report entries as you go. The real CDP gives a separate 24-hour window for a written report after the challenge window closes. Practice that habit here too: two or three sentences per challenge — what was broken, what caught it, what you did, and the one command or screenshot that proves it — written the moment you close a challenge out, not reconstructed afterward.
Before the timer starts: confirm Docker and a Postgres instance (or the capstone's docker-compose) come up clean, confirm trivy, syft, cosign, semgrep, gitleaks, checkov, conftest, and inspec are all on your PATH and print a version string, and confirm you can reach the OWASP ZAP docs and Sigstore docs if you genuinely get stuck — those are the closest analog to what the real exam's allowlist tends to permit. If any tool is missing, install it now. Never stop the clock once it starts to go fetch a binary.
The $899 cost, the 80/100 pass mark, the 6-hour and 24-hour windows, and the "no AI assistant" rule are Practical DevSecOps' own published figures as of this course's last review — confirm current terms on the vendor's own CDP page before you rely on any of them. Separately, and just as important: specific Checkov/Trivy check IDs, Semgrep rule syntax, and gitleaks' default rule pack all drift between tool releases faster than the underlying vulnerability classes do. Every ID quoted in a worked solution below is illustrative of the shape of the output, not a promise that your installed version prints the identical string — treat the finding name as the thing worth memorizing, not the numeric suffix.
Your time budget across five challenges
☺ Like you're 10: Give each of the five jobs its own slice of six hours, and when a slice runs out, write down where you stopped and move to the next one.
Budgets below are proportional to points, with the two reweighted challenges getting the largest shares on the paper — they're worth the most, but that's also exactly why the flag-and-move discipline matters most on them. If a challenge runs roughly 1.3× its budget without a passing done-when check, stop, write one line about where you stalled, leave your partial work in place, and move on. Partial, verified progress on four challenges beats a perfect first challenge and three untouched ones.
| Block | Challenge | Points | Budget | Running total |
|---|---|---|---|---|
| Read all five briefs | — | — | 10 min | 10 |
| Challenge 1 | Container & Supply-Chain Hardening | 25 | 80 min | 90 |
| Challenge 2 | Vulnerability Management & Triage | 25 | 75 min | 165 |
| Challenge 3 | Static Analysis & Secrets Detection | 15 | 45 min | 210 |
| Challenge 4 | Infrastructure-as-Code Hardening | 15 | 45 min | 255 |
| Challenge 5 | Dynamic Analysis & Compliance Evidence | 20 | 65 min | 320 |
| Verify every done-when, capture evidence | — | — | 40 min | 360 |
The paper — 5 challenges, 100 points
☺ Like you're 10: Five real jobs on one small app. Read the setup, do the job, check it against the done-when line, then open the fold to see exactly how it should have gone.
Every challenge below gives you the vulnerable starting state, a numbered task list, a done-when check you can run yourself, and a worked solution folded beneath it. Attempt each one cold before you open its fold.
Challenge 1 — Container & Supply-Chain Hardening
25 points · budget 80 min · maps to blueprint chapter 7 and capstone Part 4
Vulnerly's Dockerfile builds and ships as a single stage, running as root on a full node:18 base image, and leaks a registry token into the image's build history:
FROM node:18 ARG NPM_TOKEN WORKDIR /app COPY . . RUN npm install EXPOSE 8080 CMD ["node", "server.js"]
Your task:
- Rewrite the Dockerfile as a multi-stage build: install dependencies without leaking
NPM_TOKENinto any layer, then ship only the runtime artifact on a minimal, non-root base image. - Scan the built runtime image with Trivy and get to zero HIGH/CRITICAL findings against the actual shipped tag.
- Generate a CycloneDX SBOM for the image with Syft.
- Sign the image keylessly with cosign and verify the signature.
- Explain, in one sentence, how a dependency-confusion package (an internal-looking package name resolving to the public npm registry instead of your private one) would get caught before it ships.
Done when: docker history on the final image shows no layer containing NPM_TOKEN's value, trivy image --severity HIGH,CRITICAL --exit-code 1 against the runtime tag exits 0, an SBOM file exists, and cosign verify against the signed tag succeeds.
Worked solution
# ---- build stage ----
FROM node:18-slim AS build
WORKDIR /app
COPY package*.json ./
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci --omit=dev
COPY . .
# ---- runtime stage ----
FROM gcr.io/distroless/nodejs18-debian12
WORKDIR /app
COPY --from=build /app /app
USER nonroot:nonroot
EXPOSE 8080
CMD ["server.js"]# build, passing the token in as a BuildKit secret — never an ARG or ENV DOCKER_BUILDKIT=1 docker build \ --secret id=npm_token,env=NPM_TOKEN \ -t vulnerly:hardened . # secret mounts are never written to a layer, unlike ARG/ENV — confirm it: docker history vulnerly:hardened --no-trunc | grep -i npm_token # empty trivy image --severity HIGH,CRITICAL --exit-code 1 vulnerly:hardened syft vulnerly:hardened -o cyclonedx-json=sbom.cdx.json cosign sign --yes vulnerly:hardened cosign verify vulnerly:hardened \ --certificate-identity-regexp ".*" \ --certificate-oidc-issuer https://token.actions.githubusercontent.com
Why: ARG and ENV both persist into the image's layer history and are recoverable from any pulled image with docker history --no-trunc, even from a stage that never made it into the final tag — a BuildKit secret mount is bind-mounted only for the duration of the one RUN that needs it and is never written to any layer at all. The distroless, non-root final stage removes the shell, package manager, and everything else an attacker would use once inside a container, and matters more than it looks like it should: a container escape is far less useful against an image with no shell to escalate into. On dependency confusion — an .npmrc that pins your organization's package scope to your private registry (@vulnerly:registry=https://registry.vulnerly.internal) means a public-registry package impersonating that scope simply doesn't resolve; a supply-chain-aware SCA/CI check that flags any dependency not present in your private registry's own index catches the rest.
Tool references: Trivy, Syft & Grype, Sigstore & cosign. Background: Container Runtime Security, Software Bills of Materials.
Challenge 2 — Vulnerability Management & Triage
25 points · budget 75 min · maps to blueprint chapter 9
The week's scans just landed. Four findings, from three different tools, against the hardened build from Challenge 1 and the staging deployment behind it:
| # | Source | Finding | Severity | EPSS | KEV? |
|---|---|---|---|---|---|
| A | Trivy (SCA, container scan) | CVE-2024-3XXXX (illustrative) in form-data@3.0.0, transitive via an internal upload helper | High (CVSS 8.1) | 0.09 | No |
| B | Semgrep (SAST) | Unescaped req.query.msg written into the response in routes/feedback.js — reflected XSS, CWE-79 | High | — | — |
| C | ZAP (DAST, same staging build) | Reflected XSS alert on GET /feedback?msg= | High | — | — |
| D | Trivy (container scan) | CVE-2024-5XXXX (illustrative), Critical, in an OS package — found on vulnerly:build | Critical (CVSS 9.8) | 0.31 | No |
Your task:
- Identify which two findings are the same underlying root cause reported by two different tool types, and dedupe them into one ticket.
- For Finding A, determine whether the vulnerable code path is reachable from untrusted input: it's only exercised by an internal, authenticated CSV-upload admin route with no path from the public API.
- For Finding D, check which image tag was actually scanned before you treat it as a live production finding at all.
- Apply this course's severity-to-SLA policy (Critical: 7 days, High: 30 days, Medium: 90 days, Low: 120 days — see the canonical table) to assign a final disposition and priority to each ticket.
Done when: you have exactly three tickets, not four; each cites every source that reported it; and each carries a disposition (Active/fix-now, Active/scheduled, or Not Applicable) with a one-sentence justification that references something beyond the raw severity label.
Worked solution
| Ticket | Sources | Disposition | SLA / priority | Justification |
|---|---|---|---|---|
| 1 | B + C (deduped — C marked Duplicate under B) | Active — fix now | High / 30-day band, but confirmed by two independent methods — don't wait for day 29 | SAST found the taint path, DAST confirmed it's live and unauthenticated on the same endpoint and parameter — that's about as confirmed as a finding gets before someone's actually exploited it. |
| 2 | A | Active — scheduled, not urgent | High / 30-day band, use the full window | CVSS is High, but the path is only reachable from an internal, authenticated admin route with no untrusted-input path, and EPSS is low (0.09). Reachability and EPSS argue against escalating within the High band, not for downgrading the severity label itself. |
| 3 | D | Not Applicable — re-verify, don't celebrate a fix | N/A until re-scanned correctly | The scan targeted vulnerly:build, the discarded intermediate stage, not vulnerly:hardened, the tag that actually ships. Re-run Trivy against the shipped tag before treating this as resolved or as a real production Critical. |
# confirm which tag actually shipped, and re-scan the right one docker images | grep vulnerly trivy image --severity CRITICAL vulnerly:hardened # re-run scoped to the real artifact
Why: the trap in Finding A is treating "Semgrep found it" and "ZAP found it" as two findings just because two tools reported it — DefectDojo's own dedup logic keeps the older import Active and marks the newer one Duplicate under it precisely so a backlog doesn't double-count one bug as two open tickets and two separate SLA clocks. The trap in Finding D is scope: a multi-stage Dockerfile's earlier stages never ship, so a scanner pointed at the wrong tag manufactures a finding that isn't in production at all — the fix isn't a code change, it's re-running the scan against the artifact your registry actually serves. EPSS only applies to CVE-numbered SCA findings (Findings A and D); Findings B and C are code-level SAST/DAST results with no CVE and therefore no EPSS score of their own — severity there comes from the tool's own risk/confidence rating and confirmed exploitability, not a probability feed.
Tool references: DefectDojo, Trivy, Semgrep, OWASP ZAP. Background: the blueprint chapter. More reps: the vulnerable-dependency fire drill.
Challenge 3 — Static Analysis & Secrets Detection
15 points · budget 45 min · maps to blueprint chapter 5
Vulnerly's image-resize endpoint shells out to ImageMagick by building a command string, and its auth config hardcodes the JWT signing secret directly in source:
const { exec } = require('child_process');
app.post('/resize', (req, res) => {
const { file, width } = req.body;
exec(`convert ${file} -resize ${width} /tmp/out.jpg`, (err) => {
if (err) return res.status(500).send('resize failed');
res.sendFile('/tmp/out.jpg');
});
});// config/auth.js
module.exports = {
jwtSecret: "sup3r-s3cret-signing-key-do-not-share"
};Your task:
- Fix the command injection in
/resize—fileorwidthcontaining shell metacharacters currently reaches a real shell. - Write a custom Semgrep rule that catches
child_process.exec()built from a template literal or string concatenation, without flagging safeexecFile/spawncalls. - Find the hardcoded JWT secret. Note that gitleaks' default rule pack targets vendor key formats (
AKIA…,ghp_…, and similar) and will not flag a generic app config variable like this one without help. - Explain the rotation plan for a JWT signing secret specifically — it's not the same operation as rotating an AWS key.
Done when: exec no longer appears anywhere in /resize; your custom Semgrep rule flags the original file and is silent on the fixed one; and gitleaks detect with your custom rule reports the secret's commit.
Worked solution
const { execFile } = require('child_process');
app.post('/resize', (req, res) => {
const { file, width } = req.body;
if (!/^[0-9]{1,4}$/.test(width)) return res.status(400).send('bad width');
if (!/^[a-zA-Z0-9_-]+\.(jpg|png)$/.test(file)) return res.status(400).send('bad file');
execFile('convert', [file, '-resize', width, '/tmp/out.jpg'], (err) => {
if (err) return res.status(500).send('resize failed');
res.sendFile('/tmp/out.jpg');
});
});# .semgrep/rules/node-exec-shell-interpolation.yaml
rules:
- id: node-exec-shell-interpolation
languages: [javascript, typescript]
severity: ERROR
message: >
child_process.exec() runs its argument through a shell. Building that
argument from a template literal or concatenation lets shell
metacharacters in user input change what actually runs. Use
execFile()/spawn() with an argv array instead.
patterns:
- pattern-either:
- pattern: exec(`...${$X}...`, ...)
- pattern: exec("..." + $X + "...", ...)
metadata:
cwe: "CWE-78: OS Command Injection"
owasp: "A03:2021 - Injection"# .gitleaks.toml — extend the default pack with an app-specific pattern
[[rules]]
id = "vulnerly-jwt-signing-secret"
description = "Hardcoded JWT signing secret in app config"
regex = '''(?i)jwt[_-]?secret['"]?\s*[:=]\s*["'][A-Za-z0-9!@#$%^&*_-]{12,}["']'''
tags = ["secret", "jwt", "custom"]gitleaks detect --source . --config .gitleaks.toml -v # Finding: vulnerly-jwt-signing-secret # File: config/auth.js Commit: 7f2a1c9 vault kv put secret/vulnerly/auth jwt_signing_secret="$(openssl rand -base64 48)"
Why: exec() hands its whole first argument to /bin/sh -c, so any unsanitized character in that string is shell syntax as far as the OS is concerned; execFile() takes a program and an argv array with no shell in between, so ;, |, and backticks in file or width are just inert characters in one argument. Gitleaks ships detection rules for known vendor key formats because those have a recognizable shape (AKIA plus 16 characters, for example); an app's own config variable name has no such universal shape, so catching it requires a rule written for that specific codebase — a gap teams routinely miss because the default pack feels comprehensive. Rotating a JWT signing secret is operationally different from rotating an API key: every token already issued under the old secret becomes unverifiable the instant you swap it, which either logs out every active session at once or requires a dual-key verification window — accept signatures from both the old and new secret for a grace period until the old tokens' natural expiry, then drop the old key — so the fix doesn't create its own outage.
Tool references: Semgrep, gitleaks, HashiCorp Vault. Background: the blueprint chapter. More reps: the leaked-credential-triage drill.
Challenge 4 — Infrastructure-as-Code Hardening
15 points · budget 45 min · maps to blueprint chapter 7
Vulnerly's Terraform provisions its Postgres backend publicly accessible and unencrypted, and its application role carries an unrestricted IAM policy:
resource "aws_db_instance" "vulnerly_db" {
identifier = "vulnerly-db"
engine = "postgres"
instance_class = "db.t3.micro"
allocated_storage = 20
username = "vulnerly_app"
password = var.db_password
publicly_accessible = true
}
resource "aws_iam_role_policy" "app_role" {
name = "vulnerly-app-policy"
role = aws_iam_role.app.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "*"
Resource = "*"
}]
})
}Your task:
- Scan the plan with
trivy configand Checkov, and list the findings against both resources. - Fix the RDS instance: not publicly accessible, encrypted at rest.
- Fix the IAM policy: scope it to the specific actions and resources the app actually needs instead of a wildcard grant.
- Write an OPA/Conftest policy that fails a plan containing either the public RDS instance or the wildcard IAM policy, and passes the fixed plan.
Done when: a re-run of both scanners shows zero findings for these two resources, and conftest test against the original plan JSON fails with your policy's message while the fixed plan passes.
Worked solution
# exact check IDs drift by tool version — the finding names are what to memorize trivy config --severity HIGH,CRITICAL infra/ # aws_db_instance.vulnerly_db — publicly_accessible is true # aws_db_instance.vulnerly_db — storage_encrypted not set (defaults to false) # aws_iam_role_policy.app_role — Action "*" combined with Resource "*" grants full admin checkov -d infra/ --framework terraform --compact # CKV_AWS_17 aws_db_instance.vulnerly_db — RDS instance should not be publicly accessible: FAILED # CKV_AWS_16 aws_db_instance.vulnerly_db — RDS instance should have encryption at rest: FAILED # CKV_AWS_1 aws_iam_role_policy.app_role — IAM policy should avoid wildcard actions: FAILED
resource "aws_db_instance" "vulnerly_db" {
identifier = "vulnerly-db"
engine = "postgres"
instance_class = "db.t3.micro"
allocated_storage = 20
username = "vulnerly_app"
password = var.db_password
publicly_accessible = false
storage_encrypted = true
kms_key_id = aws_kms_key.rds.arn
}
resource "aws_iam_role_policy" "app_role" {
name = "vulnerly-app-policy"
role = aws_iam_role.app.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject", "secretsmanager:GetSecretValue"]
Resource = [
aws_s3_bucket.uploads.arn,
"${aws_s3_bucket.uploads.arn}/*",
aws_secretsmanager_secret.app.arn
]
}]
})
}# policy/vulnerly_iac.rego
package terraform.security
deny[msg] {
rds := input.resource_changes[_]
rds.type == "aws_db_instance"
rds.change.after.publicly_accessible == true
msg := sprintf("RDS instance %v is publicly accessible", [rds.address])
}
deny[msg] {
role_policy := input.resource_changes[_]
role_policy.type == "aws_iam_role_policy"
doc := json.unmarshal(role_policy.change.after.policy)
stmt := doc.Statement[_]
stmt.Effect == "Allow"
stmt.Action == "*"
stmt.Resource == "*"
msg := sprintf("IAM policy %v grants wildcard admin (Action=* Resource=*)", [role_policy.address])
}terraform show -json tfplan > plan.json conftest test --policy policy/ plan.json
Why: both findings share the same root cause as every IaC misconfiguration this course covers — AWS defaults to the less secure option unless a companion setting explicitly locks it down, so "I created the database" and "I created the database and blocked public access on it" are different claims. The IAM policy's policy attribute is a JSON-encoded string inside the plan JSON, not a nested object — json.unmarshal() is what turns it back into something Rego can actually walk; a naive rule that tries to index into .policy.Statement directly against the raw plan JSON silently matches nothing and gives false confidence that the gate works.
Tool references: Checkov, Trivy, OPA & Conftest. Background: the blueprint chapter. More reps: the broken-Terraform-plan drill, the policy-as-code drill.
Challenge 5 — Dynamic Analysis & Compliance Evidence
20 points · budget 65 min · maps to blueprint chapter 6 and chapter 8
An authenticated ZAP Automation Framework run against Vulnerly's staging deployment (use the same context/authentication job shape as the practice bank's C5 if you need the full YAML) returns three alerts:
| Alert | Risk | Confidence |
|---|---|---|
Missing Strict-Transport-Security and Content-Security-Policy headers | Low/Medium | High |
Session cookie missing Secure and HttpOnly flags | Medium | High |
Reflected XSS on POST /profile/bio — the bio field is echoed unescaped on the next page load | High | Medium |
Your task:
- Rank the three alerts by risk × confidence and state your fix order in one line.
- Fix all three: add security headers, correct the cookie flags, and escape the bio field's output.
- Map the header and cookie fixes to a SOC 2 control, and write an InSpec control that asserts the headers are present on the live staging endpoint — re-runnable evidence, not a screenshot.
Done when: a re-run of the ZAP job shows the XSS alert gone, and inspec exec against staging produces a passing JSON result for the header control.
Worked solution
Fix order: the reflected XSS first — High risk despite Medium confidence, because a confirmed unauthenticated injection point outranks two lower-risk hardening gaps regardless of how sure the scanner is about either of those. Cookie flags second — Medium risk, High confidence, and a five-line fix. Headers last — real but Low/Medium risk, defense-in-depth rather than an active exploit path.
const helmet = require('helmet');
const escapeHtml = require('escape-html');
app.use(helmet({
contentSecurityPolicy: { directives: { defaultSrc: ["'self'"] } },
hsts: { maxAge: 31536000, includeSubDomains: true }
}));
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: { secure: true, httpOnly: true, sameSite: 'strict' }
}));
app.post('/profile/bio', (req, res) => {
const bio = escapeHtml(req.body.bio);
db.query('UPDATE profiles SET bio = $1 WHERE user_id = $2', [bio, req.user.id])
.then(() => res.redirect('/profile'));
});# controls/transport_headers.rb
control 'vulnerly-transport-headers-01' do
impact 1.0
title 'Staging enforces transport-security headers and secure session cookies'
desc 'Evidence for SOC 2 CC6.6 — protecting data in transit with enforced transport controls.'
describe http('https://staging.vulnerly.internal/', ssl_verify: true) do
its('headers.Strict-Transport-Security') { should_not be_nil }
its('headers.Content-Security-Policy') { should_not be_nil }
its(['headers', 'Set-Cookie']) { should match(/Secure/) }
its(['headers', 'Set-Cookie']) { should match(/HttpOnly/) }
end
endinspec exec ./controls --target https://staging.vulnerly.internal \ --reporter cli json:evidence.json
Why: risk × confidence, not risk alone, is what decides fix order — a High-risk, Medium-confidence finding still means "worth manually confirming and fixing first," while a Low-risk, High-confidence finding is real but cheap enough to batch. Escaping output at render time (rather than trying to sanitize on input) survives every code path that touches the bio field, including ones added later, which is the same reasoning chapter 5's SQL-injection fix relies on: separate the untrusted data from the syntax it's rendered into, structurally, instead of trying to filter every dangerous character by hand. The InSpec http resource is a genuinely different check shape from a host-level control (SSH config, a running service, a package) — it asserts something about a live HTTP response instead of local machine state, which is exactly the evidence a SOC 2 auditor asking "is data in transit actually protected" wants to see re-run on demand rather than described in a slide.
Tool references: OWASP ZAP, InSpec. Background: chapter 6, chapter 8. More reps: capstone Part 6, capstone Part 7.
Score yourself
☺ Like you're 10: Add up your points, compare to 80, then look at which domain lost you the most — that second part is the useful part.
Mark after a break, not immediately after finishing — adrenaline makes generous graders. Award full points only when the done-when check actually passed on your own environment, half when the fix is broadly right but the check didn't pass cleanly, and zero for anything unattempted.
| Challenge | Domain | Points | Your score |
|---|---|---|---|
| 1 | Container & Supply-Chain Hardening | 25 | |
| 2 | Vulnerability Management & Triage | 25 | |
| 3 | Static Analysis & Secrets Detection | 15 | |
| 4 | Infrastructure-as-Code Hardening | 15 | |
| 5 | Dynamic Analysis & Compliance Evidence | 20 | |
| Total | All five domains | 100 |
The bar is 80, matching the real CDP's own published pass mark — verify that figure is still current on the exam guide's own linked source before you plan around it. That leaves 20 points of slack, which is tighter than it sounds on a reweighted paper: dropping Challenge 1 or Challenge 2 outright costs a quarter of the whole paper, and either one alone can take you under the bar even with a clean sweep everywhere else. That's the direct consequence of the reweighting — it doesn't just test the two domains harder, it makes the whole score more sensitive to how you did in them.
| Domain | Available | Yours | If you scored under two-thirds, go here |
|---|---|---|---|
| Container & supply-chain hardening | 25 | Blueprint chapter 7, Container Runtime Security, capstone Part 4. | |
| Vulnerability management & triage | 25 | Blueprint chapter 9, DefectDojo, the dependency fire drill. | |
| Static analysis & secrets detection | 15 | Blueprint chapter 5, capstone Part 2. | |
| Infrastructure-as-code hardening | 15 | Blueprint chapter 7, capstone Part 5, the Terraform drill. | |
| Dynamic analysis & compliance evidence | 20 | Blueprint chapter 6, chapter 8, capstone Part 6. |
After the sitting
☺ Like you're 10: Don't re-practice everything — just the parts that lost you points, and spend the rest of your time getting faster, not learning new things.
Resist re-drilling the whole paper. The value of a scored, weighted sitting is that it tells you exactly where not to spend the next stretch of prep. Take your two lowest domain totals, re-read their linked pages above, then re-attempt only the specific challenges you flagged — not the whole set — a few days later, cold. If Challenge 1 or Challenge 2 cost you more than a quarter of the paper between them, that's the reweighting doing its job: it found the domain where "fine" isn't the same as "exam-ready" faster than an even split would have.
Once the numbers stop moving, Set 1 is worth a second look if you haven't sat it yet, and Set 3 is held in reserve for exactly this situation — a whole domain's worth of gaps that needs one more full-length rehearsal before you book the real thing. Know It Cold and the command & tool reference are the right next stop for pure speed, and the triage playbook is what to reach for the next time a challenge breaks in a way you can't immediately diagnose.
Benny the Beaver: 71. I nailed the container challenge — clean scan, signed, verified, SBOM and all. Felt great.
Timmy the Turtle: And the triage challenge?
Benny: …I called Finding D a real production Critical and burned twenty minutes writing a remediation plan for it.
Pip the Hummingbird: Which tag did you scan?
Benny: …the build stage. Not the one that actually ships.
Nutty the Squirrel: That's not a knowledge gap, that's a "verify what you're actually looking at before you act on it" gap. I file that one differently than the others.
Professor Owl: Which is exactly why this paper put twenty-five points there instead of twenty. Set 1 wouldn't have told you that habit was expensive — it would've been one wrong answer among many, evenly weighted away. Go re-drill the triage domain specifically, then come back for Set 3 when you're ready.
1. What changed between an even-split baseline paper and this one — the domains covered, or the points assigned to them? 2. What SLA does this course's canonical severity table assign to a High-severity finding, and what two factors can justify treating it as less urgent within that same 30-day window without changing its severity label? 3. Why is a worked solution folded behind a <details> element on this page instead of being written in plain text? 4. In Challenge 2, why does EPSS apply to Finding A and D but not to Finding B and C? 5. If a domain scored under two-thirds, what's the recommended next step — re-sit the whole paper, or something narrower?
Check your answers
- The five domains stayed identical to the baseline — what changed is the point allocation: 10 of the 100 points moved from static analysis/secrets and IaC hardening into container/supply-chain hardening and vulnerability-management triage, which now carry 25 points each instead of 20.
- 30 days. Low EPSS (a low predicted probability of exploitation) and limited reachability — the vulnerable path only being reachable from an internal, authenticated route rather than untrusted input — both argue for using the full window rather than treating the finding as same-day urgent, without changing the underlying High severity label itself.
- So a genuine, timed attempt isn't spoiled by seeing the answer first — reading a solution before honestly trying measures nothing about your own pacing or gaps, which is the entire point of sitting a mock under exam conditions.
- EPSS is a probability score tied to a specific CVE identifier, so it only exists for CVE-numbered SCA findings like A and D. Findings B and C are code-level SAST/DAST results with no CVE attached — their severity comes from the tool's own risk/confidence rating and confirmed exploitability, not a CVE-based prediction feed.
- Something narrower: re-read that domain's linked lesson and blueprint chapter, then re-attempt only the specific challenge(s) you flagged a few days later, cold — not a full re-sit of the whole paper, which mostly re-confirms what you already got right.