GeoDerp/offline-ci-scan

★ 0Forks 0PythonGitHub ↗Compare

README

Offline CI Scan — STIG-Hardened, Air-Gapped CI/CD Pipeline

A self-hosted, fully offline CI/CD security pipeline that runs SAST, linting, SCA, and AI-powered code review — then reports findings to DefectDojo and posts remediation suggestions directly on pull requests.

Every component runs inside Docker containers on your network. No data leaves your environment.

Architecture

graph TB
    classDef github  fill:#0366d6,stroke:#024ea4,color:#fff
    classDef runner  fill:#6f42c1,stroke:#4e2d8a,color:#fff
    classDef scanner fill:#e36209,stroke:#b04f00,color:#fff
    classDef dojo    fill:#d73a49,stroke:#a71d2a,color:#fff
    classDef db      fill:#586069,stroke:#3a3f45,color:#fff
    classDef ai      fill:#28a745,stroke:#1a7d33,color:#fff

    subgraph GH["☁️  GitHub.com (Cloud)"]
        DevRepo["📦 Your App Repo\n(.github/workflows/ci.yml)"]
        GHActions["⚙️ GitHub Actions\n(workflow scheduler)"]
        GHPR["💬 Pull Request\n(comments & status checks)"]
        DevRepo -->|"PR opened / updated"| GHActions
    end

    subgraph Stack["🖥️  Self-Hosted Server — Docker Compose"]
        Runner["🏃 GitHub Runner\nephemeral · airgap-runner"]

        subgraph Scan["🔍 CI Scanners  (run inside the job)"]
            Scanners["Semgrep · Trivy\nMegaLinter · Lizard"]
        end

        subgraph DD["🛡️  DefectDojo  (Vulnerability Management)"]
            Nginx["Nginx\n(reverse proxy)"]
            uWSGI["uWSGI\n(app server)"]
            PG["PostgreSQL\n(persistence)"]
            Redis["Redis\n(broker)"]
            Nginx --> uWSGI
            uWSGI --> PG
            uWSGI --> Redis
        end

        Ollama["🤖 Ollama\nqwen2.5-coder:1.5b\n(local LLM)"]
    end

    GHActions -->|"dispatch job\n(HTTPS + PAT)"| Runner
    Runner -->|"executes scans"| Scanners
    Scanners -->|"SARIF results"| Runner
    Runner -->|"AI inference\n(high-severity findings)"| Ollama
    Ollama -->|"fix suggestions"| Runner
    Runner -->|"upload SARIF reports"| Nginx
    Runner -->|"post AI fix + DefectDojo link\nas PR comments"| GHPR
    GHPR -.->|"reviewed by developer"| DevRepo

    class DevRepo,GHActions,GHPR github
    class Runner runner
    class Scanners scanner
    class Nginx,uWSGI dojo
    class PG,Redis db
    class Ollama ai
