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.
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
| 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 |
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 |
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-minutesto prevent runaway builds - Concurrency — duplicate runs on the same PR are automatically cancelled
| 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 |
git clone https://github.com/your-org/offline-ci-scan.git
cd offline-ci-scan
cp .env.example .envEdit .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) |
docker compose up -dWait for all health checks to pass:
docker compose ps # All services should show "healthy"
docker compose logs -f github-runner # Confirm runner registered- Open
http://<host>:8080in a browser (you must be on the network). - Log in with
admin/ the password you set inDD_ADMIN_PASSWORD. - Navigate to API v2 Key and copy the token.
- Store it as a GitHub Actions secret named
DEFECTDOJO_API_KEYon the target repository (Settings → Secrets → Actions).
In the target repository go to Settings → Actions → Runners. You should
see a runner prefixed with airgap-runner in an Idle state.
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 withuses:, theruns-on: [self-hosted, linux]is already declared inside each job of the called workflow. If you write a standalone workflow instead (not usinguses:), you must addruns-on: [self-hosted, linux]to every job yourself.
- Fast Feedback — Semgrep (SAST), Lizard (complexity), MegaLinter (lint) run against the PR code.
- Deep Analysis — The repo's
Dockerfileis built and scanned by Trivy (SCA / CVE). - 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.
- 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.
| 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 |
.
├── .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
- Rotate credentials every 90 days (
GITHUB_ACCESS_TOKEN, DefectDojo API key, database passwords). - Pin and audit images — update version tags deliberately; never use
:latestin production. - Monitor resources —
docker statsand health-check logs will surface container issues early. - Prune regularly — run
docker system prune --volumeson a schedule to reclaim disk space. - Back up DefectDojo — the
defectdojo_postgres_datavolume holds all scan history; include it in your backup plan.