English · Русский
CloudGraph is an open-source, offline engine that explains how separate security problems in your infrastructure configuration can chain into a dangerous attack path. It reads Docker Compose, Kubernetes YAML and Terraform plan JSON, builds a directed graph of resources, permissions and network reachability, and searches it for routes from the Internet to sensitive targets: secrets, the host, cluster-admin rights, cloud accounts and data stores. It never connects to a cloud account or a cluster.
It does not stop at "this container is privileged". It reports "if the exposed web app is compromised, the token of its ServiceAccount can read every Secret in the cluster", with every step traced to a file and line, the conditions each step relies on, and the fix that breaks the most chains.
Note: a path is conditional. CloudGraph shows which preconditions must hold; it never claims that an application can actually be exploited. Where the input is incomplete it reports Unknown instead of guessing a connection. See SECURITY.md and the threat model.
The insecure Kubernetes demo: ten conditional paths, one highlighted in the graph, a single what-if fix that removes four of them, and a diff against the hardened version.
| Graph with an explained path | What-if fix and patch |
|---|---|
![]() |
![]() |
| Module | Does |
|---|---|
| Infrastructure Import | Docker Compose, Kubernetes YAML (multi-document, List), Terraform plan JSON from terraform show -json, including modules |
| Resource Discovery | Containers, services, networks, ports, volumes, secrets, ServiceAccounts, roles, bindings, security groups, IAM, S3, RDS |
| Graph Engine | Directed graph; every edge has a source line, a confidence level and the conditions it depends on |
| Security Rules | 41 rules for Compose, Kubernetes (including RBAC and NetworkPolicy) and AWS Terraform |
| Attack Path Engine | Routes from an Internet-reachable entry to a sensitive target, with prerequisites and a risk score |
| Risk Prioritization | Score = target impact × path confidence; shared choke points that break several paths at once |
| Remediation Studio | Source viewer with masked secrets, remediation text, a git apply-able patch |
| What-if Simulator | Applies fixes to a virtual copy, recomputes the graph and shows which paths disappear |
| Infrastructure Diff | Compares two states by relationships, findings and paths, not just by counts |
| Reporting & CI | Terminal, JSON, HTML, SARIF 2.1.0 (paths as codeFlows), exit codes for pipelines |
| Level | Meaning | Example |
|---|---|---|
| confirmed | Stated explicitly in the configuration | serviceAccountName: web-sa, a port published on 0.0.0.0 |
| inferred | Derived by evaluating semantics | a label selector match, a shared Docker network, NetworkPolicy evaluation |
| conditional | Holds only if an attacker precondition is met | using a pod's token after compromising the pod, a container escape |
| unknown | The input does not say; never traversed | an Ingress pointing to a Service that is not in the input, an ipBlock peer |
A path is reported only when it starts at an Internet-reachable entry, ends at a sensitive target and contains at most one network pivot, so the normal frontend → backend → database architecture is not reported as a risk. When several routes lead through the same resources to the same target, CloudGraph keeps the best supported one: the route whose weakest step is strongest wins, then the one with fewer unconfirmed steps, and only then the shorter one. A short route that needs an extra assumption never hides a longer route that the configuration confirms.
| Limit | Default | Flag / API field | When it is reached |
|---|---|---|---|
| Path depth | 10 edges (max 20) | --max-depth / maxDepth |
Incomplete analysis warning: some routes were not followed to the end |
| Candidate paths | 20 000 (max 200 000) | --max-paths / maxCandidates |
Incomplete analysis warning: the search stopped early |
| Network pivots per path | 1 (max 3) | --max-pivots / maxPivots |
Limited coverage note: paths that need more pivots are not in the result |
The limits and their outcome are recorded with every analysis and shown in the terminal, the HTML report, the
web UI and SARIF (invocations[].toolExecutionNotifications, plus runs[].properties.pathSearch). A what-if
simulation reuses the limits of the analysis it starts from, so it never "resolves" a path it simply did not search.
"Complete" is two questions, and the report answers both (pathSearch in JSON and SARIF):
| Field | Meaning |
|---|---|
searchComplete |
The search ran to the end: no depth or candidate limit stopped it. (complete is the old name of this field.) |
scopeLimited |
Some routes were left out by a limit: depth, candidates or network pivots. A search can be complete and still scope-limited. |
coverage |
What was analysed: resources by kind, the relationship types the search could follow, undecided relationships it never follows, and input resource types that are not modelled (unmodelledTypes). |
With the default of one pivot, a path that needs two network pivots does not appear, searchComplete is true and
scopeLimited is true. Do not read "no path found" as "none exists" unless scopeLimited is false.
--fail-incomplete exits 1 when searchComplete is false; --fail-limited exits 1 when scopeLimited is true.
Routes that start in the same place and end at the same target through the same last hop are merged into one path:
the best supported one represents the group, and the path carries similarRoutes (how many were merged). They can
rest on different preconditions, so an analyst can list them: --alternatives on the CLI, limits.showAlternatives
in the API, GET /api/analyses/{id}?alternatives=1 for a stored analysis, or Show alternative paths on the
overview. The representative path and the path count do not change; up to 10 alternatives are listed per path.
Terraform input covers a fixed AWS subset (security groups, EC2, IAM roles and policies, S3, RDS), not any plan. The
support matrix lists what is modelled and what is not; unsupported resource types are
reported in coverage.unmodelledTypes.
- Import: point the CLI at a directory, or upload files in the web UI. Helm and Kustomize must be rendered
first (
helm template,kustomize build); Terraform needsterraform show -json plan.out. - Read the paths: each one lists its steps, their confidence, the source line and the preconditions.
- Explore the graph: highlight a path, click a node or an edge to see why it exists.
- Simulate fixes: tick one or more fixes, watch the paths that disappear, export the patch.
- Compare two states (before/after a change, vulnerable vs hardened) and gate CI on new findings.
The interface has six views: Overview, Graph, Findings, What-if, Diff and Unknown. It opens in a dark theme; the sun/moon button in the top bar switches to the light one and remembers the choice. Fonts and icons are bundled, so the UI works without any Internet access.
Prefer a single binary? Download it for Linux, macOS or Windows from
Releases and run cloudgraph serve. With Docker:
git clone https://github.com/TroubieD3v/CloudGraph.git
cd CloudGraph
docker build -t cloudgraph .
docker run --rm -p 127.0.0.1:8080:8080 -v cloudgraph-data:/data cloudgraphOpen http://127.0.0.1:8080 and pick a demo project. The port is published on the loopback interface only.
History is kept in the cloudgraph-data volume; docker volume rm cloudgraph-data deletes it.
Bundled demo projects:
| Demo | Story |
|---|---|
| Vulnerable E-commerce | Compose shop: public frontend, privileged backend with docker.sock, published databases, plus an AWS plan with an open security group and an admin instance role |
| Insecure Kubernetes Cluster | Namespaces, ServiceAccounts, cluster-wide RBAC, a privileged workload behind a LoadBalancer, a database without NetworkPolicy |
| Hardened Infrastructure | The same stack, fixed: zero findings, zero paths; use it for diffs |
Troubleshooting
- Port 8080 already in use:
docker run --rm -p 127.0.0.1:8090:8080 cloudgraph, then open port 8090. - Upload says "no supported resources found": the files are not Compose, Kubernetes or a Terraform plan JSON (state files and HCL are not read). Templated manifests must be rendered first.
- A connection you expected is missing: open the Unknown view; it lists every reference the input could not resolve.
Requires Go 1.27+ and Node.js 22+.
make build # UI into internal/ui/dist/app, then bin/cloudgraph
./bin/cloudgraph serve # UI + API on 127.0.0.1:8080
cd web && npm run dev # optional: UI with hot reload on :5173 (proxies /api to :8080)Without make: npm --prefix web ci && npm --prefix web run build && go build -o bin/cloudgraph ./cmd/cloudgraph.
cloudgraph scan ./infrastructure # paths, findings, unknowns in the terminal
cloudgraph scan ./infrastructure --format sarif --output results.sarif --fail-on high
cloudgraph simulate ./k8s --list # how many paths each fix resolves
cloudgraph simulate ./k8s --all --patch fixes.patch # proposed patch, then: git apply --unidiff-zero fixes.patch
cloudgraph diff ./before ./after
cloudgraph scan demo:insecure-kubernetes --format html --output report.htmlExit codes: 0 ok, 1 a finding at or above --fail-on (or a truncated search with --fail-incomplete),
2 error. GitHub Code Scanning shows the SARIF findings and the attack paths as code flows:
- run: cloudgraph scan ./infrastructure --format sarif --output results.sarif --fail-on high
- uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: results.sarifThe scan-examples job in ci.yml runs exactly this on the hardened project, and then
scans the intentionally vulnerable projects: the gate must fail with exit code 1, and the SARIF must validate against
the SARIF 2.1.0 schema and contain the expected findings and attack paths (scripts/check_sarif.py).
- Offline: CloudGraph reads files. It never calls a cloud API, a cluster or the Internet.
- Secrets are masked at import: environment values, credentials in URLs, Terraform
sensitive_values, Kubernetes Secret data. Masking happens before anything is stored, logged, exported or shown in the source viewer. - What-if never touches your files; patches are exported for review.
- The server binds to
127.0.0.1, has no authentication by design, rejects cross-origin writes and non-JSON posts, and sends a restrictive Content Security Policy. - Untrusted configuration is fuzzed: self-referencing YAML anchors and corrupted inputs must not crash the engine.
go vet ./... && go test ./... # 64 test functions: parsers, graph semantics, rules, paths and limits, what-if, API, CLI
npm --prefix web run build
npm --prefix web run test:e2e # 14 Playwright tests; PW_CHANNEL=chrome reuses an installed Chrome
go test ./internal/analyze -run '^$' -fuzz FuzzAnalyze -fuzztime 60sEvery rule must fire on a vulnerable example, and the hardened project must produce zero findings and zero paths. Graph tests prove that missing information becomes Unknown instead of an invented edge. The exported patch is applied to the demo files and re-scanned, and the result must match the what-if prediction. A dedicated test checks that no planted secret ever appears in JSON, SARIF, HTML, terminal output, patches or the source viewer.
docs/RULES.md is generated with cloudgraph rules --format markdown; a test fails when it is stale.
web/scripts/screenshots.mjs and web/scripts/make_gif.py regenerate the screenshots in this README.
Compose / Kubernetes / Terraform plan ──► parse (lines kept, secrets masked) ──► inventory
inventory ──► graph (edges + confidence + conditions) ──► rules (findings + fixes) ──► paths (score, hotspots)
analysis ──► CLI · REST API + SQLite history · React UI (@xyflow/react) · JSON / HTML / SARIF
what-if: clone inventory ──► apply fixes ──► rerun everything ──► compare
cmd/cloudgraph/: CLI and server entry point; one static binary with the UI and demos embedded.internal/parse/: importers;internal/graph/: relationships, RBAC and NetworkPolicy semantics.internal/rules/: the rules;internal/paths/: path search and scoring.internal/analyze/: pipeline, what-if, patch export, diff;internal/report/: output formats.internal/server/,internal/store/: REST API and SQLite snapshots;internal/mask/: secret masking.web/: React + TypeScript + Vite, Playwright tests.examples/: the demo projects.
Details: docs/architecture.md · docs/threat-model.md · docs/RULES.md · docs/terraform-support.md · CONTRIBUTING.md · CHANGELOG.md.
More Terraform providers (GCP, Azure) and AWS services (EKS, Lambda, load balancers), Helm and Kustomize rendering built in, Kubernetes admission objects (PodSecurity labels), and runtime context imported from existing scanners.
| Version | Result |
|---|---|
| v0.1.0 | Compose, Kubernetes and Terraform import, 41 rules, attack paths, what-if with patch export, diff, SARIF, web UI, Docker |
| v0.2.0 | Redesigned interface: new palette and type, dark theme by default with a light theme toggle, bundled fonts and icons, restyled HTML report |
| v0.3.0 | searchComplete / scopeLimited / coverage, alternative paths, Terraform support matrix, -race in CI |
Pushing a v* tag builds static binaries for Linux, macOS and Windows with checksums
(release.yml).




