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.

7 Powerful Steps: Add an ASVS 5.0 Gate to CI/CD

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

  1. A GitHub Actions pipeline that runs Semgrep, Bandit (if Python is present), and ZAP Baseline (DAST in GitHub Actions).
  2. A lightweight ASVS gate script that maps findings to ASVS 5.0 and fails the build on high-risk auth/session/access-control issues.
  3. 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

Screenshot of the free tools webpage where you can access security assessment tools for different vulnerability detection.
Screenshot of the free tools webpage where you can access security assessment tools for different vulnerability detection.

Sample report to check Website Vulnerability with key findings and risk levels

An example of a vulnerability assessment report generated with our free tool provides insights into possible vulnerabilities.
An example of a vulnerability assessment report generated with our free tool provides insights into possible vulnerabilities.

6) Recent reads from Cyber Rely (for deeper dives)

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:

(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.


Free Consultation

If you have any questions or need expert assistance, feel free to schedule a Free consultation with one of our security engineers>>

🔐 Frequently Asked Questions (FAQs)

Find answers to commonly asked questions about ASVS 5.0 Gate to CI/CD.

Get a Quote

Leave a Comment

Your email address will not be published. Required fields are marked *