Mock Exam · Set 3 — fix it, prove it, write the control
Five challenges, one target application, one 4-hour clock, 100 points, an 80-point pass mark — the same shape as the CDP itself and the same shape as the other sittings in this run. What's different about Set 3 is the rubric underneath each challenge. Where an earlier sitting mostly asked "did you fix it," every task here is graded in three even pieces: fix the finding (10 pts), prove it with a re-scan (5 pts), and write the exact compliance control the fix satisfies (5 pts). Half the paper's points, every single time, live in the half most candidates skip when they're racing a clock — which is exactly the half the CDP's real 24-hour report window is graded on. Work all five against Anchorpoint, a small fictional payments API built for this sitting, and by the end you'll have produced something closer to a real audit packet than a pile of green checkmarks.
Fixing a leaky faucet and being able to prove to your landlord that it's actually fixed are two different skills. One is a wrench. The other is a photo of the dry floor, dated, with a note about which pipe it was and why it won't leak again. This test doesn't just hand you five leaky faucets — it takes away half your points if you fix all five and hand back an empty photo album.
Why this sitting grades the report, not just the fix
☺ Like you're 10: Knowing how to fix something and being able to write down, clearly, what you fixed and why it counts are two separate skills — and the second one is the one people practice the least.
The CDP exam guide already says this plainly: the exam's score comes from a 6-hour challenge window and a separate 24-hour report window, and "I changed the config" is not the same claim as "I changed the config, and here is the scan showing it worked." Candidates who ace the challenge window still lose points in the report window constantly, for a boring reason — under six hours of pressure, writing things down is the first habit to slip, and by the time the report is due, half the specifics are gone. Set 3 makes that failure mode visible during practice instead of on exam day, by refusing to award full credit for a fix nobody can audit. Every challenge below is worth 20 points split three ways, and the last five points are unreachable unless you can name a real control, in a real framework, that your fix actually satisfies.
Use this exact shape for every "map the control" task below — it's short enough to fill in without breaking your pace, and it's close to what a real audit evidence entry looks like:
### Control: <short name> - **Control ID / framework:** <ID> (<framework>, <version>) - **Control statement:** <what the control requires, one sentence> - **Finding:** <what was wrong, one sentence> - **Remediation:** <what changed — commit, config, command> - **Evidence:** <the exact command + output, or a link to the CI run> - **Residual risk:** <anything this fix doesn't fully cover> - **Owner / review date:** <who owns it, when it's re-checked>
A control write-up that skips "Evidence" is an opinion. A control write-up that skips "Residual risk" is marketing. Both cost you the same two points on this rubric that they'd cost you on a real audit.
PCI DSS renumbered heavily going from v3.2.1 to v4.0. NIST SP 800-53 renumbered and re-organized going from Rev 4 to Rev 5. ISO/IEC 27001's Annex A was substantially restructured in the 2022 edition. Every control ID cited on this page is written against the current major version of its framework at the time this page was written, and every one of them is a real, citable control — but treat the exact wording and numbering the way this course treats DSOMM levels and Checkov check IDs elsewhere: confirm against the framework's own current published text before it goes into a real audit packet. Knowing the right control family cold is worth more than memorizing a number that might have shifted.
Meet Anchorpoint — the target for today's sitting
☺ Like you're 10: One small pretend company's app, with five real mistakes hidden in it — a different app from the capstone's Vulnerly, built just for this sitting so you're seeing it cold.
Anchorpoint is a small Node.js/Express payments-reconciliation API backed by Postgres, deployed to AWS via Terraform, containerized, and fronted by an EJS-rendered invoice search page — shaped like the kind of internal fintech tool this course keeps coming back to, but a fresh scenario built specifically for this paper, not the capstone's Vulnerly. Five seeded weaknesses map one-to-one onto the five challenges below, in the same domain spread the exam guide already argues the real CDP draws its five challenges from:
| # | Domain | What's broken | Points |
|---|---|---|---|
| C1 | Static analysis & secrets | String-built SQL query + a live Stripe key committed to history | 20 |
| C2 | Software composition analysis | A transitive lodash prototype-pollution CVE, three hops deep | 20 |
| C3 | Dynamic analysis | Reflected XSS on the invoice search page, staging deploy | 20 |
| C4 | Infrastructure as code | A public, unencrypted RDS instance and an unencrypted S3 bucket | 20 |
| C5 | Triage & compliance evidence | Everything above, plus two new findings, assembled into one report | 20 |
Pre-flight — stand Anchorpoint up before the clock starts
☺ Like you're 10: Get every tool open and working the night before, so the first minute of the real timer isn't spent waiting for something to install.
Anchorpoint's layout for this sitting:
anchorpoint/ ├── src/ │ ├── routes/invoices.js # C1 — SQL injection │ ├── config/stripe.js # C1 — hardcoded key (already deleted, still in history) │ ├── views/search.ejs # C3 — unescaped output │ └── package.json # C2 — transitive lodash CVE ├── infra/main.tf # C4 — public, unencrypted RDS + S3 └── k8s/deploy.yaml # staging Deployment + Service for C3
Before the timer starts, confirm each of these independently — a tool that isn't actually installed shouldn't cost you minutes mid-challenge:
semgrep --version && trufflehog --version && gitleaks version npm --version # 8.3+, for the overrides field in C2 syft version && grype version docker run --rm zaproxy/zap-stable zap.sh -version checkov --version && conftest --version kind create cluster --name cdp-mock3 kubectl apply -f k8s/deploy.yaml && kubectl -n anchorpoint rollout status deploy/anchorpoint
Challenges 1, 2, and 4 are static — source, dependency manifest, and Terraform plan — and need no running target. Challenge 3 is the one exception: ZAP has to reach a live, deployed Anchorpoint on your kind cluster before an active scan finds anything. Deploy it during pre-flight, not when you reach C3, or you'll burn C3's 40-minute budget waiting on a rollout.
Exam conditions for this sitting
☺ Like you're 10: Practice it the way you'll actually be tested — one clock, no pausing, and only the references you'd really have on the day.
One timer, 4 hours, started once. That's shorter than the CDP's own 6-hour challenge window on purpose — this sitting compresses five self-contained challenges into a tighter block precisely because the "map the control" step is folded into every task instead of deferred to a separate 24-hour window, so there's less raw challenge-solving time to fill and more report-writing reps packed into the same clock. No chatbots, no AI assistants, and no other pages of this course open — the real CDP bars AI outright, and this course's own pages are exactly the kind of look-up the exam won't let you have either. Official docs only: the docs for the specific tool a task names (Semgrep, TruffleHog, npm, Syft/Grype, OWASP ZAP, Checkov, Conftest) and the framework pages you'd cite (nist.gov, pcisecuritystandards.org, iso.org) are fair game; nothing else.
The flag-and-move rule, extended for three-part tasks. If the fix itself is fighting you past its budget, don't abandon the whole task — leave whatever you applied in place, note where you stalled, and still attempt the compliance-control write-up for the fix you intended to make. That's not a loophole; it's realistic. On the real exam, a report written honestly about a partial fix still earns partial credit, while a task abandoned with nothing written down earns none. Verifying is still mandatory where it's possible — never write "prove" text for a re-scan you didn't actually run.
Your time budget
☺ Like you're 10: Ten minutes to get set, forty minutes per puzzle, and half an hour at the very end to staple all five write-ups into one real report.
| Block | Challenge | Budget | Running total | Note |
|---|---|---|---|---|
| Read & pre-flight check | — | 10 min | 10 | Confirm every tool from pre-flight actually runs; deploy Anchorpoint to staging now, not at C3. |
| Challenge 1 | SAST & secrets | 40 min | 50 | ~20 fix / ~10 prove / ~10 map — the split every challenge follows. |
| Challenge 2 | SCA | 40 min | 90 | The transitive-dependency trap is the time sink; don't let it eat the map step. |
| Challenge 3 | DAST | 40 min | 130 | The ZAP automation run itself takes real wall-clock time — kick it off early in the block. |
| Challenge 4 | IaC hardening | 40 min | 170 | Writing the regression-guard Conftest rule is part of "fix," not extra credit. |
| Challenge 5 | Triage & consolidated report | 40 min | 210 | Two new findings plus everything from C1–C4, ranked and written up in one pass. |
| Final assembly | — | 30 min | 240 | Compile all five control write-ups into one submission file — this block is worth rehearsing on its own. |
The paper — 5 challenges, each graded on fix, proof, and control
☺ Like you're 10: Here are the five puzzles. Each one tells you what's broken, what you have to do about it, and exactly what "done" looks like — including the write-up.
Challenge 1 — Static analysis & secrets: the search endpoint and the key in history
Anchorpoint's invoice search route builds SQL by string concatenation, and three commits back, someone briefly committed a live-looking Stripe secret key before "fixing" it in the very next commit by deleting the line.
Given — src/routes/invoices.js:
router.get('/invoices/search', async (req, res) => {
const q = req.query.customer;
const [results] = await sequelize.query(
`SELECT id, amount, status FROM invoices WHERE customer_name = '${q}'`
);
res.json(results);
});Given — src/config/stripe.js (three commits back, later deleted from the working tree but not from history):
module.exports = {
secretKey: "sk_live_51NxJ2KFh1c3RhY2tPYXltZW50c19rZXk",
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET
};Your task:
- Fix (10 pts). Run Semgrep against the route, confirm the SQL injection, and fix it with a bound parameter. Separately, treat the Stripe key as a confirmed compromise — rotate it, wire the new key in from an environment variable or a secrets manager, and add a pre-commit hook so a new fake key can't be committed going forward.
- Prove (5 pts). Re-run Semgrep on the fixed route and show zero SQL-injection findings. Show the rotated key is actually dead — a live API call authenticated with the old key must fail.
- Map the control (5 pts). Using Nutty's template above, write one control entry for the injection fix and one for the credential-rotation fix, each citing a real control ID from a recognized framework.
Done when: Semgrep reports zero SQL-injection findings on the fixed route; the old Stripe key returns an authentication error when called directly; a staged fake key is rejected by the pre-commit hook before it's committed; and both control write-ups name a real, citable control ID with evidence that actually exists.
Show the worked solution
const { QueryTypes } = require('sequelize');
router.get('/invoices/search', async (req, res) => {
const q = req.query.customer;
const results = await sequelize.query(
'SELECT id, amount, status FROM invoices WHERE customer_name = :customer',
{ replacements: { customer: q }, type: QueryTypes.SELECT }
);
res.json(results);
});# 1. SAST
semgrep --config p/javascript --config p/sql-injection src/routes/invoices.js
# ✓ 0 findings
# 2. confirm the historical secret and rotate it
trufflehog git file://. --only-verified
# Detector: Stripe Verified: true Commit: a1b2c3d (3 commits back)
# → rotate the key in the Stripe dashboard immediately, THEN prove it's dead:
curl -s -u sk_live_51NxJ2KFh1c3RhY2tPYXltZW50c19rZXk: https://api.stripe.com/v1/charges
# {"error":{"message":"Invalid API Key provided: sk_live_***", "type":"invalid_request_error"}}
# 3. pre-commit gate against a repeat
cat >> .pre-commit-config.yaml <<'EOF'
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks
EOF
pre-commit install### Control: Input validation on the invoice search route - **Control ID / framework:** SI-10 (NIST SP 800-53 Rev 5); equivalent under PCI DSS v4.0 Req 6.2.4 and ISO/IEC 27001:2022 Annex A.8.28 (Secure Coding) - **Control statement:** The system validates and safely handles all externally-supplied input to prevent injection attacks. - **Finding:** /invoices/search built its SQL query by string interpolation, allowing arbitrary SQL via the `customer` query parameter. - **Remediation:** Replaced string interpolation with a Sequelize bound parameter (commit a1b2c3d). - **Evidence:** `semgrep --config p/sql-injection` — 0 findings on the fixed route (run attached). - **Residual risk:** Ruleset covers first-order SQLi only; second-order injection via stored, later-reused input is tracked separately. - **Owner / review date:** AppSec — re-verified on every PR via the required `sast` status check. ### Control: Credential rotation and secure storage - **Control ID / framework:** IA-5 (NIST SP 800-53 Rev 5); equivalent under PCI DSS v4.0 Req 3.6/8.3 and ISO/IEC 27001:2022 Annex A.8.24 (Use of Cryptography) - **Control statement:** Authenticators are not stored in cleartext in source and are rotated immediately upon suspected exposure. - **Finding:** A live Stripe secret key was committed to `src/config/stripe.js` and remained in reachable git history after deletion from the working tree. - **Remediation:** Key rotated in the Stripe dashboard; new key sourced from an environment variable at runtime; gitleaks wired into pre-commit. - **Evidence:** Direct API call with the old key returns `invalid_request_error` (attached); pre-commit hook rejects a staged fake key (attached). - **Residual risk:** Key was live in reachable history for an unknown window before this rotation — logged as a historical-exposure item, not closed by rotation alone. - **Owner / review date:** AppSec / Platform — incident record filed.
Why: deleting a secret from the working tree doesn't undo its exposure — anything pushed to a shared remote, even briefly, has to be treated as compromised regardless of whether HEAD still shows it, which is exactly why the "prove" step here isn't "the file is gone," it's "the old key no longer authenticates against the live API." The two control write-ups are deliberately separate entries, not one merged paragraph — an injection fix and a credential-rotation fix satisfy different control families, and an auditor reading one entry that tries to cover both will reasonably ask which control actually applies.
Background: Static Analysis & Secrets Detection. Tools: Semgrep, TruffleHog, gitleaks, HashiCorp Vault. More reps: the leaked-credential triage drill.
Challenge 2 — Software composition analysis: a prototype-pollution CVE, three hops deep
Anchorpoint's own package.json never names lodash directly. It pulls in an internal utility package, @anchorpoint/config-loader, which pins an old lodash transitively — and nobody has looked at the resolved dependency tree since the project started.
Given — fragment of package.json:
{
"dependencies": {
"@anchorpoint/config-loader": "1.4.0",
"express": "^4.18.2"
}
}Your task:
- Fix (10 pts). Run an SCA scan against the resolved lockfile, identify the CVE, its CVSS score, its EPSS score, and whether it's in CISA's KEV catalog. Pin the transitive
lodashto a fixed version using npm'soverridesfield — without bumpingconfig-loaderitself, since a major-version bump there is out of scope for this fix. - Prove (5 pts). Regenerate the SBOM and re-scan; show zero criticals or highs for
lodash. - Map the control (5 pts). Write a control entry for vulnerability remediation, citing the SLA your fix actually met.
Done when: npm ls lodash shows a single resolved copy at a fixed version; a re-run SCA scan shows zero critical/high findings for lodash; and the control write-up states a real SLA framework, not just "we fixed it."
Show the worked solution
trivy fs --scanners vuln --severity CRITICAL,HIGH,MEDIUM . # lodash 4.17.15 (via @anchorpoint/config-loader) # CVE-2020-8203 Prototype Pollution HIGH CVSS 7.4 EPSS ~0.02 Not KEV-listed # fixed in: 4.17.19
{
"overrides": {
"@anchorpoint/config-loader": {
"lodash": "^4.17.21"
}
}
}npm install npm ls lodash # anchorpoint@1.0.0 # └─┬ @anchorpoint/config-loader@1.4.0 # └── lodash@4.17.21 overridden syft dir:. -o cyclonedx-json=sbom.json grype sbom:sbom.json # 0 Critical, 0 High for lodash
### Control: Timely remediation of known vulnerabilities - **Control ID / framework:** RA-5 / SI-2 (NIST SP 800-53 Rev 5); equivalent under PCI DSS v4.0 Req 6.3.3 and ISO/IEC 27001:2022 Annex A.8.8 (Management of Technical Vulnerabilities) - **Control statement:** Known vulnerabilities in software are identified through regular scanning and remediated within a risk-based timeframe. - **Finding:** CVE-2020-8203 (lodash prototype pollution, CVSS 7.4) reachable transitively via `@anchorpoint/config-loader@1.4.0`. Not KEV-listed, EPSS ~2% — real but not under active mass exploitation. - **Remediation:** Pinned the transitive `lodash` resolution to `^4.17.21` via npm `overrides`, without a major-version bump to `config-loader`. - **Evidence:** `npm ls lodash` resolves to a single 4.17.21 copy; Grype re-scan against a regenerated SBOM shows 0 Critical/High for `lodash`. - **Residual risk:** `config-loader` itself remains on its current major version; a future upstream release may reintroduce a different transitive pin, so this override is re-checked on every dependency bump. - **Owner / review date:** Platform — SLA met same-day for a HIGH, non-KEV, fix-available finding, per the vulnerability-management runbook.
Why: the trap is the same shape as a real Log4Shell-style transitive finding — the direct dependency in package.json never names the vulnerable package, so a scan against package.json alone (rather than the resolved lockfile) misses it entirely. overrides forces the resolved version without requiring a coordinated upstream release from config-loader's maintainers, which matters because bumping a transitive dependency's owner package to a new major version risks breaking changes the fix has nothing to do with. EPSS and KEV status earn their place in the control write-up, not just the fix — a HIGH-CVSS, non-KEV, low-EPSS finding still needed same-day action here because a fix was cheap and available, but the write-up's SLA justification is different from what it would be for something actively exploited in the wild.
Background: Software Composition Analysis in Depth, Software Bills of Materials, Dependency & License Risk Management. Tools: Trivy, Syft & Grype, Snyk. More reps: the vulnerable-dependency fire drill.
Challenge 3 — Dynamic analysis: reflected XSS on the invoice search page
Anchorpoint's staging deploy renders search results with EJS. Somewhere along the way, the "no results" message switched from EJS's escaping tag to its raw, unescaped one — probably copied from a spot elsewhere in the template that legitimately needed to render trusted HTML.
Given — src/views/search.ejs:
<p>No invoices found for "<%- q %>"</p>
Your task:
- Fix (10 pts). Configure an authenticated ZAP Automation Framework run against the staging deploy, confirm the reflected XSS on
/invoices/search?q=, and manually replay the payload in a browser to confirm it actually executes — not just that the scanner flagged it. Fix by switching to EJS's escaping tag, and add aContent-Security-Policyheader as defense in depth. - Prove (5 pts). Re-run the same ZAP automation job against the fixed deploy and show the XSS alert is gone.
- Map the control (5 pts). Write a control entry for output encoding, and note the CSP header as a second, complementary control rather than folding it into the same entry.
Done when: the ZAP automation run's spider report shows the login-gated pages were actually reached (proving auth worked); the pre-fix report shows the reflected XSS alert; the post-fix report doesn't; and the browser replay is documented as evidence, not just asserted.
Show the worked solution
# zap-automation.yaml
env:
contexts:
- name: anchorpoint
urls: ["http://anchorpoint.staging.svc:8080"]
authentication:
method: form
parameters:
loginPageUrl: http://anchorpoint.staging.svc:8080/login
loginRequestUrl: http://anchorpoint.staging.svc:8080/api/auth/login
loginRequestBody: 'email={%username%}&password={%password%}'
users:
- name: tester
credentials: { username: tester@example.com, password: "Testpass1!" }
jobs:
- type: spider
parameters: { context: anchorpoint, user: tester, url: http://anchorpoint.staging.svc:8080 }
- type: activeScan
parameters: { context: anchorpoint, user: tester }
- type: report
parameters: { template: traditional-html, reportFile: zap-report.html }docker run -v $(pwd):/zap/wrk/:rw -t zaproxy/zap-stable \ zap.sh -cmd -autorun /zap/wrk/zap-automation.yaml # pre-fix report: 1 High — Cross Site Scripting (Reflected), /invoices/search # manual replay curl 'http://anchorpoint.staging.svc:8080/invoices/search?q=%3Cscript%3Ealert(1)%3C/script%3E' # response body contains the literal, unescaped <script> tag
<!-- fix: escaped output, EJS's default tag --> <p>No invoices found for "<%= q %>"</p> <!-- plus a CSP header set at the Express app level -->
# re-run the same automation job against the fixed deploy docker run -v $(pwd):/zap/wrk/:rw -t zaproxy/zap-stable \ zap.sh -cmd -autorun /zap/wrk/zap-automation.yaml # post-fix report: 0 High, 0 Medium XSS-related alerts
### Control: Output encoding on user-controlled content - **Control ID / framework:** SI-10 (NIST SP 800-53 Rev 5); equivalent under OWASP ASVS 5.3.3 (Output Encoding) and ISO/IEC 27001:2022 Annex A.8.28 - **Control statement:** All user-controlled data rendered into HTML responses is contextually encoded before output. - **Finding:** search.ejs used EJS's unescaped `<%- %>` tag to render the `q` query parameter, producing reflected XSS on /invoices/search. - **Remediation:** Switched to EJS's default escaping tag `<%= %>` (commit d4e5f6a). - **Evidence:** ZAP automation report pre-fix shows 1 High XSS alert; identical run post-fix shows 0; manual payload replay confirms no script execution in the browser. - **Residual risk:** Escaping covers this route; a template-wide audit for other `<%- %>` usages is tracked as a follow-up, not yet complete. - **Owner / review date:** AppSec — re-verified by the staging DAST job on every deploy. ### Control: Content-Security-Policy as defense in depth - **Control ID / framework:** SC-18 (NIST SP 800-53 Rev 5); equivalent under ISO/IEC 27001:2022 Annex A.8.28 and SOC 2 CC8.1 (Change Management, for the header being introduced as a tracked configuration change) - **Control statement:** A restrictive Content-Security-Policy limits the impact of any output-encoding gap that escapes review. - **Finding:** No CSP header was set anywhere in the response chain. - **Remediation:** Added a CSP header via Express middleware, deployed alongside the encoding fix. - **Evidence:** `curl -I` against staging shows the header present. - **Residual risk:** CSP is defense in depth, not a substitute for the encoding fix above — treated as a second, independent control. - **Owner / review date:** Platform — reviewed each time a new script or style source is added to the app.
Why: the exam-relevant trap is authentication, not the payload — a ZAP scan that never gets past a login wall reports false confidence, which is why the done-when explicitly checks that the spider actually reached login-gated pages before trusting the "no findings" result on either run. The two control entries stay separate for the same reason C1's did: encoding and CSP are genuinely different controls with different evidence and different owners of "does this actually work," and merging them into one entry hides that CSP alone would never have caught this specific bug.
Background: Dynamic Analysis in Practice, API Security in Depth. Tools: OWASP ZAP, Burp Suite.
Challenge 4 — Infrastructure as code: a public, unencrypted database
Anchorpoint's Terraform provisions its Postgres instance and an S3 bucket for generated invoice PDFs. Nobody's run a scanner against the plan yet.
Given — fragment of infra/main.tf:
resource "aws_db_instance" "anchorpoint" {
identifier = "anchorpoint-db"
engine = "postgres"
instance_class = "db.t3.medium"
allocated_storage = 50
publicly_accessible = true
username = "app_admin"
password = var.db_password
}
resource "aws_s3_bucket" "invoice_pdfs" {
bucket = "anchorpoint-invoice-pdfs"
}Your task:
- Fix (10 pts). Run Checkov against the plan and list the failing check IDs. Fix all three: disable public accessibility on the RDS instance, enable storage encryption on it, and enable default server-side encryption on the S3 bucket. Then write an OPA/Conftest policy that fails
terraform planin future if anyaws_db_instancesetspublicly_accessible = true, so this exact class of finding can't regress silently. - Prove (5 pts). Re-run Checkov against the fixed file and show all three checks passing. Run Conftest against a deliberately-reintroduced public RDS instance in a test plan and show your policy blocking it.
- Map the control (5 pts). Write one control entry covering both the encryption fixes and one covering the network-exposure fix.
Done when: Checkov shows zero failures for these two resources on the fixed file; Conftest fails a plan containing a public RDS instance with your policy's message; and both control write-ups exist with real evidence attached.
Show the worked solution
checkov -d infra/ --compact # CKV_AWS_17 aws_db_instance.anchorpoint — publicly accessible # CKV_AWS_16 aws_db_instance.anchorpoint — storage not encrypted # CKV_AWS_19 aws_s3_bucket.invoice_pdfs — bucket not encrypted
resource "aws_kms_key" "anchorpoint" {
description = "Anchorpoint data-at-rest encryption"
}
resource "aws_db_instance" "anchorpoint" {
identifier = "anchorpoint-db"
engine = "postgres"
instance_class = "db.t3.medium"
allocated_storage = 50
publicly_accessible = false
storage_encrypted = true
kms_key_id = aws_kms_key.anchorpoint.arn
username = "app_admin"
password = var.db_password
}
resource "aws_s3_bucket" "invoice_pdfs" {
bucket = "anchorpoint-invoice-pdfs"
}
resource "aws_s3_bucket_server_side_encryption_configuration" "invoice_pdfs" {
bucket = aws_s3_bucket.invoice_pdfs.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.anchorpoint.arn
}
}
}# policy/rds_public.rego
package terraform.rds
deny contains msg if {
rc := input.resource_changes[_]
rc.type == "aws_db_instance"
rc.change.after.publicly_accessible == true
msg := sprintf("%s must not set publicly_accessible = true", [rc.address])
}terraform plan -out tfplan && terraform show -json tfplan > plan.json checkov -d infra/ --compact # 0 failures for these two resources conftest test --policy policy/ plan.json # PASS on the fixed plan # regression check — reintroduce the finding in a test plan and confirm the gate holds conftest test --policy policy/ test-fixtures/public-rds-plan.json # FAIL - aws_db_instance.anchorpoint must not set publicly_accessible = true
### Control: Encryption of data at rest - **Control ID / framework:** SC-28 (NIST SP 800-53 Rev 5); equivalent under PCI DSS v4.0 Req 3.5/3.6 and ISO/IEC 27001:2022 Annex A.8.24 - **Control statement:** Data at rest in production databases and object storage is encrypted using a managed key. - **Finding:** aws_db_instance.anchorpoint had no `storage_encrypted` attribute (defaults false); aws_s3_bucket.invoice_pdfs had no SSE configuration at all. - **Remediation:** Added `storage_encrypted = true` with a KMS key on the RDS instance; added an `aws_s3_bucket_server_side_encryption_configuration` resource on the bucket. - **Evidence:** Checkov re-run — CKV_AWS_16 and CKV_AWS_19 both PASS on the fixed plan (attached). - **Residual risk:** KMS key rotation policy is the account default; a dedicated rotation schedule for this key is a follow-up item. - **Owner / review date:** Platform — verified on every `terraform plan` via the required Checkov CI job. ### Control: Restriction of database network exposure - **Control ID / framework:** SC-7 (NIST SP 800-53 Rev 5); equivalent under PCI DSS v4.0 Req 1.3 and CIS AWS Foundations Benchmark's networking recommendations (exact sub-control numbering has shifted across benchmark versions — verify against the version pinned in your compliance tooling) - **Control statement:** Databases holding production data are not directly reachable from the public internet. - **Finding:** aws_db_instance.anchorpoint set `publicly_accessible = true`. - **Remediation:** Set `publicly_accessible = false`; added a Conftest policy blocking any future plan that reintroduces the setting. - **Evidence:** Checkov CKV_AWS_17 PASSES on the fixed plan; Conftest FAILS a deliberately-reintroduced public-RDS test fixture (both attached). - **Residual risk:** None identified for this specific resource; the policy covers `aws_db_instance` only — a future `aws_rds_cluster` resource would need its own rule. - **Owner / review date:** Platform — this policy is a required CI gate on every Terraform PR going forward.
Why: both findings share the same root cause as every AWS resource in this position — the less-secure option is the default unless a companion resource or attribute explicitly overrides it, so "I created the database" is never the same claim as "I created the database and turned off public access." The Conftest policy is the part that turns a one-time cleanup into a permanent guarantee; without it, the exact same mistake ships again the next time someone copies this resource block as a starting point.
Background: Infrastructure as Code Hardening. Tools: Checkov, tfsec, OPA & Conftest. More reps: the broken-Terraform-plan drill.
Challenge 5 — Triage & compliance evidence: assemble today's report
End of sitting. DefectDojo's rollup for today pulls together everything from Challenges 1–4, plus two findings that only show up once every scan for the day is aggregated in one place. This challenge is scored a little differently — it's the report-writing finale, not another isolated fix — so its 20 points split as triage & SLA decisions (8 pts), compensating-control reasoning for the two new findings (6 pts), and consolidated report completeness (6 pts).
| Source | Finding | CVSS | EPSS | KEV? | Status |
|---|---|---|---|---|---|
| SAST | SQLi, /invoices/search (C1) | 9.1 | — | No | Fixed this sitting |
| Secrets | Stripe key in history (C1) | — | — | No | Rotated this sitting |
| SCA | lodash prototype pollution (C2) | 7.4 | 0.02 | No | Fixed this sitting |
| DAST | Reflected XSS, search page (C3) | 6.1 | 0.01 | No | Fixed this sitting |
| IaC | Public, unencrypted RDS + S3 (C4) | 8.6 | — | No | Fixed this sitting |
| Container | Base-image CVE, no vendor patch yet | 7.5 | 0.04 | No | New — unfixed |
| DAST | Missing X-Content-Type-Options header | 2.6 | 0.01 | No | New — unfixed |
Your task:
- Triage & SLA (8 pts). For the two new findings, decide a remediation path and an SLA. The base-image CVE has no vendor fix yet — don't leave it as an indefinite "risk accepted."
- Compensating-control reasoning (6 pts). For the base-image CVE specifically, propose a compensating control that reduces risk today, with a named trigger for when it gets retired (the vendor patch shipping).
- Consolidated report (6 pts). Assemble one document — the format the CDP itself accepts: PDF, Markdown, or screenshots as evidence — containing every control write-up from C1–C4 plus the two new entries from this challenge, in Nutty's template shape, ready to submit.
Done when: the two new findings each have a stated SLA and, for the one with no fix, a compensating control with a named retirement trigger; and the consolidated report file exists with all seven entries present and none of them missing an Evidence line.
Show the worked solution
| Finding | Decision | SLA |
|---|---|---|
| Base-image CVE, no fix | Compensating control now; monitor for vendor patch | Compensating control live within 48h; re-check weekly for a fix |
Missing X-Content-Type-Options | Fix — one header, cheap, batch with next deploy | Next sprint |
Compensating control for the base-image CVE: a WAF rule blocking the specific exploit pattern the CVE describes, deployed within 48 hours, plus a weekly automated re-scan of the base image so the compensating control is retired the moment a patched image is available — not left in place indefinitely as a substitute for the real fix.
### Control: Compensating control for an unpatched base-image CVE - **Control ID / framework:** RA-5 / SI-2 (NIST SP 800-53 Rev 5); equivalent under PCI DSS v4.0 Req 6.3.3 (which explicitly permits a documented compensating control when no vendor patch exists) - **Control statement:** Where a known vulnerability has no available patch, a documented compensating control reduces risk until a fix ships. - **Finding:** Base image carries a CVSS 7.5 finding with no upstream patch as of this scan. - **Remediation:** WAF rule blocking the specific exploit pattern, deployed within 48 hours; weekly automated re-scan to detect a vendor patch. - **Evidence:** WAF rule ID and deploy timestamp; scheduled re-scan job definition. - **Residual risk:** The compensating control mitigates the known exploit pattern only, not the underlying flaw — explicitly tracked as temporary. - **Owner / review date:** Platform — reviewed weekly until superseded by a real patch, then retired.
Why: the report is the deliverable this challenge actually grades, not any single fix — which is exactly the shape of the real CDP's own report window, where the score comes from what the write-up proves across everything done that day, not from any one task in isolation. Deduplication and prioritization matter here for the same reason they did in the practice challenge bank's C8: an unscrubbed, unranked pile of findings looks worse than it is and costs credibility with whoever's reading the report. The compensating-control entry is the one most candidates skip entirely — "no fix available" is not the same as "nothing to write down."
Background: Vulnerability Management & Triage, Compliance as Code at Scale. Tools: DefectDojo, InSpec, OpenSCAP.
Score yourself
☺ Like you're 10: Add up your points honestly, then look at which column you actually lost points in — that tells you exactly what kind of practice you need next.
Mark after a break, the same rule every sitting on this site uses — grading your own work while still adrenalized produces generous nonsense. Award full credit only when the done-when check actually passed, and be strict about the "map the control" column specifically: a write-up that names no real, citable control ID earns zero on that line regardless of how well the fix itself worked.
| # | Challenge | Fix | Prove | Map the control | Your score |
|---|---|---|---|---|---|
| C1 | SAST & secrets | 10 | 5 | 5 | |
| C2 | SCA | 10 | 5 | 5 | |
| C3 | DAST | 10 | 5 | 5 | |
| C4 | IaC hardening | 10 | 5 | 5 | |
| C5 | Triage & report† | 8 | 6 | 6 | |
| Total | 100 | ||||
† C5's three columns measure triage/SLA, compensating-control reasoning, and report completeness respectively — not fix/prove/map in the literal sense, but the same total weight.
80/100 is the pass mark, matching the CDP's own published passing score — verify that figure is still current on Practical DevSecOps' own exam page before you rely on it for real. What matters more than the total is which column you lost points in, because the three columns are diagnosing three different things:
| Column you lost | What it actually means | Go here |
|---|---|---|
| Fix | A genuine technical gap — you didn't recognize the vulnerability class or couldn't produce the correct remediation | The blueprint chapter linked under each challenge, then Know It Cold for the configs themselves |
| Prove | You applied a fix but never actually re-ran the check — or you eyeballed the output instead of reading it carefully | The triage playbook and the specific tool page for the scanner in question |
| Map the control | The fix and the proof were both right, but the write-up named no real control ID, skipped Evidence, or asserted "compliant" without anything to back it | Compliance as Code at Scale, then re-drill using Nutty's template until it's automatic |
A "Map the control" score under 60% across all five challenges is not a knowledge gap in the traditional sense — you already proved you know the fix, since the Fix and Prove columns say so. It's a rehearsal gap in exactly the skill the real CDP's 24-hour report window measures, and it's the single most fixable weak spot a candidate can have, because it needs no new technical skill at all — just the habit of writing the template down every single time, starting now instead of on exam day.
Benny the Beaver: Ninety-two out of a hundred! All five fixes verified, every re-scan clean. New record.
Timmy the Turtle: Let me see the control write-ups.
Benny: I mean — I know which controls they'd map to. In my head.
Nutty the Squirrel: "In my head" isn't a document an auditor can read, Benny. Show me the Evidence line on the RDS fix.
Benny: ...there isn't one. I just wrote "fixed, encrypted now."
Timmy: That's zero on the Map column, five times over. Your real score is sixty-two.
Benny: Sixty-two?! But the fixes were all correct.
Professor Owl: And on the real exam, correct fixes with no report earn the same thing — very little. The report window is worth as much as the challenge window. Go back through all five, Benny, and fill in Nutty's template properly this time. The wrench was never the hard part.
After the sitting
☺ Like you're 10: Don't redo everything — just redo the exact part that cost you points, and keep the next sitting sealed until you're ready for it.
Take your weakest column across all five challenges and drill only that. A weak Fix column sends you back to the relevant blueprint chapter and the command & tool reference; a weak Prove column means slowing down on verification specifically, which the triage playbook is built for; a weak Map column means running Nutty's template as its own five-minute drill, independent of any fix, until naming a real control ID under time pressure stops feeling effortful. Whichever it is, re-sit only the challenges you scored under 16/20 on, not the whole paper — the rest already proved itself.
When you're ready for the next rehearsal, move on to whichever of the other three sittings you haven't sat yet, and keep at least one of them sealed as a genuinely cold measurement close to your real exam date, the same discipline the study plan recommends for the blueprint as a whole.
1. Why does every challenge on this paper split its 20 points evenly across fixing, proving, and mapping the control, instead of weighting the fix the heaviest? 2. Name two things Nutty's control-write-up template requires that a plain "I fixed it" sentence doesn't. 3. In Challenge 2, why does the fix use npm's overrides field instead of bumping @anchorpoint/config-loader to a newer major version directly? 4. In Challenge 5, why do the two new findings get different treatment — one fixed outright, one given a compensating control — instead of both being fixed immediately? 5. What is the pass mark for this sitting, and does it match the CDP's own published passing score? 6. A challenge's fix and its re-scan were both correct, but the control write-up cited no real framework or control ID. What should that score, and why?
Check your answers
- Because that's roughly the actual weight of the real CDP's own scoring — the exam's score comes from a 6-hour challenge window and a separate 24-hour report window, and a fix nobody can prove or audit doesn't fully count on either. Splitting every challenge's points this way turns that abstract fact into something you feel on every single task.
- An Evidence line (the actual command/output/link proving the fix works, not an assertion that it does) and a Residual risk line (what the fix doesn't fully cover) — both are easy to skip under time pressure and both are exactly what a real audit entry needs that a bare "fixed it" sentence lacks.
- Because
config-loader's maintainers own its major-version releases, and bumping it to a new major version risks breaking changes unrelated to the CVE. npm'soverridesfield forces the resolvedlodashversion precisely, the same way the practice bank's Java example used Maven'sdependencyManagementto pin a transitive dependency without touching its owning artifact. - Because they're genuinely different situations: the missing security header is cheap and has a fix available now, so it just gets scheduled. The base-image CVE has no vendor patch yet — fixing it "immediately" isn't actually possible, so the correct move is a compensating control with a named retirement trigger (the patch shipping), not an indefinite, undocumented "risk accepted."
- 80 out of 100, matching the CDP's own published pass mark — and per the standing caveat on this course's exam-prep pages, confirm that figure is still current on Practical DevSecOps' own exam page before relying on it for real.
- Zero on the "map the control" column, even though the fix and the proof both earned full credit — the whole point of this sitting's rubric is that a correct, verified fix with no real, citable control mapping fails the exact test the real exam's report window applies.
That's the sitting — five challenges, three graded parts each, one consolidated report at the end. Set the 4-hour timer, close everything except the tool docs, and let Anchorpoint's five seeded weaknesses take you through a paper that rewards writing things down as much as it rewards fixing them. Score it honestly, note your weakest column, and go back to the practice challenge bank for more untimed reps, or on to the course home to pick a lesson.