Mock Exam · Set 1 — five challenges, one live environment, six hours
This is the closest a static page can get to sitting the real Certified DevSecOps Professional exam: 5 challenges, 100 points, an 80-point bar, and one unbroken 6-hour block, all against a single shared fictional target — a small reporting SaaS this page calls Orbit — the way the real CDP hands you one live environment rather than five disconnected quizzes. Each challenge below gives you the exact vulnerable artifact (a manifest, a source file, a Terraform module, a host config) to reproduce yourself, a set of scored tasks with a done-when check, and a full worked solution folded behind a summary so you can grade honestly instead of guessing. This page only simulates the 6-hour challenge window — the separate 24-hour report window that follows it in the real exam is covered on the CDP exam page, not here. If you haven't built the underlying skill yet, start with the study plan and the exam blueprint first; this paper measures whether you can execute it under a clock, not whether you've read about it.
Most tests hand you a worksheet. This is more like being handed a small, slightly broken workshop and a toolbox, and told "you have six hours — find what's broken, fix it, and prove it's fixed." There are five stations in the workshop: the parts bin (are any of the parts you bought recalled?), the blueprints (did you draw a mistake into the plan?), the finished machine (does it actually break when someone pokes it?), the building itself (are the doors locked?), and the inspection binder (can you prove, on paper, that all four of those are true?). Knowing how to use every tool in the box gets you through the six hours. Writing down what you did the moment you do it is what keeps you from forgetting, an hour later, which station you already finished.
How this paper works, and where it sits in your prep
☺ Like you're 10: This is a full practice concert, not five separate practice songs — the same five skills you've drilled separately, now back to back, on a clock that doesn't wait for you.
Practical DevSecOps doesn't publish a percentage breakdown across its five live challenges — the CDP exam page already makes that point, and this paper takes it seriously rather than inventing a precision the vendor itself doesn't claim. So every challenge below is weighted the same: 20 points each, split across four 5-point tasks per challenge. The five challenges are Software Composition Analysis, Static Analysis & Secrets Detection, Dynamic Analysis (DAST), Infrastructure as Code Hardening, and Compliance as Code — the same five areas the exam page names as what the real challenges likely draw on. This is Set 1 of four; Sets 2, 3, and 4 are separate, unseen papers, held back on purpose so you have fresh material for later rehearsals instead of re-solving today's answers from memory.
| Stage | What you do | What it tells you |
|---|---|---|
| 1 · Build fluency | Work the 9-chapter blueprint and drill Know It Cold and the command & tool reference until the commands below aren't new to you. | Whether you know the material at all — untimed. |
| 2 · Sit Set 1 | This page. One 6-hour block, no pausing, the full 100-point paper. | Your pacing and evidence discipline under a clock — can you bank 80 points, not just eventually get there. |
| 3 · Fix gaps | Re-drill only the domains where you dropped points in the practice challenge bank and the matching capstone lab part. | Converts a diagnosis into targeted practice instead of a full re-read. |
| 4 · Sit Sets 2–4 | Fresh, unseen papers spaced out before your real exam date, each further from today's memory. | Your actual readiness on material you haven't already seen the answer to. |
Treat that split as this course's own study scaffolding, not a leaked answer key — an even split is the honest choice when the vendor doesn't publish real weights, not a claim that the real exam scores this way. What is transferable is the shape: multiple small, verifiable tasks inside each challenge, so a candidate who gets 80% of the way there on a hard one still banks partial credit instead of an all-or-nothing zero.
Before you start — build each target yourself
☺ Like you're 10: There's no download link for "the broken workshop" — you build the mess yourself, from the exact instructions in each challenge, so the fix you find is a fix you actually earned.
Unlike the real CDP, this page has no vendor-hosted environment behind it — it can't, being a static site. What it gives you instead is the exact vulnerable artifact for every challenge: a requirements.txt, a source file, a Terraform module, a host config. Recreate each one in a scratch directory as you reach it, so the finding you produce and the fix you apply are both real, not read. Confirm every tool below actually runs before you start the clock — a missing binary mid-challenge is time you don't get back:
# --- ninety seconds before the clock starts ---------------------------
trivy --version # or: dependency-check.sh --version -- SCA
semgrep --version && gitleaks version && trufflehog --version # SAST & secrets
docker run --rm zaproxy/zap-stable zap-baseline.py -h | head -1 # DAST
checkov --version && tfsec --version && conftest --version # IaC
inspec version && oscap --version # compliance-as-code
# anything that 404s here is a tool you install now, not mid-challenge
mkdir -p ~/orbit-mock1/{sca,sast,dast,iac,compliance} && cd ~/orbit-mock1The real CDP explicitly disallows AI assistants and chatbots during the challenge window, a rule the exam page covers in full. Enforce it on yourself here too, or this sitting measures the wrong thing. If a command's syntax isn't already a reflex, that's exactly the gap Know It Cold exists to close — note it, look it up after the clock stops, and drill it before Set 2.
Exam conditions for this sitting
☺ Like you're 10: Same rules as the real thing — one timer, no pausing, and no peeking at the worked solution until you're actually stuck or actually done.
One timer, started once. 6 hours. You don't pause it to install a missing tool, fix a typo'd Terraform block, or answer a phone call — losing ten minutes to your own laptop is exactly the kind of friction the real exam won't refund either. No peeking at a worked solution before you've either solved the task or burned its budget. Reading the answer first turns a diagnostic into a comprehension exercise — you'll feel productive and learn nothing about your own pacing, then repeat the same stall on exam day. Skim all 5 challenge briefs before touching a terminal — five minutes spent up front is the highest-return five minutes of the whole sitting, because it tells you which challenge you're already strong on and which one is going to eat the clock.
Each task below is budgeted individually, but the mechanical rule is the same everywhere: when a task passes roughly twice its listed budget without a passing done-when check, stop. Write one line about where you stalled, leave whatever you've applied in place — partial credit is real, and a half-finished fix beats a reverted one — and move to the next task. You are not paid to finish tasks in order; you are paid to bank points before the clock runs out.
Your time budget across the 6-hour window
☺ Like you're 10: Give every station in the workshop its own fair slice of the six hours, plus a little extra at the end just for double-checking your work.
Six hours is 360 minutes. Reading and orientation take 15, a verification-and-note-taking buffer at the end takes 45, and the remaining 300 split unevenly across the five challenges — DAST gets the largest share because authenticating a spider and manually confirming an IDOR genuinely takes longer than diffing a Terraform block, the same real-world asymmetry the exam page's own tactics section describes.
| Block | Budget | Running total | Note |
|---|---|---|---|
| Skim all 5 briefs | 15 min | 15 | Mark each challenge cheap / medium / expensive before you write a single command. |
| Challenge 1 — SCA | 55 min | 70 | Four 5-point tasks; reachability tracing is the part people rush. |
| Challenge 2 — SAST & secrets | 60 min | 130 | The git-history purge order (rotate, then purge) is worth re-reading before you touch it. |
| Challenge 3 — DAST | 70 min | 200 | The single most expensive challenge on the paper — authenticated spidering eats real minutes. |
| Challenge 4 — IaC hardening | 50 min | 250 | Cheapest challenge per point if you already know your Terraform and Rego syntax cold. |
| Challenge 5 — Compliance as code | 65 min | 315 | Two tools, one host — the InSpec/OpenSCAP reconciliation step is where time actually goes. |
| Verify, screenshot, and note | 45 min | 360 | Re-run every done-when check. This block earns more points per minute than anything else on the paper. |
Challenge 1 — Software Composition Analysis (20 points)
☺ Like you're 10: Before you touch a single line of Orbit's own code, check every part it borrowed from someone else against the recall list.
Scenario. Orbit's reporting-service is a small Flask app. Recreate its dependency manifest exactly as shown, and a config loader that reads an uploaded YAML file:
# requirements.txt
Flask==2.0.1
PyYAML==5.3.1
requests==2.25.1
setuptools==59.6.0
gunicorn==20.1.0# app/report_config.py
import yaml
def load_report_config(uploaded_file):
# Called from POST /reports/import — no auth check runs before this line.
return yaml.load(uploaded_file.read(), Loader=yaml.Loader)| # | Task | Done when | Points | Budget |
|---|---|---|---|---|
| 1.1 | Run an SCA scan against the manifest and identify the highest-severity finding. | You can name the CVE ID, CVSSv3 score, and affected package/version. | 5 | 10 min |
| 1.2 | Trace whether the vulnerable code path is actually reachable. | You can cite the exact source line and the entry point that reaches it. | 5 | 10 min |
| 1.3 | Remediate both the manifest and the code. | The scanner no longer reports the CVE, and the fix doesn't rely on the version bump alone. | 5 | 15 min |
| 1.4 | Re-scan clean and document a suppression for the remaining lower finding. | A suppression file exists with a package reference, a CVE ID, and a written justification. | 5 | 10 min |
Worked solution — Challenge 1
1.1 — Scan. OWASP Dependency-Check or Trivy's filesystem scanner both read the manifest without executing anything:
$ dependency-check.sh --project orbit-reporting --scan . --format JSON --out dc-report/
# or: trivy fs --scanners vuln --severity HIGH,CRITICAL .PyYAML:5.3.1
CVE-2020-14343 CVSSv3: 9.8 (CRITICAL) fixed in: >=5.4
→ yaml.load() without an explicit safe Loader permits arbitrary code execution
from a crafted YAML file.
setuptools:59.6.0
CVE-2022-40897 CVSSv3: 7.5 (HIGH) fixed in: >=65.5.1
→ ReDoS in package_index.py's HTML parsing.The highest-severity finding is PyYAML 5.3.1, CVE-2020-14343, CVSSv3 9.8.
1.2 — Reachability. A CVSS score alone doesn't tell you whether your code ever calls the vulnerable function — that's a separate question the scan output can't answer for you:
$ grep -rn "yaml.load" app/
app/report_config.py:6: return yaml.load(uploaded_file.read(), Loader=yaml.Loader)app/report_config.py:6 calls yaml.load() with Loader=yaml.Loader — the unsafe, full-featured loader — directly on bytes read from a file the caller uploaded, reached from POST /reports/import with no authentication check ahead of it. Verdict: reachable and exploitable, not a theoretical finding sitting in an unused code path.
1.3 — Remediate. The version bump alone isn't sufficient defense in depth — a future regression to Loader=yaml.Loader would reopen the same class of bug even on a patched PyYAML, so fix the manifest and the call site:
# requirements.txt
-PyYAML==5.3.1
+PyYAML==6.0.1# app/report_config.py
- return yaml.load(uploaded_file.read(), Loader=yaml.Loader)
+ return yaml.safe_load(uploaded_file.read())1.4 — Suppress the rest, with a reason. Re-scanning clean on the critical finding still leaves the setuptools ReDoS. It's genuinely lower priority here: package_index.py's HTML parsing only runs during package resolution at build time, and Orbit's production image never invokes easy_install or resolves packages at runtime — but "lower priority" is a documented decision, not silence:
<!-- suppression.xml -->
<suppress>
<notes>CVE-2022-40897 — ReDoS in setuptools' package_index.py HTML parsing.
Build-time-only code path; never invoked at runtime in the production image.
Accepted risk, ticket ORBIT-482, review by next quarterly dependency sweep.</notes>
<packageUrl regex="true">^pkg:pypi/setuptools@.*$</packageUrl>
<cve>CVE-2022-40897</cve>
</suppress>$ dependency-check.sh --project orbit-reporting --scan . --suppression suppression.xml --failOnCVSS 9
# exits 0 — the critical is fixed, the high is suppressed with a reason on fileMore depth: Software Composition Analysis in Depth, the OWASP Dependency-Check tool page, and hands-on reps in Part 3 of the capstone and the dependency fire drill.
Challenge 2 — Static Analysis & Secrets Detection (20 points)
☺ Like you're 10: Read Orbit's own code the way a proofreader would, and check whether anyone ever left a key lying around in the drafts.
Scenario. Recreate Orbit's billing module:
# app/billing.py
def get_invoice(customer_id):
query = "SELECT * FROM invoices WHERE customer_id = '" + customer_id + "'"
return db.execute(query).fetchall()Then simulate a leaked credential: commit a file, leak a fake-but-realistic AWS key inside it, then remove it in a later commit — which is exactly the mistake that leaves it recoverable from git history:
$ git init orbit-billing && cd orbit-billing
$ printf 'AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE\nAWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n' > deploy/staging.env
$ git add -A && git commit -m "add staging deploy env"
$ git rm deploy/staging.env && git commit -m "remove staging env from repo"
# the key is gone from the working tree, but it's still sitting in the commit six lines up| # | Task | Done when | Points | Budget |
|---|---|---|---|---|
| 2.1 | Run a SAST scan and identify the SQL-injection finding. | You can name the file, line, and rule that flagged it. | 5 | 10 min |
| 2.2 | Scan the full git history — not just the working tree — for leaked secrets. | You've found the AWS key and stated whether it's Verified or Unverified. | 5 | 10 min |
| 2.3 | Fix the injection and purge the credential from history, in the correct order. | The query is parameterized, and the key is gone from every commit — after being rotated, not before. | 5 | 20 min |
| 2.4 | Wire both scanners in as merge-blocking CI checks. | A CI config exists where either scanner failing fails the pipeline. | 5 | 10 min |
Worked solution — Challenge 2
2.1 — SAST. Semgrep's auto config catches string-built SQL immediately:
$ semgrep --config=auto app/billing.pyapp/billing.py:3
python.lang.security.audit.formatted-sql-query.formatted-sql-query
Detected possible SQL injection using string concatenation. Use parameter
binding instead.2.2 — Full-history secrets scan. A diff-only scan never looks past the current tree — the whole point of this task is that the key was already deleted from it:
$ trufflehog git file://. --only-verifiedFound verified result 🐷🔑
Detector Type: AWS
Verified: true
Commit: a1b2c3d "add staging deploy env"
File: deploy/staging.env
Raw: AKIAIOSFODNN7EXAMPLETruffleHog's Verified: true comes from a live, read-only call — sts:GetCallerIdentity for an AWS key — so this isn't "a string shaped like a key," it's confirmation a real credential still authenticates. (Gitleaks would find the same commit faster on a working-tree diff scan, but doesn't verify liveness by default — the two tools are complementary, not redundant, exactly as chapter 5 covers.)
2.3 — Fix, in order. Parameterize the query first:
- query = "SELECT * FROM invoices WHERE customer_id = '" + customer_id + "'"
- return db.execute(query).fetchall()
+ return db.execute(
+ "SELECT * FROM invoices WHERE customer_id = %s", (customer_id,)
+ ).fetchall()Then the credential. Rotate before you purge — the git history rewrite doesn't retroactively invalidate a key that's already been cloned, forked, or scraped, so a purge with no rotation just hides the leak instead of closing it:
# 1. rotate first — the key stops working immediately, regardless of git history
$ aws iam update-access-key --access-key-id AKIAIOSFODNN7EXAMPLE --status Inactive
$ aws iam delete-access-key --access-key-id AKIAIOSFODNN7EXAMPLE
$ aws iam create-access-key --user-name orbit-staging-deploy
# 2. purge second — now the history rewrite is cleanup, not the only safeguard
$ git filter-repo --path deploy/staging.env --invert-paths
# coordinate a force-push and tell every clone to re-fetch — a rewritten history
# doesn't self-propagate to clones nobody warned2.4 — Gate the pipeline.
# .github/workflows/security.yml
jobs:
sast-and-secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 } # secrets scan needs full history, not a shallow clone
- run: semgrep ci --config=auto --error
- uses: gitleaks/gitleaks-action@v2More depth: Static Analysis & Secrets Detection, Semgrep, gitleaks, TruffleHog, hands-on in Part 2 of the capstone and the leaked credential triage drill.
Challenge 3 — Dynamic Analysis / DAST (20 points)
☺ Like you're 10: Stop reading the blueprints — go attack the finished, running building the way a real intruder would, from the outside, with no floor plan.
Scenario. Run Orbit's app locally (any small Flask/Node app with a login form and an /api/invoices/<id> route reproduces this well enough) at http://localhost:5000, and give it exactly one bug on purpose: the invoices endpoint never checks that the requested invoice actually belongs to the logged-in session.
# app/api.py — the bug: no ownership check before returning the record
@app.get("/api/invoices/<int:invoice_id>")
@login_required
def get_invoice_api(invoice_id):
invoice = Invoice.query.get_or_404(invoice_id)
return jsonify(invoice.to_dict()) # returns it regardless of whose session this is| # | Task | Done when | Points | Budget |
|---|---|---|---|---|
| 3.1 | Run an unauthenticated ZAP baseline scan against the target. | You can name the highest-risk alert and its confidence level. | 5 | 10 min |
| 3.2 | Configure an authentication context and re-spider. | You can state the route count before and after auth, and why they differ. | 5 | 20 min |
| 3.3 | Run an authenticated active scan and manually confirm an IDOR with curl. | A curl request as customer A returns customer B's invoice data. | 5 | 20 min |
| 3.4 | Fix the authorization check and re-verify. | The same curl request now returns 403, and a re-scan shows it closed. | 5 | 15 min |
Worked solution — Challenge 3
3.1 — Baseline. Passive only — it inspects traffic the spider generates, it never attacks:
$ docker run --rm -v $(pwd):/zap/wrk/:rw -t zaproxy/zap-stable zap-baseline.py \
-t http://localhost:5000 -r zap-baseline-report.html -IUnauthenticated, the spider reaches only the login page and a health-check route, and baseline reports something like Medium risk / High confidence: Content Security Policy Header Not Set. That's real, but it's not the interesting bug on this target — the spider never got past the login form to find it.
3.2 — Authenticate the spider. ZAP's Automation Framework wires a login context in as YAML instead of hand-clicking a wizard:
env:
contexts:
- name: "orbit"
urls: ["http://localhost:5000"]
authentication:
method: "form"
parameters:
loginPageUrl: "http://localhost:5000/login"
loginRequestUrl: "http://localhost:5000/login"
loginRequestBody: "email={%username%}&password={%password%}"
verification:
method: "response"
loggedInRegex: "\\QDashboard\\E"
loggedOutRegex: "\\QPlease log in\\E"
users:
- name: "customer-a"
credentials: { username: "a@orbit.test", password: "$ORBIT_TEST_PW" }
jobs:
- type: spider
parameters: { context: "orbit", user: "customer-a" }
- type: activeScan
parameters: { context: "orbit", user: "customer-a" }Unauthenticated, the spider found 2 routes. With the context wired in, it finds 14 — the dashboard, the invoices list, the invoice detail API, an account settings page — because a link-only crawler that never logs in can't discover anything sitting behind the login wall. That gap is exactly why an unauthenticated baseline "passing" proves almost nothing about an app that's mostly behind auth.
3.3 — Confirm the IDOR by hand. An active scan flags the invoices endpoint as a possible access-control issue; a scanner alert alone isn't proof, so verify it manually as two different customers:
$ curl -s -H "Cookie: session=$CUSTOMER_A_SESSION" \
http://localhost:5000/api/invoices/1042 | jq '.customer_id'
1041
# customer A's own session, requesting invoice 1042 — which belongs to customer 1041, not AThe endpoint returns any invoice for any authenticated session, regardless of who owns it. Confirmed IDOR — real, reproducible, not a tool false positive.
3.4 — Fix and re-verify.
@app.get("/api/invoices/<int:invoice_id>")
@login_required
def get_invoice_api(invoice_id):
invoice = Invoice.query.get_or_404(invoice_id)
if invoice.customer_id != current_user.customer_id:
abort(403)
return jsonify(invoice.to_dict())$ curl -s -o /dev/null -w "%{http_code}\n" -H "Cookie: session=$CUSTOMER_A_SESSION" \
http://localhost:5000/api/invoices/1042
403More depth: Dynamic Analysis in Practice, OWASP ZAP, Burp Suite, hands-on in Part 6 of the capstone.
Challenge 4 — Infrastructure as Code Hardening (20 points)
☺ Like you're 10: Catch the mistake in the building's blueprint before the building actually gets built — a misconfigured plan is cheaper to fix than a misconfigured bucket that's already public.
Scenario. Recreate Orbit's storage-and-network module:
# infra/main.tf
resource "aws_s3_bucket" "reports" {
bucket = "orbit-customer-reports"
acl = "public-read"
}
resource "aws_security_group" "app" {
name = "orbit-app-sg"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}| # | Task | Done when | Points | Budget |
|---|---|---|---|---|
| 4.1 | Run Checkov or tfsec against the module and list the failing checks. | You can name at least three failing check IDs and what each one means. | 5 | 10 min |
| 4.2 | Fix the S3 bucket. | Public access is blocked, encryption and versioning are on, and the scanner passes on this resource. | 5 | 15 min |
| 4.3 | Fix the security group. | Port 22 is no longer open to 0.0.0.0/0. | 5 | 10 min |
| 4.4 | Write a policy that fails the plan if this regresses. | An OPA/Conftest policy fails against the original plan and passes against the fixed one. | 5 | 15 min |
Worked solution — Challenge 4
4.1 — Scan.
$ checkov -d infra/ --framework terraformCheck: CKV_AWS_20 "S3 Bucket has an ACL defined which allows public READ access"
FAILED for resource: aws_s3_bucket.reports
Check: CKV_AWS_19 "Ensure the S3 bucket has server-side encryption enabled"
FAILED for resource: aws_s3_bucket.reports
Check: CKV_AWS_21 "Ensure the S3 bucket has versioning enabled"
FAILED for resource: aws_s3_bucket.reports
Check: CKV_AWS_24 "Ensure no security groups allow ingress from 0.0.0.0/0 to port 22"
FAILED for resource: aws_security_group.app4.2 — Fix the bucket. A private ACL alone isn't the full fix — pair it with the account-level public-access block, plus encryption and versioning:
resource "aws_s3_bucket" "reports" {
bucket = "orbit-customer-reports"
}
resource "aws_s3_bucket_public_access_block" "reports" {
bucket = aws_s3_bucket.reports.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_server_side_encryption_configuration" "reports" {
bucket = aws_s3_bucket.reports.id
rule {
apply_server_side_encryption_by_default { sse_algorithm = "aws:kms" }
}
}
resource "aws_s3_bucket_versioning" "reports" {
bucket = aws_s3_bucket.reports.id
versioning_configuration { status = "Enabled" }
}4.3 — Fix the security group. Scope SSH to a known range, or drop direct SSH entirely in favor of SSM Session Manager — this challenge takes the scoped-CIDR route:
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
- cidr_blocks = ["0.0.0.0/0"]
+ cidr_blocks = ["203.0.113.0/24"] # the office/VPN egress range, not the whole internet
}4.4 — Prevent the regression. A one-time fix doesn't stop the next engineer from reintroducing it — write the rule as policy, evaluated against the plan itself:
# policy/ssh_exposure.rego
package terraform.security
deny[msg] {
some i
input.resource_changes[i].type == "aws_security_group"
rule := input.resource_changes[i].change.after.ingress[_]
rule.from_port <= 22
rule.to_port >= 22
rule.cidr_blocks[_] == "0.0.0.0/0"
msg := sprintf("%s opens SSH to the whole internet", [input.resource_changes[i].address])
}$ terraform show -json tfplan.binary > plan.json
$ conftest test plan.json -p policy/
FAIL - plan.json - terraform.security - aws_security_group.app opens SSH to the whole internet
# ...fix applied, re-plan, re-test...
$ conftest test plan.json -p policy/
0 tests, 0 failures, 0 exceptionsMore depth: Infrastructure as Code Hardening, IaC security & policy as code, Checkov, tfsec, OPA & Conftest, hands-on in Part 5 of the capstone and the broken Terraform drill and the policy-as-code drill.
Challenge 5 — Compliance as Code (20 points)
☺ Like you're 10: Handing an auditor "trust me" isn't proof. Handing them a checklist that a computer ran, with a timestamp, is.
Scenario. Orbit's staging host needs two CIS Ubuntu controls proven and, right now, fails both: SSH allows root login, and password aging isn't enforced.
| # | Task | Done when | Points | Budget |
|---|---|---|---|---|
| 5.1 | Run an InSpec profile against the host and identify both failing controls. | You can name both failing control IDs and what each checks. | 5 | 15 min |
| 5.2 | Fix both controls on the host and re-run to a clean pass. | InSpec reports 0 failures. | 5 | 15 min |
| 5.3 | Cross-check with OpenSCAP and reconcile any discrepancy. | You can state any control OpenSCAP flags that the InSpec profile didn't cover, and why. | 5 | 15 min |
| 5.4 | Produce an audit-ready evidence bundle. | A combined control-mapping table exists, suitable to attach to the CDP's 24-hour report. | 5 | 15 min |
Worked solution — Challenge 5
5.1 — Run InSpec.
$ inspec exec dev-sec/linux-baseline -t ssh://deploy@staging-host \
--reporter cli json:results/staging-host.json × sshd-01: SSH root login should be disabled
× PermitRootLogin is expected to cmp == "no"
expected: "no"
got: "yes"
× os-05: Password maximum age should be 90 days or less
× PASS_MAX_DAYS is expected to cmp <= 90
expected: <= 90
got: 99999
Profile Summary: 39 successful controls, 2 control failures, 0 controls skipped5.2 — Fix and re-verify.
$ ssh deploy@staging-host 'sudo sed -i "s/^#\?PermitRootLogin.*/PermitRootLogin no/" /etc/ssh/sshd_config && sudo systemctl reload sshd'
$ ssh deploy@staging-host 'sudo sed -i "s/^PASS_MAX_DAYS.*/PASS_MAX_DAYS 90/" /etc/login.defs'
$ inspec exec dev-sec/linux-baseline -t ssh://deploy@staging-host --reporter cli
Profile Summary: 41 successful controls, 0 control failures, 0 controls skipped5.3 — Reconcile against OpenSCAP. InSpec and OpenSCAP rarely cover the exact same control set — that gap is the point of running both, not a bug in either:
$ oscap xccdf eval \
--profile xccdf_org.ssgproject.content_profile_cis_level1_server \
--results results.xml --report report.html --results-arf arf.xml \
/usr/share/xml/scap/ssg/content/ssg-ubuntu2204-ds.xmlThe two failing controls both come back pass, matching InSpec's re-run — but OpenSCAP's CIS profile additionally flags auditd_enabled as fail, a control the dev-sec/linux-baseline profile in use here simply doesn't include. That's the reconciliation: note the gap explicitly (ticket ORBIT-511, add an auditd control or layer in a broader InSpec profile) rather than treating a clean InSpec run as proof of a clean host.
5.4 — Evidence bundle. Normalize both tools' output into one common format with MITRE's SAF CLI, then summarize:
$ saf convert inspec2hdf -i results/staging-host.json -o hdf/inspec.json
$ saf convert oscap2hdf -i arf.xml -o hdf/oscap.json
$ saf validate threshold -i hdf/inspec.json -F threshold.yml # gates the pipeline on the result| Control | CIS section | Tool | Result |
|---|---|---|---|
| sshd-01 — root login disabled | CIS Ubuntu 22.04 §5.2.10 | InSpec | Pass (fixed) |
| os-05 — password max age ≤ 90d | CIS Ubuntu 22.04 §5.5.1.2 | InSpec | Pass (fixed) |
| auditd_enabled | CIS Ubuntu 22.04 §4.1.1.1 | OpenSCAP | Fail — tracked as ORBIT-511, out of this sitting's scope |
That table, plus the two JSON/ARF artifacts it summarizes, is exactly the shape of evidence the CDP's real 24-hour report window expects — a claim ("it's fixed") backed by a machine-generated result, not a screenshot of a terminal someone trusts you didn't edit.
More depth: Compliance as Code at Scale, InSpec, OpenSCAP, hands-on in Part 7 of the capstone.
Score yourself
☺ Like you're 10: Add up your points, compare to the 80 line, then look at which questions cost you — that second part is the part that actually helps.
Mark after a short break, not immediately — adrenaline makes for generous self-grading. Award full points only when the done-when check in the table above genuinely passed, half when the artifact exists and is broadly right but the check didn't pass clean, and zero for anything unattempted. "I knew how to do that one" earns nothing that a passing check doesn't already prove.
| # | Task | Points | Your score |
|---|---|---|---|
| 1.1 | SCA — identify the highest-severity finding | 5 | |
| 1.2 | SCA — reachability trace | 5 | |
| 1.3 | SCA — remediate manifest + code | 5 | |
| 1.4 | SCA — documented suppression | 5 | |
| 2.1 | SAST — identify the SQL-injection finding | 5 | |
| 2.2 | Secrets — full-history scan, verified | 5 | |
| 2.3 | SAST/secrets — fix in the correct order | 5 | |
| 2.4 | SAST/secrets — merge-blocking CI gate | 5 | |
| 3.1 | DAST — unauthenticated baseline | 5 | |
| 3.2 | DAST — authenticated re-spider | 5 | |
| 3.3 | DAST — manually confirm the IDOR | 5 | |
| 3.4 | DAST — fix and re-verify | 5 | |
| 4.1 | IaC — enumerate failing checks | 5 | |
| 4.2 | IaC — fix the S3 bucket | 5 | |
| 4.3 | IaC — fix the security group | 5 | |
| 4.4 | IaC — write the regression policy | 5 | |
| 5.1 | Compliance — identify both failing controls | 5 | |
| 5.2 | Compliance — fix and re-verify | 5 | |
| 5.3 | Compliance — reconcile against OpenSCAP | 5 | |
| 5.4 | Compliance — evidence bundle | 5 | |
| Total | All 5 challenges | 100 |
Computing your result. The points total 100, so your raw score is your percentage, and the bar is 80. That leaves 20 points spendable — a full challenge's worth. Then do the more useful arithmetic and total each challenge separately, because an 80 built from four clean challenges and one zero is a genuinely worse outcome than an 80 spread evenly, even though the number is identical — the first candidate has a hard blind spot, the second has an even, shallow gap.
| Challenge | Available | Yours | If you scored under 15, go here |
|---|---|---|---|
| Software Composition Analysis | 20 | Chapter 4, then the practice bank's SCA set and the dependency fire drill. | |
| Static Analysis & Secrets Detection | 20 | Chapter 5, then the leaked credential drill. | |
| Dynamic Analysis (DAST) | 20 | Chapter 6, then Part 6 of the capstone. | |
| Infrastructure as Code Hardening | 20 | Chapter 7 and IaC security & policy as code, then the broken Terraform drill. | |
| Compliance as Code | 20 | Chapter 8, then Part 7 of the capstone. |
What each miss means
☺ Like you're 10: There are three different reasons to lose a point, and each one needs a different fix — guessing the wrong one wastes a week of studying the wrong thing.
Sort your zeros and halves into three piles before you decide what to re-study. A task you didn't recognize at all is a genuine knowledge gap — go read the chapter linked above, then redo that exact task cold in two or three days. A task where you knew the shape but burned the clock hunting for flag syntax or a field name is a speed problem, not a knowledge one, and the cure is Know It Cold and the command & tool reference, not another read of the chapter. A task that broke in a way you couldn't diagnose — a scan that errored, a tool that returned nothing — is a triage gap, and the triage playbook is built for exactly that. And one thing sits outside all three: count how many tasks you left completely untouched. More than three or four, and the honest diagnosis is pacing, not knowledge — you never got to spend points you actually had.
You've now seen every artifact and every fix on this paper. A strong score on Set 1 tells you that you can execute this specific material under a clock — genuinely useful, but not the same claim as "I'm ready for the real exam." Set 2 is unseen for exactly this reason; save it, and the two after it, for later in your schedule rather than sitting all four back to back this week.
Foxy: 76. Missed the pass bar by four points, and every single one of them came from Challenge 3.
Timmy: How long did you actually spend on the auth context?
Foxy: ...forty minutes. I kept rewriting the login regex instead of moving on and coming back.
Benny: Same trap I fall into on Challenge 4 — I get the S3 fix working and I'm so relieved I skip straight past writing the Rego policy. Four points, gone, for something that takes ten minutes.
Nutty: And did you screenshot the curl output the moment 403 came back, Foxy, or are you about to try to remember it an hour from now?
Foxy: ...I'll go do that right now, actually.
Professor Owl: Forty minutes on one auth context is exactly what the flag-and-move rule is for. Twenty spendable points means you can afford to lose a whole challenge and still clear the bar — but only if you let go of the one that's fighting you and go collect the four challenges that aren't.
After this sitting
☺ Like you're 10: Only re-practice the parts you actually got wrong, and save the next full concert for a couple of weeks from now.
Resist re-drilling everything — the entire value of a scored paper is that it tells you where not to spend your next week. Take your one or two weakest challenges, work only those sections of the practice challenge bank and the matching capstone lab part, and leave the challenges you cleared alone — re-solving something you already proved you can do is comfort, not preparation. When you're ready for the next fresh sitting, move to Set 2; leave Set 3 and Set 4 sealed until closer to your real exam date. For the logistics of the actual day — registration, the two-clock structure, and what the report window expects — the CDP exam page covers all of it.
1. How many challenges does this paper have, how many points is each worth, and why are they weighted evenly rather than to some published breakdown? 2. What is the flag-and-move threshold on this paper, and what do you do with the work you abandon? 3. In Challenge 2, why does the credential have to be rotated before the git history is purged, not after? 4. Why does a clean InSpec run in Challenge 5 not, by itself, prove the host is fully compliant? 5. You score 80 exactly, but all 20 missed points came from one challenge — what does that tell you that a score of 80 spread evenly across all five wouldn't?
Check your answers
- 5 challenges, 20 points each, 100 total. Even weighting is used because Practical DevSecOps doesn't publish a real percentage breakdown for its five live challenges — inventing uneven weights would imply a precision this course doesn't actually have.
- Roughly twice a task's listed budget without a passing done-when check. Write one line noting where you stalled, leave the partial work in place — partial credit is real — and move to the next task.
- Because purging git history doesn't invalidate a credential that may already have been cloned, forked, or scraped before the rewrite — rotating first makes the key stop working immediately regardless of what happens to the history; purging second is cleanup, not the only safeguard.
- Because InSpec and OpenSCAP rarely cover the identical control set — in the worked solution, OpenSCAP's CIS profile flags an
auditdcontrol the InSpec profile in use simply didn't include. A clean run only proves the host passes the specific controls that profile checked, not every control that could matter. - That you have one concentrated blind spot rather than a shallow, even gap. Two candidates can post the identical 80, but the one who lost every point in a single challenge has a specific, fixable hole — and, on the real exam, a task draw that leans even slightly harder into that same domain is genuinely riskier for them than for a candidate with the identical score spread evenly.
That's the paper. Build each artifact yourself, start one timer, and let the five challenges above take you through a six hours that should feel noticeably less comfortable than working through the blueprint chapters untimed. Score it honestly, note your weakest challenge, and spend the next stretch of your schedule exactly there — then come back for Set 2 when you're ready, or head to the course home to pick up wherever the blueprint left off.