Loading
Component Purpose Image
GitHub Runner Ephemeral self-hosted Actions runner myoung34/github-runner:2.321.0
Ollama Local LLM inference (qwen2.5-coder) ollama/ollama:0.6.2
DefectDojo Vulnerability management & governance defectdojo/*:2.44.1
PostgreSQL DefectDojo persistence postgres:16-alpine
Redis DefectDojo broker redis:7-alpine

DISA STIG Compliance

The docker-compose.yml is hardened per the DISA Container Platform SRG:

Control Implementation
V-235803 Image provenance All images pinned to specific version tags — no :latest
V-235804 Non-root Containers run as non-root where the upstream image allows it
V-235806 Read-only FS read_only: true on stateless containers (Nginx, Redis)
V-235809 Capabilities cap_drop: ALL on every container; only required caps added back
V-235810 Health checks Every service has a healthcheck with start-period grace
V-235815 Resource limits mem_limit, cpus, pids_limit set on every container
V-235819 Network segmentation Four isolated Docker networks segment traffic between tiers
Privilege escalation security_opt: no-new-privileges:true on every container
Logging json-file driver with max-size / max-file rotation limits
Telemetry Disabled in DefectDojo, Ollama, Semgrep, MegaLinter, and Trivy

Workflow Hardening

The GitHub Actions workflow (.github/workflows/holistic-ci.yml) follows supply-chain best practices:

  • Pinned actions — every uses: reference is locked to a full SHA
  • Least-privilege permissions — permissions: set at workflow and job level
  • Timeouts — every job has timeout-minutes to prevent runaway builds
  • Concurrency — duplicate runs on the same PR are automatically cancelled

Quick Start

Prerequisites

Requirement Minimum
Docker Engine 24.0+
Docker Compose v2.20+
Host RAM 8 GB (16 GB recommended)
Host disk 40 GB free
Network Outbound HTTPS to github.com for runner registration; all scanning is offline

1. Clone and configure

git clone https://github.com/your-org/offline-ci-scan.git
cd offline-ci-scan
cp .env.example .env

Edit .env and fill in all values:

Variable Description
GITHUB_REPO_URL Repository the runner registers with
GITHUB_ACCESS_TOKEN PAT with repo + Actions scope (rotate every 90 days)
DD_ADMIN_PASSWORD DefectDojo admin password (≥15 chars, mixed complexity)
DD_POSTGRES_PASSWORD PostgreSQL password (same complexity rules)
DD_PORT Host port for DefectDojo UI (default 8080)

2. Start the stack

docker compose up -d

Wait for all health checks to pass:

docker compose ps          # All services should show "healthy"
docker compose logs -f github-runner   # Confirm runner registered

3. Obtain the DefectDojo API key

  1. Open http://<host>:8080 in a browser (you must be on the network).
  2. Log in with admin / the password you set in DD_ADMIN_PASSWORD.
  3. Navigate to API v2 Key and copy the token.
  4. Store it as a GitHub Actions secret named DEFECTDOJO_API_KEY on the target repository (Settings → Secrets → Actions).

4. Verify the runner

In the target repository go to Settings → Actions → Runners. You should see a runner prefixed with airgap-runner in an Idle state.


Using the Self-Hosted Runner in Your Repository

Create .github/workflows/ci.yml in any repository that should be scanned. The workflow below calls the reusable pipeline hosted in this repo:

# .github/workflows/ci.yml — drop this into your application repository
name: CI Pipeline

on:
  pull_request:
    branches: [main]

jobs:
  security-scan:
    # Call the central reusable workflow from this repo.
    # runs-on is defined inside the reusable workflow so it automatically
    # targets the self-hosted runner — no need to specify it here.
    uses: your-org/offline-ci-scan/.github/workflows/holistic-ci.yml@main
    with:
      image_name: "my-app"
      image_tag: ${{ github.sha }}
      # Optional: override if DefectDojo is on a different host
      # defectdojo_url: "http://defectdojo-nginx:8080"
    secrets:
      DEFECTDOJO_API_KEY: ${{ secrets.DEFECTDOJO_API_KEY }}

Why no runs-on? When you call a reusable workflow with uses:, the runs-on: [self-hosted, linux] is already declared inside each job of the called workflow. If you write a standalone workflow instead (not using uses:), you must add runs-on: [self-hosted, linux] to every job yourself.

What happens on a pull request

  1. Fast Feedback — Semgrep (SAST), Lizard (complexity), MegaLinter (lint) run against the PR code.
  2. Deep Analysis — The repo's Dockerfile is built and scanned by Trivy (SCA / CVE).
  3. AI Remediation — High-severity SARIF findings are sent to the local Ollama LLM. The model suggests secure-code fixes, which are posted as a PR comment.
  4. Governance — All SARIF reports are uploaded to DefectDojo. A second PR comment is posted with a direct link to the DefectDojo findings page.

Note: The DefectDojo link is only accessible from the internal network where the stack is running.

Application requirements

Requirement Reason
Dockerfile at repo root Needed by the Deep Analysis job to build and scan
Runner labels self-hosted, linux The workflow targets these labels
DEFECTDOJO_API_KEY secret Required for the Governance job

Directory Structure

.
├── .github/workflows/
│   └── holistic-ci.yml        # Reusable CI/CD pipeline
├── configs/
│   ├── defectdojo/             # DefectDojo overrides (reserved)
│   ├── lizard/.lizardrc        # Complexity thresholds
│   ├── semgrep/rules/local.yaml# Custom SAST rules
│   └── trivy/                  # Trivy overrides (reserved)
├── scripts/
│   └── ai_fixer.py             # AI remediation agent
├── .env.example                # Environment variable template
├── docker-compose.yml          # STIG-hardened container stack
└── README.md

Security & Maintenance

  • Rotate credentials every 90 days (GITHUB_ACCESS_TOKEN, DefectDojo API key, database passwords).
  • Pin and audit images — update version tags deliberately; never use :latest in production.
  • Monitor resources — docker stats and health-check logs will surface container issues early.
  • Prune regularly — run docker system prune --volumes on a schedule to reclaim disk space.
  • Back up DefectDojo — the defectdojo_postgres_data volume holds all scan history; include it in your backup plan.

License

MIT

Contributors

CopilotGeoDerp

Issues