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

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.

☺ Explain it like I'm 10

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.

🦫🐢Your hosts for this topic: Benny the Beaver & Timmy the Turtle — Benny built the container that three of these five challenges live inside, and Timmy refuses to award a point for any fix whose done-when check hasn't actually re-run and passed.

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.

DomainEven-split baselineThis paper (Set 2)Delta
Container & supply-chain hardening2025+5
Vulnerability management & triage2025+5
Static analysis & secrets detection2015−5
Infrastructure-as-code hardening2015−5
Dynamic analysis & compliance evidence2020
Same 100 points, five challenges — reweighted, not relabeled Set 1 baseline — 20 pts each Set 2 — this paper 20 25 Container & supply-chain 20 25 Vuln mgmt & triage 20 15 Static analysis & secrets 20 15 IaC hardening 20 20 Dynamic & compliance 10 of the 100 points moved — into the two domains a written report leans on hardest.
◆ Key idea

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.

🐢 Timmy's pre-flight check · 10 min

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.

⚠ Numbers, IDs, and syntax drift — verify before you rely on them

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.

BlockChallengePointsBudgetRunning total
Read all five briefs10 min10
Challenge 1Container & Supply-Chain Hardening2580 min90
Challenge 2Vulnerability Management & Triage2575 min165
Challenge 3Static Analysis & Secrets Detection1545 min210
Challenge 4Infrastructure-as-Code Hardening1545 min255
Challenge 5Dynamic Analysis & Compliance Evidence2065 min320
Verify every done-when, capture evidence40 min360

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:

  1. Rewrite the Dockerfile as a multi-stage build: install dependencies without leaking NPM_TOKEN into any layer, then ship only the runtime artifact on a minimal, non-root base image.
  2. Scan the built runtime image with Trivy and get to zero HIGH/CRITICAL findings against the actual shipped tag.
  3. Generate a CycloneDX SBOM for the image with Syft.
  4. Sign the image keylessly with cosign and verify the signature.
  5. 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:

#SourceFindingSeverityEPSSKEV?
ATrivy (SCA, container scan)CVE-2024-3XXXX (illustrative) in form-data@3.0.0, transitive via an internal upload helperHigh (CVSS 8.1)0.09No
BSemgrep (SAST)Unescaped req.query.msg written into the response in routes/feedback.js — reflected XSS, CWE-79High
CZAP (DAST, same staging build)Reflected XSS alert on GET /feedback?msg=High
DTrivy (container scan)CVE-2024-5XXXX (illustrative), Critical, in an OS package — found on vulnerly:buildCritical (CVSS 9.8)0.31No

Your task:

  1. Identify which two findings are the same underlying root cause reported by two different tool types, and dedupe them into one ticket.
  2. 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.
  3. For Finding D, check which image tag was actually scanned before you treat it as a live production finding at all.
  4. 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
TicketSourcesDispositionSLA / priorityJustification
1B + C (deduped — C marked Duplicate under B)Active — fix nowHigh / 30-day band, but confirmed by two independent methods — don't wait for day 29SAST 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.
2AActive — scheduled, not urgentHigh / 30-day band, use the full windowCVSS 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.
3DNot Applicable — re-verify, don't celebrate a fixN/A until re-scanned correctlyThe 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:

  1. Fix the command injection in /resizefile or width containing shell metacharacters currently reaches a real shell.
  2. Write a custom Semgrep rule that catches child_process.exec() built from a template literal or string concatenation, without flagging safe execFile/spawn calls.
  3. 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.
  4. 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:

  1. Scan the plan with trivy config and Checkov, and list the findings against both resources.
  2. Fix the RDS instance: not publicly accessible, encrypted at rest.
  3. Fix the IAM policy: scope it to the specific actions and resources the app actually needs instead of a wildcard grant.
  4. 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:

AlertRiskConfidence
Missing Strict-Transport-Security and Content-Security-Policy headersLow/MediumHigh
Session cookie missing Secure and HttpOnly flagsMediumHigh
Reflected XSS on POST /profile/bio — the bio field is echoed unescaped on the next page loadHighMedium

Your task:

  1. Rank the three alerts by risk × confidence and state your fix order in one line.
  2. Fix all three: add security headers, correct the cookie flags, and escape the bio field's output.
  3. 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
end
inspec 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.

ChallengeDomainPointsYour score
1Container & Supply-Chain Hardening25
2Vulnerability Management & Triage25
3Static Analysis & Secrets Detection15
4Infrastructure-as-Code Hardening15
5Dynamic Analysis & Compliance Evidence20
TotalAll five domains100

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.

DomainAvailableYoursIf you scored under two-thirds, go here
Container & supply-chain hardening25Blueprint chapter 7, Container Runtime Security, capstone Part 4.
Vulnerability management & triage25Blueprint chapter 9, DefectDojo, the dependency fire drill.
Static analysis & secrets detection15Blueprint chapter 5, capstone Part 2.
Infrastructure-as-code hardening15Blueprint chapter 7, capstone Part 5, the Terraform drill.
Dynamic analysis & compliance evidence20Blueprint 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.

🎬 At the Shift-Left Squad
🦫

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.

✓ Checkpoint

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
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
📄 The mock exam sets

Set 1 · Set 2 (you are here) · Set 3 · Set 4. All four are scored out of 100 against the same 80-point bar; see the CDP study plan for when to sit each one, and the practice challenge bank for untimed, domain-by-domain drilling before you sit a full paper.