file

PatchPilot — Autonomous DevSecOps for Every Push

A self-healing DevSecOps pipeline that scans, patches, and visualizes application security — automatically, on every push.

Software development today operates at an unprecedented speed. Continuous Integration (CI) and Continuous Deployment (CD) allow developers to push updates multiple times a day. However, this rapid development cycle introduces a serious challenge: security vulnerabilities can easily slip into the codebase without being detected.

Traditional security testing models, which rely on late-stage manual assessments, are no longer adequate. To overcome these challenges, DevSecOps integrates security into every phase of the DevOps pipeline.

Welcome to PATCHPILOT, an automated DevSecOps pipeline that performs security scanning, rule-based patching, auditing, and visualization—without any manual effort. In this walkthrough, I will show you how to build a system that detects vulnerabilities immediately when developers push code, automatically takes corrective actions, and provides a beautiful dashboard for visualization.

What We Are Building

PATCHPILOT is a fully automated end-to-end DevSecOps pipeline using GitHub Actions. It integrates:

  • Trivy for container and dependency vulnerability scanning.
  • Bandit for Python Static Application Security Testing (SAST).
  • Semgrep for rapid, multi-language pattern analysis. While Bandit handles our Python backend, Semgrep bridges the gap by scanning frontend code, configuration files, and HTML/Jinja2 templates. It uses highly customizable, rule-based matching to instantly catch dangerous misconfigurations—such as disabled autoescaping in templates or hardcoded secrets in YAML files—ensuring security across the entire application stack.
  • A Custom Rule-Based Auto-Patching Engine to automatically fix known vulnerabilities (like XSS).
  • A Streamlit Dashboard to visualize the Before/After impact of our automated patches.

Project Architecture

Here is a visual representation of how PATCHPILOT operates from the moment a developer commits code to the final vulnerability report:

$$Insert “PROJECT DIAGRAM” Image Here$$

> (Caption: PATCHPILOT Workflow Architecture)

Phase 1: Prerequisites & Setup

Before we build the pipeline, you need a few basic tools installed on your local machine.

1. Install Git

If you don’t have Git installed, download it from the official website. Keep the default installation settings.

image

2. Install Docker Desktop

We will use Docker to containerize our application. Download it from Docker’s official site. Ensure WSL 2 is enabled if you are on Windows.

image

3. Install Python

Download Python 3.x from python.org. Important: Make sure to check the box that says “Add Python to PATH” during installation.

image

Phase 2: GitHub Repository & Secrets Setup

1. Create a New Repository

  1. Log in to GitHub and create a new repository.
  2. Name it patchpilot.
  3. Set it to Public and check Initialize with README.

2. Configure GitHub Secrets

Our GitHub Actions pipeline needs to push Docker images to Docker Hub. We need to store your credentials securely.

  1. Go to your repository Settings -> Secrets and variables -> Actions.
  2. Click New repository secret.
  3. Add the following secrets:
    • DOCKER_USERNAME: Your Docker Hub username.
    • DOCKER_PASSWORD: Your Docker Hub password or access token.
image

Phase 3: Project Folder Structure

Open your terminal or PowerShell and run the following commands to create the core directory layout for PATCHPILOT:

New-Item -ItemType Directory -Path .\patchpilot\.github\workflows -Force
New-Item -ItemType Directory -Path .\patchpilot\vulnerable_app\templates -Force
New-Item -ItemType Directory -Path .\patchpilot\scripts -Force
New-Item -ItemType Directory -Path .\patchpilot\dashboard -Force
Set-Location .\patchpilot

image

Phase 4: Building the CI/CD Pipeline

The brain of PATCHPILOT is the GitHub Actions workflow. Navigate to .github/workflows/ and create a file named devsecops_pipeline.yml.

Copy and paste the following configuration. (Note: Replace rupamgit123 with your actual Docker Hub username in the ‘Define Image Tags’ step).

name: DevSecOps Pipeline

on:
  push:
    branches:
      - main

