infra-guard
# infra-guard
An MCP server that scans Terraform and Dockerfiles for real security misconfigurations — open security groups, public S3 buckets, wildcard IAM policies, hardcoded secrets, containers running as root — and hands back structured findings instead of a guess.
It plugs into Claude Code, Claude Desktop, or Cursor as a tool. Ask your AI assistant to review your infrastructure code, and it calls `infra-guard`, gets back real findings from [Checkov](https://www.checkov.io/), and explains them to you.
**Try it in the browser:** [infra-guard-frontend-production.up.railway.app](https://infra-guard-frontend-production.up.railway.app) — paste Terraform, click Scan, see real findings. No install required.
**MCP endpoint:** `https://infra-guard-production.up.railway.app/mcp`
## Why this exists
I did cloud infrastructure work at A.P. Moller–Maersk — Terraform, Docker, AWS provisioning at real scale. Most portfolio projects are generic web apps; this one is the tool I actually wished existed: something that turns "does my Terraform have any obvious security holes" into a real, structured answer instead of an AI assistant's best guess.
`infra-guard` doesn't guess. It runs your file through Checkov, a real static analysis engine with hundreds of built-in checks, and returns the actual findings — check ID, title, affected resource, line range, code snippet. The hosting LLM (Claude, or whatever's on the other end of the MCP connection) explains the findings in plain English. The tool's job is just to be correct.
## How it works
```
scanner.py → core engine: scan_terraform(...) / scan_dockerfile(...) -> structured dict
server.py → wraps both as MCP tools, served over stdio or Streamable HTTP
api.py → wraps both as a plain REST API (POST /api/scan, POST /api/scan-dockerfile),
plus POST /api/explain for optional local AI remediation
frontend/ → React + Vite playground: severity badges, sort/group findings,
click a finding to scroll/highlight its lines in a CodeMirror
editor, "Explain & Fix" for AI-generated remediation, calls api.py
```
`scanner.py` shells out to the Checkov CLI, parses its JSON output, and returns the same shape regardless of which framework ran:
```json
{
"summary": { "passed": 14, "failed": 34, "total_checks": 48 },
"findings": [
{
"check_id": "CKV_AWS_24",
"title": "Ensure no security groups allow ingress from 0.0.0.0:0 to port 22",
"resource": "aws_security_group.app_sg",
"severity": "critical",
"start_line": 6,
"end_line": 24,
"code_snippet": "resource \"aws_security_group\" \"app_sg\" { ... }"
}
]
}
```
`server.py` exposes two MCP tools, `scan_terraform_file(file_content, filename)` and `scan_dockerfile_file(file_content, filename)`, with no interpretation layer of its own — the structured data goes straight to whatever LLM is hosting the session.
**A note on `severity`:** Checkov's open-source CLI always returns `severity: null` — real per-check severity only exists when a scan is connected to Bridgecrew/Prisma Cloud's paid platform (`--bc-api-key`), which means an account and sending scan content to that platform. This project doesn't do that, so `scanner.py` assigns severity locally from a small `_SEVERITY_MAP` keyed by check ID, falling back to `"info"` for anything unmapped. It's a deliberate, disclosed approximation, not a Checkov feature.
[`insecure_example.tf`](insecure_example.tf) has four intentional Terraform issues (open SSH ingress, a public+unencrypted S3 bucket, a wildcard IAM policy, a hardcoded RDS password) — 14 passed / 34 failed checks. [`insecure_example.Dockerfile`](insecure_example.Dockerfile) has five (unpinned base image, `ADD` instead of `COPY`, port 22 exposed, no `HEALTHCHECK`, runs as root) — 26 passed / 5 failed checks.
## Running it locally
Requires [uv](https://docs.astral.sh/uv/).
```bash
git clone https://github.com/SanjanaJanardhan/infra-guard.git
cd infra-guard
uv sync
```
Run the scanner directly:
```bash
uv run python3 scanner.py
```
Run the MCP server over stdio (for local clients like Claude Code/Desktop):
```bash
uv run python3 server.py
```
Run it over Streamable HTTP (for remote clients, or to reproduce the deployed setup):
```bash
uv run python3 server.py --transport streamable-http --port 8000
```
## Connecting it to an MCP client
**Claude Code / Claude Desktop** — add to `.mcp.json` (project-level) or your global MCP config:
```json
{
"mcpServers": {
"infra-guard": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/infra-guard", "run", "python3", "server.py"]
}
}
}
```
**Any Streamable HTTP client** (including the live deployment above) — point it at:
```
https://infra-guard-production.up.railway.app/mcp
```
## Running the playground locally
```bash
# terminal 1 — API
uv run python3 api.py
# terminal 2 — frontend
cd frontend
npm install
npm run dev
```
The frontend reads its API base URL from `VITE_API_URL` (see `frontend/.env.local`), defaulting to `http://localhost:8001`.
### Enabling "Explain & Fix" (optional, local-only)
Clicking a finding shows an "Explain & Fix" button that calls Claude (Haiku) to generate a plain-English explanation and a suggested fixed code snippet, grounded in that finding's real `check_id`/`resource`/`code_snippet`.
This deliberately **isn't enabled on the public deployment** — the playground is public and unauthenticated, so wiring a paid API behind a button there means anyone's clicks spend your API credits. Instead, `api.py` looks for `ANTHROPIC_API_KEY` in the environment (via `.env`, loaded with `python-dotenv`) and returns `{"configured": false}` if it's missing, which the frontend renders as a plain "not enabled" message rather than a broken button.
To try it locally:
```bash
cp .env.example .env
# edit .env and add your own key from https://console.anthropic.com/
uv run python3 api.py
```
`.env` is gitignored — never commit a real key, and never set `ANTHROPIC_API_KEY` on the Railway API service.
## Deployment
Three services on [Railway](https://railway.app), all built from Docker/Nixpacks with no manual server config:
- **MCP server** — `Dockerfile`, Streamable HTTP
- **REST API** — `Dockerfile.api`, same `scanner.py` core, powers the playground
- **Frontend** — Railway's Nixpacks builder auto-detects the Vite app in `frontend/`; `VITE_API_URL` is set at build time to the deployed API's URL
Both Python services read `PORT` from the environment, so they adapt to whatever port Railway assigns with no config changes.
## Stack
Python · [Checkov](https://www.checkov.io/) · [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) · FastAPI · React · Vite · [uv](https://docs.astral.sh/uv/) · Docker · Railway
## Roadmap
- [x] Core Terraform scanning engine
- [x] MCP server over stdio
- [x] Streamable HTTP transport
- [x] Deployed to Railway
- [x] Web frontend with a live playground
- [x] Dockerfile scanning, including a Terraform/Dockerfile toggle in the playground
- [x] AI-generated remediation ("Explain & Fix"), local-only by design
- [ ] Cost-impact estimate for findings
## License
MIT
TDQS
Scored across 2 tools
The two tools are cleanly separated by file type: one handles Terraform files and the other handles Dockerfiles. There is no functional overlap or realistic risk of selecting the wrong tool.
Both tools follow the same scan_<format>_file pattern, and the descriptions mirror each other for symmetry. Naming is predictable and internally consistent.
Two tools is a thin set: they cover exactly Terraform and Dockerfile scanning, but the server named infra-guard feels narrow. The count is coherent but borderline for the apparent purpose.
For the supported file types, the scanning workflow is functional, but common IaC targets such as Kubernetes manifests or CloudFormation templates are absent. There is also no batch or exception-handling capability, leaving notable coverage gaps.