chapter 03
Gates in Action
Every block below is real, captured directly from the pipeline running in the public repository. None of the screenshots here are illustrative.
Gate 1 β Gitleaks
A test PR (#1) added a file with a fake AWS credential, in the format recognized by the aws-access-token rule. The first attempt used the most famous example on the internet β AKIAIOSFODNN7EXAMPLE β and sailed right through: that string is on Gitleaks' own internal allowlist, because it shows up in thousands of official AWS tutorials. Swapping in a different fake key, the block actually happened:
In PR #2 (gate 2, below), a hardcoded password "supersecret123" sailed cleanly through Gitleaks β low entropy, no known provider pattern β but got caught by SonarCloud. It's practical proof that the two gates cover different things, not the same thing twice.
Gate 2 β SonarCloud
PR #2 introduced a login.js file with classic problems, on purpose:
const DB_PASSWORD = "supersecret123";
function buildQuery(userId) {
return "SELECT * FROM users WHERE id = " + userId;
}
function runCommand(input) {
return eval(input);
}
Hardcoded credential, SQL built by string concatenation, eval() on unvalidated input, and zero test coverage on any of it. The Quality Gate failed:
Gate 3 β OWASP ZAP
The pre-deploy-dast.yml workflow spins up Juice Shop in a container on the Actions runner itself, waits for it to respond, and runs ZAP's baseline scan against http://localhost:3000:
- name: Start Juice Shop (staging surrogate)
run: docker run -d --name juice-shop -p 3000:3000 bkimminich/juice-shop
- name: ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.15.0
with:
target: 'http://localhost:3000'
rules_file_name: 'zap/zap-baseline-rules.conf'
fail_action: true
Real result, against the real target: 59 rules passed, 8 had a real finding.
| Rule | Finding |
|---|---|
| 10038 | Content Security Policy (CSP) Header Not Set |
| 10049 | Non-Storable Content |
| 10063 | Deprecated Feature Policy Header Set |
| 10096 | Timestamp Disclosure - Unix |
| 10098 | Cross-Domain Misconfiguration |
| 10109 | Modern Web Application |
| 10110 | Dangerous JS Functions |
| 90004 | Cross-Origin-Embedder-Policy Header Missing or Invalid |
Validating from the terminal, no browser needed
Every result above can also be confirmed straight from the command line, without relying on GitHub's UI:
RUN_ID=$(gh run list --workflow=pre-deploy-dast.yml --limit 1 --json databaseId --jq '.[0].databaseId')
gh run watch $RUN_ID # required: a still-running run has no log available yet
JOB_ID=$(gh run view $RUN_ID --json jobs --jq '.jobs[0].databaseId')
gh run view $RUN_ID --log --job=$JOB_ID | grep -iE "WARN-NEW|FAIL-NEW|PASS:|Total of"
Each piece solves a specific problem: gh run list --json databaseId --jq grabs the run's numeric ID without opening a browser; gh run watch is necessary because asking for the log of a still-running job just returns "logs will be available when it is complete"; gh run view --json jobs resolves the specific job ID inside the run (a run can have more than one job); and the final grep -iE isolates just ZAP's own summary lines from the full, verbose log.