jobs:
  build-and-scan:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.x'

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Get current commit SHA
        id: commit
        run: echo "SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT

      - name: Define Image Tags
        id: tags
        run: |
          echo "VULN_IMAGE_TAG=rupamgit123/autoshield-vulnerable-app:${{ steps.commit.outputs.SHA }}" >> $GITHUB_OUTPUT
          echo "PATCHED_IMAGE_TAG=rupamgit123/autoshield-patched-app:${{ steps.commit.outputs.SHA }}" >> $GITHUB_OUTPUT

      # --- BEFORE PATCHING ---
      - name: Build Vulnerable Docker Image
        run: |
          docker build -t ${{ steps.tags.outputs.VULN_IMAGE_TAG }} ./vulnerable_app
          docker push ${{ steps.tags.outputs.VULN_IMAGE_TAG }}

      - name: Run Trivy Scan (Vulnerable Image)
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ steps.tags.outputs.VULN_IMAGE_TAG }}
          format: 'json'
          output: 'dashboard/trivy_scan_before.json'
          severity: 'CRITICAL,HIGH'
        env:
          TRIVY_NO_PROGRESS: 'true'

      - name: Save Vulnerable Scan Report (JSON)
        uses: actions/upload-artifact@v4
        with:
          name: trivy_scan_before_json
          path: dashboard/trivy_scan_before.json

      - name: Run Trivy Scan (Vulnerable Image - Text Report)
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ steps.tags.outputs.VULN_IMAGE_TAG }}
          format: 'table'
          output: 'dashboard/trivy_scan_before.txt'
          severity: 'CRITICAL,HIGH'
        env:
          TRIVY_NO_PROGRESS: 'true'

      - name: Save Vulnerable Scan Report (TXT)
        uses: actions/upload-artifact@v4
        with:
          name: trivy_scan_before_txt
          path: dashboard/trivy_scan_before.txt

      # --- SAST Scans ---
      - name: Run Bandit SAST Scan
        run: |
          pip install bandit
          bandit -r ./vulnerable_app --severity-level HIGH --confidence-level HIGH -f json -o dashboard/bandit_report.json || true
        working-directory: ${{ github.workspace }}

      - name: Run Semgrep SAST Scan
        id: semgrep_scan
        run: |
          pip install semgrep
          semgrep scan --config auto --autofix --sarif --output semgrep_results.sarif || true
        continue-on-error: true

      - name: Convert Semgrep SARIF to JSON
        run: |
          mv semgrep_results.sarif dashboard/semgrep_report.json || true
        continue-on-error: true

      # --- PATCHING PROCESS ---
      - name: Run Patching Script (Unpin requirements.txt & modify code)
        run: |
          python scripts/patch_vulnerabilities.py

      - name: Modify Dockerfile for Patched Image (Base OS Upgrade)
        run: |
          sed -i 's/FROM python:3\.8-slim-buster/FROM python:3.11-slim-bookworm/' ./vulnerable_app/Dockerfile || true

      # --- AFTER PATCHING ---
      - name: Build Patched Docker Image
        run: |
          docker build -t ${{ steps.tags.outputs.PATCHED_IMAGE_TAG }} ./vulnerable_app
          docker push ${{ steps.tags.outputs.PATCHED_IMAGE_TAG }}

      - name: Run Trivy Scan (Patched Image)
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ steps.tags.outputs.PATCHED_IMAGE_TAG }}
          format: 'json'
          output: 'dashboard/trivy_scan_after.json'
          severity: 'CRITICAL,HIGH'
        env:
          TRIVY_NO_PROGRESS: 'true'

      - name: Save Patched Scan Report (JSON)
        uses: actions/upload-artifact@v4
        with:
          name: trivy_scan_after_json
          path: dashboard/trivy_scan_after.json

      - name: Run Trivy Scan (Patched Image - Text Report)
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ steps.tags.outputs.PATCHED_IMAGE_TAG }}
          format: 'table'
          output: 'dashboard/trivy_scan_after.txt'
          severity: 'CRITICAL,HIGH'
        env:
          TRIVY_NO_PROGRESS: 'true'

      - name: Save Patched Scan Report (TXT)
        uses: actions/upload-artifact@v4
        with:
          name: trivy_scan_after_txt
          path: dashboard/trivy_scan_after.txt

      - name: Commit and Push Reports
        uses: stefanzweifel/git-auto-commit-action@v5
        with:
          commit_message: "docs: Automated scan reports for ${{ steps.commit.outputs.SHA }}"
          branch: main
          file_pattern: 'dashboard/trivy_scan_*.json dashboard/trivy_scan_*.txt dashboard/bandit_report.json dashboard/semgrep_report.json'

$$Insert Figure.8.7.1: Preparing CICD pipeline here$$

Phase 5: The Auto-Patching Scripts

$$Insert Figure.8.8.1: Scripts folder overview here$$

