From PDF Policy to Pull Request Guardrails: Secure Coding That Ships
Stop writing policy PDFs nobody reads. Wire your standards into editors, commits, and CI so developers get fast feedback and auditors get proof.
> Secure coding standards don’t live in PDFs. They live in editors, commits, and pipelines—and they leave a paper trail auditors can verify.Back to all posts
The moment policy meets the pull request
If your secure coding standards live in a Confluence PDF, you don’t have standards—you have a wish list. I’ve watched three fintechs fail SOC 2 the same way: gorgeous policies, zero guardrails in the repo. Developers ship what the CI lets them ship.
The fix: turn sentences into checks. Wire them into editors, pre-commit hooks, and CI. Give auditors automated proofs. Keep pipelines fast by gating riskier changes harder than the rest.
This is how we do it at GitPlumbers when we’re called to rescue a team buried under vibe-coded features and last-minute compliance panic.
Translate policy into checks developers actually feel
Start local. If the first time a developer hears about a rule is a failed CI job 10 minutes into a build, they’ll hate it—and you’ll get bypass culture.
- IDE plugins: Semgrep, ESLint, and Bandit extensions for VS Code/JetBrains. Fast, actionable, near the code.
- Pre-commit hooks: consistent, cross-editor, and quick. Use
pre-commitorlefthook. - Paved-path templates: repo scaffolds with guardrails baked in.
A minimal .pre-commit-config.yaml we ship on day one:
repos:
- repo: https://github.com/returntocorp/semgrep
rev: v1.85.0
hooks:
- id: semgrep
args: ["--config", "p/ci", "--error", "--timeout", "3"]
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
- repo: https://github.com/PyCQA/bandit
rev: 1.7.8
hooks:
- id: bandit
args: ["-q", "-ll"]
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v9.13.0
hooks:
- id: eslint
additional_dependencies: ["eslint@9.13.0"]A concrete policy-to-check example: “No PII in logs.” We express that as a Semgrep rule that flags log calls with PII-looking fields.
rules:
- id: no-pii-in-logs
message: "Do not log PII (email, ssn, dob). Use structured redaction."
languages: [python, javascript, typescript]
severity: ERROR
patterns:
- pattern-either:
- pattern: logger.$METHOD(..., $ARG, ...)
- pattern: console.$METHOD($ARG)
metavariable-regex:
ARG: ".*(email|ssn|social|dob|dateOfBirth|customer_ssn).*"Yes, it’s heuristic. That’s fine for local warnings; we tune for signal before we block.
CI gates that don’t murder delivery speed
Make the fast path fast. Run changed-file scans and cache aggressively. Reserve heavy scans for nightly or on-release.
- Changed-file scope:
semgrep --enable-metrics --error --exclude tests --timeout 30 --filter-paths. - Caching: GitHub Actions
actions/cache, GitLabcache:key: filesfor dependencies and scan databases. - Parallelism: split by tool and target; fail fast.
- Risk-based gating: stricter checks when code touches regulated data paths.
Example GitHub Actions workflow with risk-aware gates:
name: ci-security
on: [pull_request]
jobs:
classify:
runs-on: ubuntu-latest
outputs:
pii_touched: ${{ steps.touch.outputs.pii }}
steps:
- uses: actions/checkout@v4
- id: touch
shell: bash
run: |
if git diff --name-only origin/main... | grep -E 'src/(payments|billing|users)/'; then
echo "pii=true" >> $GITHUB_OUTPUT
else
echo "pii=false" >> $GITHUB_OUTPUT
fi
sast:
needs: classify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.11' }
- name: Semgrep changed-only
if: needs.classify.outputs.pii_touched == 'false'
run: semgrep ci --config p/ci --changed-since=origin/main
- name: Semgrep full (PII paths)
if: needs.classify.outputs.pii_touched == 'true'
run: semgrep ci --config p/strict
secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: detect-secrets scan --all-files --exclude-files '.*test.*' --baseline .secrets.baseline
iac:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkov Terraform
run: checkov -d infra/terraform -o sarif --quiet
containers:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Trivy image scan
run: trivy image --exit-code 1 --severity CRITICAL,HIGH ghcr.io/org/app:${{ github.sha }}This keeps PR latency under two minutes for normal code, and still turns the screws on sensitive changes.
Policy as code with OPA/Conftest (IaC, K8s, Terraform)
Text policies rot. Rego rules don’t. We codify cloud and Kubernetes standards with OPA/Conftest so every plan/apply gets a deterministic answer.
Example: disallow public S3 buckets unless a waiver tag is present.
package s3.secure
violation[msg] {
input.resource_type == "aws_s3_bucket"
not input.tags["waiver-expire"]
input.acl == "public-read"
msg := sprintf("Bucket %s is public without a time-bound waiver", [input.name])
}Run it with Conftest against Terraform plans in CI:
terraform init && terraform plan -out tf.plan
terraform show -json tf.plan > tf.json
conftest test --policy policy/ tf.jsonWe pair this with Checkov/tfsec for breadth and Rego for the opinions your auditors actually care about.
Automated proofs: SBOMs, attestations, and verifiable trails
Auditors don’t want screenshots; they want artifacts. Generate them every run.
- SBOMs:
syftorcyclonedxfor packages; store in artifact repo. - Vulnerability reports: SARIF outputs from Trivy/SAST for PRs.
- Attestations: sign pipelines and artifacts with Cosign + in-toto; adopt SLSA builders.
Minimal example tying it together:
# Build
docker build -t ghcr.io/org/app:${GITHUB_SHA} .
# SBOM
syft packages ghcr.io/org/app:${GITHUB_SHA} -o cyclonedx-json > sbom.json
# Sign image and SBOM
cosign sign --key $COSIGN_KEY ghcr.io/org/app:${GITHUB_SHA}
cosign attest --key $COSIGN_KEY --predicate sbom.json --type cyclonedx ghcr.io/org/app:${GITHUB_SHA}
# Verify in deploy pipeline
cosign verify ghcr.io/org/app:${GITHUB_SHA} \
--certificate-identity-regexp ".*github.com/org/repo/.+" \
--certificate-oidc-issuer https://token.actions.githubusercontent.comWe also drop a short JSON “build receipt” into an evidence bucket per release: build ID, commit, checks run, versions, signatures. Auditors love it; developers don’t have to assemble anything at audit time.
Handling regulated data without freezing the org
You can keep PCI/SOC 2/HiTRUST happy and still ship daily. The trick is scoping and paved roads.
- CodeOwner boundaries:
CODEOWNERSforsrc/payments/,infra/prod/, etc. Security must approve high-risk changes. - Stricter SLOs: vulnerabilities touching regulated domains get 72h MTTR; others 14 days.
- Golden libraries: provide pre-approved clients for encryption, tokenization, logging redaction. Enforce via lint rules.
A pragmatic ESLint rule to forbid raw console.log in regulated modules and force a redacting logger:
{
"rules": {
"no-restricted-syntax": [
"error",
{
"selector": "CallExpression[callee.object.name='console'][callee.property.name='log']",
"message": "Use secureLogger.info() with redaction"
}
]
}
}Pair it with directory-based config overrides so it only bites where it should.
Exceptions that expire (or they never will)
Every shop needs a break-glass process. Make it observable, time-bound, and noisy.
- Waiver files:
.waivers/ID.yamlwith owner, reason, risk, expiry, and link to ticket. - CI step that fails if a waiver is expired or owner is missing.
- Slack alerts 7 days before expiry; auto-create a follow-up issue.
Example waiver schema we’ve used:
id: WVR-2024-112
owner: security@company.com
resource: docker://ghcr.io/org/app@sha256:...
risk: Medium
reason: Third-party lib fix scheduled next sprint
expires: 2025-02-15
controls:
- compensating: WAF rule 123 enabled
- monitoring: Prometheus alert pii_log_suspected == 0 for 7dThis turns exceptions into a managed queue, not a graveyard.
Operate it like a product: metrics that matter
If you can’t measure it, you’ll ship around it. What we track in the first 90 days:
- PR security failure rate and median added latency to CI.
- Vulnerability MTTR (by severity and regulated domain).
- False positive rate (from developer feedback). Aim <10%.
- Waiver backlog size and % expired.
- Coverage: % repos with pre-commit installed; % services with signed SBOMs.
At a fintech client (SOC 2 + PCI DSS), we cut PR security failures from 14% to 3%, kept median PR overhead under 90 seconds, and dropped critical vuln MTTR from 11 days to 3—all without adding headcount. The secret wasn’t a new scanner; it was tuning, scoping, and proofs.
What I’d roll out next week (with your team)
- Baseline the top 10 policies as checks (PII logging, HTTPS only, secrets, IaC misconfig) and ship pre-commit hooks repo-wide.
- Add Semgrep rules for your languages and a tiny set of OPA/Conftest policies for Terraform/K8s.
- Wire a risk-aware CI workflow: changed-only for typical code, full for regulated paths.
- Produce SBOMs and sign images with Cosign; archive proofs per release.
- Stand up golden libraries for redaction/encryption; lint to enforce usage.
- Introduce waiver files with expirations and Slack reminders.
- Instrument metrics and review weekly with engineering leads; tune until false-positive rage drops.
If you’re cleaning up AI-generated “vibe code,” do the same—start with high-signal guards and progressively turn dials. We’ve rescued repos where LLMs hallucinated insecure HTTP clients and leaky JSON logs. The playbook above caught 80% of it before review.
Key takeaways
- Translate policy sentences into specific checks that run in the IDE, pre-commit, and CI.
- Use policy-as-code (Semgrep, OPA/Conftest) with progressive enforcement: warn locally, block in CI.
- Generate automated proofs (attestations, SBOMs, SARIF) that auditors accept without slowing engineers.
- Apply stricter rules to regulated-data paths and critical services; keep fast lanes for low-risk code.
- Time-box exceptions with signed waivers and alerts so they don’t become permanent debt.
Implementation checklist
- Install and tune local-first scanners (Semgrep, detect-secrets, ESLint, Bandit) via `pre-commit` or `lefthook`.
- Write 5-10 high-signal Semgrep rules for your top risks (e.g., PII logging, insecure HTTP).
- Add OPA/Conftest policies for IaC (Terraform, K8s) and block critical misconfigs.
- Wire CI to run changed-file scans fast (<2 min) and full scans nightly.
- Emit and sign SBOMs/attestations with Cosign; store proofs for audits.
- Define risk tiers and map stricter gates to code paths and services.
- Create an exception workflow with owners, expirations, and follow-up alerts.
Questions we hear from teams
- How do we avoid drowning developers in false positives?
- Start with a tiny, high-signal rule set (5–10 rules). Run as warn-only locally and block only the highest-confidence patterns in CI. Incorporate developer feedback, track false-positive rate, and tune weekly for the first month.
- What about monorepos with dozens of services?
- Scope checks by path and service ownership. Use CODEOWNERS and path-based configs so regulated domains get stricter gates. Run changed-only scans per directory and schedule full scans nightly.
- We already have scanners. Do we need more tools?
- Probably not. You need better wiring and tuning. Most wins come from pre-commit, changed-file CI, OPA for IaC, and signing artifacts. We often remove redundant scanners to reduce noise and cost.
- How do we handle third-party/AI-generated code safely?
- Treat it as untrusted: run full SCA/SAST on vendor/AI diffs, force golden libraries for crypto/logging, and require review from a security champion. Our teams use Semgrep rules that specifically catch common AI-generated insecure patterns.
- Will this slow our releases?
- Not if you scope it. Our target is <90 seconds median CI overhead on PRs, with heavier checks on high-risk paths only. Full scans run nightly or pre-release, not on every commit.
Ready to modernize your codebase?
Let GitPlumbers help you transform AI-generated chaos into clean, scalable applications.
