7 Powerful Steps: Add an ASVS 5.0 Gate to CI/CD
Shipping features is great—shipping evidence-backed security is better. This post turns ASVS 5.0 into executable CI/CD checks using GitHub Actions, Semgrep, Bandit, and DAST in GitHub Actions via ZAP Baseline. You’ll get ready-to-paste workflows, tiny diffs for SSRF/IDOR/token handling, and a way to store “evidence that sticks” for security and compliance.
Update (October 23, 2025): We’ve published a developer guide on shipping a SEC Item 1.05 pipeline—automating cyber 8-K materiality signals, a four-business-day timer, and a signed evidence store. Includes Python/TypeScript/GitHub Actions code you can copy-paste.
Read now: https://www.cybersrely.com/sec-item-1-05-pipeline-cyber-8-k/
Where this publishes: Cyber Rely → for developers & engineering leaders.
TL;DR: What you’ll build
- A GitHub Actions pipeline that runs Semgrep, Bandit (if Python is present), and ZAP Baseline (DAST in GitHub Actions).
- A lightweight ASVS gate script that maps findings to ASVS 5.0 and fails the build on high-risk auth/session/access-control issues.
- Artifact uploads (SARIF, ZAP HTML, logs, a signed summary) so devs can hand audit-ready evidence to security/compliance.
1) Map ASVS 5.0 to checks you can automate
Create a mapping file asvs-map.json
that aligns tool rules to ASVS 5.0 areas you care about first (Auth, Session, Access Control, SSRF/Input, Crypto/Token handling). This is the backbone of your ASVS 5.0 CI gate.
{
"V2-Authentication": {
"description": "Authentication robustness",
"semgrep_rules": ["auth-basic", "jwt-missing-verify", "no-password-hashing"],
"zap_alerts": ["Authentication", "Weak Authentication"]
},
"V3-Session": {
"description": "Session management",
"semgrep_rules": ["insecure-cookie", "session-missing-httponly"],
"zap_alerts": ["Cookie No HttpOnly Flag", "Cookie Without SameSite Attribute"]
},
"V4-AccessControl": {
"description": "Broken access control / IDOR",
"semgrep_rules": ["idor-suspect", "path-traversal"],
"zap_alerts": ["Insecure Direct Object Reference", "Path Traversal"]
},
"V5-Validation-SSRF": {
"description": "Validation & SSRF hardening",
"semgrep_rules": ["ssrf-raw-http", "user-controlled-request"],
"zap_alerts": ["Server-Side Request Forgery"]
},
"V9-DataProtection-Tokens": {
"description": "Token protection / crypto",
"semgrep_rules": ["jwt-none-alg", "weak-jwt-secret"],
"zap_alerts": ["Sensitive Information in Token"]
}
}
Tip: Start small. Add rules as you learn. “ASVS 5.0 CI” works best when it’s iterative—not perfect on day one.
2) GitHub Actions: OWASP pipeline checks + DAST in GitHub Actions
Create .github/workflows/security-asvs.yml
:
name: security-asvs-gate
on:
pull_request:
branches: [ main ]
workflow_dispatch:
permissions:
contents: read
security-events: write # needed for SARIF uploads
actions: read
jobs:
sast-semgrep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python (for gate script)
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install Semgrep
run: pip install semgrep
- name: Run Semgrep (OWASP pipeline checks)
run: semgrep ci --sarif --output semgrep.sarif || true
- name: Upload Semgrep SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: semgrep.sarif
- name: Store Semgrep artifact
uses: actions/upload-artifact@v4
with:
name: semgrep-sarif
path: semgrep.sarif
sast-bandit:
runs-on: ubuntu-latest
if: ${{ hashFiles('**/*.py') != '' }}
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install Bandit
run: pip install bandit
- name: Run Bandit
run: bandit -r . -f sarif -o bandit.sarif || true
- name: Upload Bandit SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: bandit.sarif
- name: Store Bandit artifact
uses: actions/upload-artifact@v4
with:
name: bandit-sarif
path: bandit.sarif
dast-zap-baseline:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Start app under test
run: |
docker compose -f docker-compose.yml up -d
sleep 15
- name: ZAP Baseline Scan (DAST in GitHub Actions)
run: |
docker run --network host --rm \
-v $PWD:/zap/wrk/:rw \
owasp/zap2docker-stable zap-baseline.py \
-t http://localhost:8080 \
-r zap-baseline.html \
-x zap-baseline.xml || true
- name: Store ZAP Reports
uses: actions/upload-artifact@v4
with:
name: zap-baseline
path: |
zap-baseline.html
zap-baseline.xml
asvs-gate:
runs-on: ubuntu-latest
needs: [sast-semgrep, sast-bandit, dast-zap-baseline]
steps:
- uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Install gate deps
run: pip install lxml jsonschema
- name: Load mapping & evaluate (fail on high-risk)
run: |
python .github/scripts/asvs_gate.py \
--map asvs-map.json \
--semgrep artifacts/semgrep-sarif/semgrep.sarif \
--bandit artifacts/bandit-sarif/bandit.sarif \
--zap artifacts/zap-baseline/zap-baseline.xml \
--fail-on V2-Authentication V3-Session V4-AccessControl
- name: Upload ASVS summary
uses: actions/upload-artifact@v4
with:
name: asvs-summary
path: gate-summary.json
Create .github/scripts/asvs_gate.py
:
#!/usr/bin/env python3
import json, sys, argparse
from xml.etree import ElementTree as ET
def load_sarif(path):
with open(path) as f:
data = json.load(f)
results = []
for run in data.get("runs", []):
for r in run.get("results", []):
rid = r.get("ruleId") or (r.get("rule", {}).get("id"))
sev = (r.get("level") or "").lower()
results.append((rid or "unknown", sev))
return results
def load_zap_xml(path):
tree = ET.parse(path)
alerts = []
for a in tree.findall(".//alertitem"):
name = a.findtext("alert") or ""
risk = (a.findtext("riskcode") or "0")
alerts.append((name, risk))
return alerts
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--map", required=True)
ap.add_argument("--semgrep")
ap.add_argument("--bandit")
ap.add_argument("--zap")
ap.add_argument("--fail-on", nargs="+", default=[])
args = ap.parse_args()
mapping = json.load(open(args.map))
semgrep = load_sarif(args.semgrep) if args.semgrep and os.path.exists(args.semgrep) else []
bandit = load_sarif(args.bandit) if args.bandit and os.path.exists(args.bandit) else []
zap = load_zap_xml(args.zap) if args.zap and os.path.exists(args.zap) else []
summary = {k: {"hits": []} for k in mapping.keys()}
def hit(cat, item, src, sev):
summary[cat]["hits"].append({"source": src, "id": item, "severity": sev})
# correlate Semgrep/Bandit to categories
for cat, defn in mapping.items():
rules = set(defn.get("semgrep_rules", []))
for rid, sev in semgrep + bandit:
if rid in rules:
hit(cat, rid, "sast", sev or "warning")
# correlate ZAP to categories
for cat, defn in mapping.items():
alerts = set(defn.get("zap_alerts", []))
for name, risk in zap:
if name in alerts:
risk_level = {"0":"info","1":"low","2":"medium","3":"high"}.get(risk, "info")
hit(cat, name, "dast", risk_level)
# decide failure
failures = []
for cat in args.fail_on:
for h in summary.get(cat, {}).get("hits", []):
if h["severity"] in ("high", "error", "critical", "medium"):
failures.append((cat, h))
with open("gate-summary.json","w") as f:
json.dump(summary, f, indent=2)
if failures:
print("ASVS gate failed on categories:", ", ".join(set(c for c,_ in failures)))
sys.exit(1)
print("ASVS gate passed.")
sys.exit(0)
if __name__ == "__main__":
import os
main()
What this gives you: an ASVS 5.0 CI gate that treats “OWASP pipeline checks” as code: SAST (Semgrep/Bandit) + DAST in GitHub Actions (ZAP) → mapped to ASVS categories → hard fail when high-risk Auth/Session/Access-Control issues are detected.
3) Minimal code hardening: SSRF, IDOR, token handling (tiny diffs + tests)
3.1 SSRF allow-list (Node.js, Axios)
// ssrf-guard.js
import dns from "node:dns/promises";
import { URL } from "node:url";
import axios from "axios";
const ALLOWED_HOSTS = new Set(["api.my-svc.internal", "updates.example.com"]);
export async function safeFetch(targetUrl) {
const u = new URL(targetUrl);
if (!ALLOWED_HOSTS.has(u.hostname)) throw new Error("SSRF blocked");
const { address } = await dns.lookup(u.hostname, { verbatim: true });
// deny link-local, private ranges
if (/^(0\.|10\.|127\.|169\.254\.|172\.(1[6-9]|2\d|3[0-1])\.|192\.168\.)/.test(address)) {
throw new Error("SSRF to private address");
}
return axios.get(u.toString(), { maxRedirects: 0, timeout: 5000 });
}
Test:
// ssrf-guard.test.js
import { safeFetch } from "./ssrf-guard";
test("blocks unknown host", async () => {
await expect(safeFetch("http://169.254.169.254/latest/meta-data")).rejects.toThrow();
});
ASVS map: V5-Validation-SSRF.
3.2 IDOR fix (Express + ownership check)
// routes/orders.js
app.get("/orders/:id", async (req, res) => {
const order = await db.orders.findById(req.params.id);
if (!order || order.user_id !== req.user.id) return res.sendStatus(404);
res.json(order);
});
Test:
// orders.test.js
it("denies access to others' orders", async () => {
const r = await agentAs("alice").get("/orders/BOB_ORDER_ID");
expect(r.status).toBe(404);
});
ASVS map: V4-AccessControl.
3.3 Token handling (short-lived access, rotating refresh, secure cookies)
// auth/cookies.js
const cookieOpts = {
httpOnly: true,
sameSite: "Strict",
secure: true
};
res.cookie("access", accessJwt, { ...cookieOpts, maxAge: 10 * 60 * 1000 }); // 10m
res.cookie("refresh", refreshJwt, { ...cookieOpts, maxAge: 7 * 24 * 60 * 60 * 1000 });
Rotation endpoint:
app.post("/auth/rotate", requireAuth, async (req, res) => {
// verify refresh, check jti not reused/blocked
// issue new pair, revoke old refresh by jti
res.json({ ok: true });
});
ASVS map: V2-Authentication, V3-Session, V9-DataProtection-Tokens.
4) “Evidence that sticks” (artifacts your auditors love)
Enhance the workflow with a PR summary and durable artifacts:
pr-summary:
runs-on: ubuntu-latest
needs: [asvs-gate]
if: always()
steps:
- uses: actions/download-artifact@v4
with: { path: artifacts }
- name: Summarize results for PR
run: |
echo "### ASVS 5.0 CI Summary" >> $GITHUB_STEP_SUMMARY
echo "- Semgrep: artifacts/semgrep-sarif/semgrep.sarif" >> $GITHUB_STEP_SUMMARY
echo "- Bandit: artifacts/bandit-sarif/bandit.sarif" >> $GITHUB_STEP_SUMMARY
echo "- ZAP: artifacts/zap-baseline/zap-baseline.html" >> $GITHUB_STEP_SUMMARY
echo "- Gate: artifacts/asvs-summary/gate-summary.json" >> $GITHUB_STEP_SUMMARY
Artifacts provide immutable proof of the OWASP pipeline checks you ran, the findings, and the ASVS gate decision—great for risk reviews and audits.
5) Quick external check (free): run our scanner
Before merging, run an outside-in check with our free tool: Website Vulnerability Scanner. Drop the scan link in your PR so reviewers see both inside-out (SAST) and outside-in (DAST) signals.
Screenshot of the free Security tool homepage showing scan input
Sample report to check Website Vulnerability with key findings and risk levels
6) Recent reads from Cyber Rely (for deeper dives)
- Best 7 Ways to Prevent LDAP Injection in React.js — practical dev tips with examples.
- CRLF Injection in TypeScript-based ERP: Best 7 Ways to Prevent It — step-by-step code defenses.
- Fix Weak SSL/TLS Configuration in TypeScript — 10 best practices with code.
Browse all posts: Cyber Rely Blog.
7) When you need help: assessments & remediation
If your ASVS 5.0 CI reveals systemic risks, our teams can help you prioritize and fix:
- Risk Assessment Services — HIPAA, PCI DSS, SOC 2, ISO 27001 & GDPR.
- Remediation Services — close gaps fast with audit-ready evidence.
(You can also explore specialized offerings like SOC 2 and ISO 27001 remediation if that’s your path.)
8) Full example: monorepo-friendly composite action
Wrap the gate into a reusable action .github/actions/asvs-gate/action.yml
:
name: ASVS Gate
runs:
using: "composite"
steps:
- run: pip install lxml jsonschema
shell: bash
- run: |
python $GITHUB_ACTION_PATH/asvs_gate.py \
--map $GITHUB_WORKSPACE/asvs-map.json \
--semgrep $GITHUB_WORKSPACE/semgrep.sarif \
--bandit $GITHUB_WORKSPACE/bandit.sarif \
--zap $GITHUB_WORKSPACE/zap-baseline.xml \
--fail-on V2-Authentication V3-Session V4-AccessControl
shell: bash
Call it from any workflow to keep ASVS 5.0 CI consistent across services.
Add this ASVS 5.0 CI gate today
Copy the workflow, commit the tiny SSRF/IDOR/token diffs, and ship your next feature with confidence. If you want a quick outside-in check first, run our free scanner and attach the report to your PR.
🔐 Frequently Asked Questions (FAQs)
Find answers to commonly asked questions about ASVS 5.0 Gate to CI/CD.