Navigate to the scripts/ folder. We will create our automated remediation script here. Create a file named patch_vulnerabilities.py. This script reads the Bandit and Semgrep reports and injects security fixes directly into the codebase.

# scripts/patch_vulnerabilities.py
import json
import re
import os

# Define paths for all reports
DASHBOARD_DIR = "./dashboard"
BANDIT_REPORT_PATH = os.path.join(DASHBOARD_DIR, "bandit_report.json")
SEMGREP_REPORT_PATH = os.path.join(DASHBOARD_DIR, "semgrep_report.json")
APP_CODE_BASE_PATH = "./vulnerable_app"

def fix_reflected_xss_in_fstring(code_lines, line_number):
    """Rule to fix a specific XSS pattern by wrapping variables in escape()"""
    line_index = line_number - 1
    original_line = code_lines[line_index]
    
    match = re.search(r'\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}', original_line)
    if not match:
        print(f"  -    Rule for XSS couldn't find a variable to escape on line {line_number}.")
        return None, None
        
    variable_to_escape = match.group(1)
    new_line_content = original_line.replace(f"{{{variable_to_escape}}}", f"{{escape({variable_to_escape})}}")
    code_lines[line_index] = new_line_content
    
    # Ensure 'escape' is imported
    has_import = any("from markupsafe import escape" in line for line in code_lines)
    if not has_import:
        for i, line in enumerate(code_lines):
            if line.strip().startswith("from") or line.strip().startswith("import"):
                code_lines.insert(i, "from markupsafe import escape\n")
                break
        else:
             code_lines.insert(0, "from markupsafe import escape\n")
             
    print(f"  -    Patched XSS on line {line_number} by escaping '{variable_to_escape}'.")
    return "".join(code_lines), new_line_content

def fix_sql_injection(code_lines, line_number):
    # Example placeholder for SQL Injection logic
    return None, None

def fix_jinja_xss(code_lines, line_number):
    """Fixes autoescape being disabled in Jinja2 templates."""
    line_index = line_number - 1
    original_line = code_lines[line_index]
    if "autoescape false" in original_line:
        new_line_content = original_line.replace("autoescape false", "autoescape true")
        code_lines[line_index] = new_line_content
        print(f"  -    Patched Jinja2 XSS on line {line_number} by enabling autoescape.")
        return "".join(code_lines), new_line_content
    return None, None

# --- Rulebook ---
FIX_RULES = {
    "B608": fix_sql_injection,
    "B703": fix_reflected_xss_in_fstring,
    "jinja2.security.autoescape-off.autoescape-off": fix_jinja_xss,
}

def apply_fixes(file_path, vulnerabilities, code_lines):
    modified_code = None
    original_code_for_file = list(code_lines)
    for vuln in vulnerabilities:
        vuln_code = vuln['id']
        line_num = vuln['line']
        if vuln_code in FIX_RULES:
            print(f"   Found '{vuln_code}' in {file_path} on line {line_num}. Attempting fix.")
            fix_function = FIX_RULES[vuln_code]
            new_code_content, applied_change = fix_function(original_code_for_file, line_num)
            
            if new_code_content:
                modified_code = new_code_content
                original_code_for_file = modified_code.splitlines(keepends=True)
    return modified_code

def main():
    print("---    Starting Rule-Based Remediation ---")
    files_to_patch = {}
    
    # Process Bandit Report
    try:
        if os.path.exists(BANDIT_REPORT_PATH):
            with open(BANDIT_REPORT_PATH, 'r') as f:
                bandit_report = json.load(f)
            if bandit_report.get('results'):
                for vuln in bandit_report['results']:
                    file_path = vuln['filename']
                    if file_path not in files_to_patch:
                        files_to_patch[file_path] = []
                    files_to_patch[file_path].append({'id': vuln['test_id'], 'line': vuln['line_number']})
    except (FileNotFoundError, json.JSONDecodeError):
        pass

    # Process Semgrep Report
    try:
        if os.path.exists(SEMGREP_REPORT_PATH):
            with open(SEMGREP_REPORT_PATH, 'r') as f:
                semgrep_report = json.load(f)
            if semgrep_report.get('results'):
                for vuln in semgrep_report['results']:
                    file_path = vuln['path']
                    if file_path not in files_to_patch:
                        files_to_patch[file_path] = []
                    files_to_patch[file_path].append({'id': vuln['check_id'], 'line': vuln['start']['line']})
    except (FileNotFoundError, json.JSONDecodeError):
        pass

    if not files_to_patch:
        print("   No patchable vulnerabilities found.")
        return

    for file_path, vulnerabilities in files_to_patch.items():
        print(f"\nProcessing file: {file_path}")
        try:
            with open(file_path, 'r') as f:
                code_lines = f.readlines()
            modified_content = apply_fixes(file_path, vulnerabilities, code_lines)
            if modified_content:
                with open(file_path, 'w') as f:
                    f.write(modified_content)
                print(f"   Successfully patched {file_path}.")
        except FileNotFoundError:
            print(f"  -    Could not find file {file_path} to patch.")

