Quick Start
Install
pip install misconfig-index
Scan a directory
Point the scanner at any directory containing Terraform, Kubernetes, CloudFormation, or Dockerfile files:
misconfig scan --path ./infra
Example output:
Scanning /home/user/infra …
────────────────────────────────────────
Misconfig Score: 76/100 (Grade B)
────────────────────────────────────────
Category breakdown:
networking ████████░░ 80/100
identity ███████░░░ 70/100
storage █████████░ 90/100
workload ███████░░░ 72/100
────────────────────────────────────────
JSON output
misconfig scan --path ./infra --output json | jq '.score'
CI / GitHub Actions
Gate every pull request on your Misconfig Score in three steps:
- Get an API key — create an org via
POST /v1/orgs, thenPOST /v1/orgs/{id}/keys. - Add
MISCONFIG_API_KEYto your repo secrets: Settings → Secrets → Actions → New repository secret. - Drop this file into your repo:
.github/workflows/misconfig-index.yml
name: Misconfig Index
on:
push:
branches: [main, master]
paths: ['**.tf', '**.yaml', '**.yml', '**/Dockerfile']
pull_request:
paths: ['**.tf', '**.yaml', '**.yml', '**/Dockerfile']
env:
MISCONFIG_API_URL: https://api.misconfig.dev
MIN_SCORE: 60
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install misconfig-index
- name: Scan IaC
env:
MISCONFIG_API_KEY: ${{ secrets.MISCONFIG_API_KEY }}
run: |
misconfig ingest \
--path . \
--repo "${{ github.repository }}" \
--branch "${{ github.ref_name }}" \
--commit "${{ github.sha }}" \
--min-score $MIN_SCORE
Exit codes: 0 success · 1 below threshold · 2 error.
MIN_SCORE: 60 (Grade C) and tighten it as your posture improves.
Score Badges
Add a live score badge to your README that updates on every push:
Markdown

Badges are grade-coloured (🟢 A/B · 🟡 C · 🔴 D/F) and cached for 5 minutes.
CLI Reference
misconfig scan
| Option | Default | Description |
|---|---|---|
--path, -p | . | Directory to scan |
--output, -o | table | table or json |
--save | off | Persist to local DB (requires DATABASE_URL) |
misconfig ingest
| Option | Env var | Description |
|---|---|---|
--path, -p | — | Directory to scan |
--repo, -r | — | e.g. github.com/org/repo |
--api-key | MISCONFIG_API_KEY | API key (prefix mi_) |
--api-url | MISCONFIG_API_URL | API base URL |
--branch | MISCONFIG_BRANCH | Git branch name |
--commit | MISCONFIG_COMMIT | Git commit SHA |
--min-score | — | Exit 1 if score falls below this |
--dry-run | off | Print payload without posting |
misconfig serve
| Option | Default | Description |
|---|---|---|
--host | 127.0.0.1 | Bind host |
--port | 8000 | Bind port |
--workers | 1 | Uvicorn workers |
--reload | off | Auto-reload (dev mode) |
REST API
Full interactive reference at api.misconfig.dev/docs. Key endpoints:
Create an organisation
curl -X POST https://api.misconfig.dev/v1/orgs \
-H "Content-Type: application/json" \
-d '{"name": "Acme", "slug": "acme"}'
Create an API key
curl -X POST https://api.misconfig.dev/v1/orgs/{id}/keys \
-H "Content-Type: application/json" \
-d '{"name": "ci-prod"}'
# Returns the full key (mi_…) — save it immediately.
Ingest a scan
curl -X POST https://api.misconfig.dev/v1/ingest \
-H "X-API-Key: mi_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"repo": "github.com/acme/infra",
"branch": "main",
"commit_sha": "abc123",
"total_files_scanned": 42,
"findings": [...]
}'
Get score history
curl https://api.misconfig.dev/v1/repos/{id}/history \
-H "X-API-Key: mi_YOUR_KEY"
Scoring Explained
Each finding is weighted by severity. Weights are then normalised using a square-root of files scanned denominator so that large repos aren't unfairly penalised compared to tiny ones:
| Severity | Weight |
|---|---|
| Critical | 10 |
| High | 5 |
| Medium | 2 |
| Low | 1 |
penalty = Σ severity_weights(findings)
score = clamp(0, 100 − (penalty / √files) × 3)
The × 3 scale factor is calibrated so that a single high-severity finding in a 10-file repo yields roughly 95/100 (grade A), while a repo with 10 critical findings in 50 files scores around 30/100 (grade F).
Category scores (Networking, Identity, Storage, etc.) use the same formula applied per-category and are displayed as the coloured bars in scan results.
| Grade | Score | Meaning |
|---|---|---|
| A | ≥ 90 | Excellent — minor or no findings |
| B | ≥ 75 | Good — a few issues to address |
| C | ≥ 60 | Fair — moderate security concerns |
| D | ≥ 40 | Poor — multiple significant findings |
| F | < 40 | Critical — severe misconfigurations |
The scoring engine is open source — see scanner/scoring.py for the full implementation.
Supported IaC
| Type | Detected by | Rule categories |
|---|---|---|
| Terraform | .tf | networking, identity, storage, database |
| Kubernetes | .yaml / .yml | workload, storage, image |
| CloudFormation | AWSTemplateFormatVersion in YAML | networking, storage |
| Dockerfile | filename Dockerfile | image |
Self-Hosting
# 1. Clone and configure
git clone https://github.com/cjb00/misconfig-index
cd misconfig-index
cp .env.example .env
# edit .env → set POSTGRES_PASSWORD, ENVIRONMENT=production
# 2. Start the full stack
docker compose up -d
# 3. Open the dashboard
open http://localhost
Environment variables
| Variable | Default | Description |
|---|---|---|
POSTGRES_PASSWORD | required | PostgreSQL password |
ENVIRONMENT | development | development or production |
API_WORKERS | 2 | Uvicorn worker count |
CORS_ORIGINS | * | Allowed origins — restrict in production |
WEB_PORT | 80 | Host port for nginx |
CORS_ORIGINS=https://yourdomain.com and terminate SSL with Let's Encrypt. See the launch post for a full walkthrough.