if __name__ == "__main__":
    main()

$$Insert Figure.8.8.2 / Figure.8.8.3: rule_based_patcher.py / patch_vulnerabilities.py overview here$$

Phase 6: Creating Blank Dashboard Files

Before the pipeline runs for the first time, we need to create empty JSON and TXT placeholder files so the pipeline doesn’t throw “File Not Found” errors.

Run these commands in PowerShell from your project root:

Set-Content -Path ".\dashboard\trivy_scan_before.json" -Value '{"Results": []}'
Set-Content -Path ".\dashboard\trivy_scan_before.txt" -Value ""
Set-Content -Path ".\dashboard\trivy_scan_after.json" -Value '{"Results": []}'
Set-Content -Path ".\dashboard\trivy_scan_after.txt" -Value ""
Set-Content -Path ".\dashboard\bandit_report.json" -Value '{"results": []}'
Set-Content -Path ".\dashboard\semgrep_report.json" -Value '{"results": []}'

Phase 7: Add Your Application

Now, paste your application files inside the vulnerable_app/ folder. This should include your app.py, Dockerfile, requirements.txt, and any templates/ your application uses.

Phase 8: Initialize Git and Push to GitHub

Initialize the repository, commit your files, and push them to your newly created GitHub repo.

git init
git add .
git commit -m "Added PATCHPILOT application and initial setup"
git branch -M main
git remote add origin [https://github.com/your-username/patchpilot.git](https://github.com/your-username/patchpilot.git)
git push -u origin main

Check GitHub Actions

As soon as you push your code, GitHub detects it and starts the pipeline automatically. It will:

  1. Build the vulnerable image.
  2. Scan with Trivy, Bandit, and Semgrep.
  3. Run the Auto-Patching script.
  4. Rebuild the patched image.
  5. Save and commit the new reports.
image
image

Phase 9: Visualizing Results with Streamlit

Once the pipeline finishes, pull the latest changes to get the generated vulnerability reports to your local machine:

git pull

If you have created a Streamlit application (app.py) inside your dashboard folder, you can run it to visualize your results!

cd dashboard
streamlit run app.py
image

Dashboard Breakdown

Our dashboard gives us incredible visual insight into the security posture of our application:

  • Vulnerability Comparison: See the exact drop in high-severity vulnerabilities before and after our auto-patcher ran.
image
  • Trivy Dependencies: The pipeline automatically updated the base Docker image from Debian Buster to Bookworm, wiping out a vast majority of OS-level CVEs.
image
image
  • Bandit & Semgrep Results: Identify exact lines of code where XSS vulnerabilities and unsafe Flask setups were located (and patched!).

You can even download a consolidated CSV file of all findings for your compliance team!

image
image
image

Results & Analysis

Through automated pipeline execution, PATCHPILOT significantly improved the security of the test application:

  1. OS Vulnerabilities: By automatically swapping the Docker base image (python:3.8-slim-buster $\rightarrow$ python:3.11-slim-bookworm), massive amounts of CRITICAL and HIGH severity CVEs were eliminated.
  2. Code Security: Our rule-based patcher automatically wrapped vulnerable variables in escape() to prevent Reflected XSS, and safely re-enabled autoescape inside Jinja2 templates.
  3. No Developer Friction: The entire process—from detection to remediation to reporting—occurred seamlessly on git push.

Conclusion

PATCHPILOT marks an important step toward building an intelligent DevSecOps pipeline. By combining the powers of GitHub Actions, Trivy, Bandit, Semgrep, and custom Python remediation scripting, we successfully built a system where security truly evolves with every commit.

While rule-based patching has limitations with complex business-logic flaws, this framework removes a massive amount of technical debt and foundational security vulnerabilities without slowing down developer velocity.

Have questions about implementing this in your own CI/CD pipeline? Drop a comment below!

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

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