cloudmap 1.0.0rc1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cloudmap-1.0.0rc1/.github/workflows/ci.yml +26 -0
- cloudmap-1.0.0rc1/.github/workflows/publish.yml +26 -0
- cloudmap-1.0.0rc1/.gitignore +19 -0
- cloudmap-1.0.0rc1/ARCHITECTURE.md +158 -0
- cloudmap-1.0.0rc1/FORMAT.md +89 -0
- cloudmap-1.0.0rc1/LICENSE +21 -0
- cloudmap-1.0.0rc1/PKG-INFO +291 -0
- cloudmap-1.0.0rc1/PLAN.md +117 -0
- cloudmap-1.0.0rc1/README.md +275 -0
- cloudmap-1.0.0rc1/cloudmap/__init__.py +3 -0
- cloudmap-1.0.0rc1/cloudmap/__main__.py +6 -0
- cloudmap-1.0.0rc1/cloudmap/adapters/__init__.py +91 -0
- cloudmap-1.0.0rc1/cloudmap/ask/__init__.py +111 -0
- cloudmap-1.0.0rc1/cloudmap/ask/intent.py +133 -0
- cloudmap-1.0.0rc1/cloudmap/ask/narration.py +54 -0
- cloudmap-1.0.0rc1/cloudmap/ask/queries.py +296 -0
- cloudmap-1.0.0rc1/cloudmap/cli.py +534 -0
- cloudmap-1.0.0rc1/cloudmap/extract/__init__.py +0 -0
- cloudmap-1.0.0rc1/cloudmap/extract/extractors.py +653 -0
- cloudmap-1.0.0rc1/cloudmap/extract/llm.py +89 -0
- cloudmap-1.0.0rc1/cloudmap/graph.py +208 -0
- cloudmap-1.0.0rc1/cloudmap/ingest/__init__.py +0 -0
- cloudmap-1.0.0rc1/cloudmap/ingest/azure.py +380 -0
- cloudmap-1.0.0rc1/cloudmap/ingest/fixture.py +15 -0
- cloudmap-1.0.0rc1/cloudmap/interactive.py +227 -0
- cloudmap-1.0.0rc1/cloudmap/local_model.py +49 -0
- cloudmap-1.0.0rc1/cloudmap/model.py +43 -0
- cloudmap-1.0.0rc1/cloudmap/render/__init__.py +0 -0
- cloudmap-1.0.0rc1/cloudmap/render/azure_icons.py +72 -0
- cloudmap-1.0.0rc1/cloudmap/render/csv_export.py +47 -0
- cloudmap-1.0.0rc1/cloudmap/render/drawio.py +149 -0
- cloudmap-1.0.0rc1/cloudmap/render/html.py +579 -0
- cloudmap-1.0.0rc1/cloudmap/render/json_out.py +52 -0
- cloudmap-1.0.0rc1/cloudmap/render/mermaid.py +25 -0
- cloudmap-1.0.0rc1/cloudmap/scrub.py +300 -0
- cloudmap-1.0.0rc1/estate-viewer.png +0 -0
- cloudmap-1.0.0rc1/fixtures/acme_orders.json +87 -0
- cloudmap-1.0.0rc1/fixtures/contoso.json +216 -0
- cloudmap-1.0.0rc1/fixtures/estate.json +373 -0
- cloudmap-1.0.0rc1/pyproject.toml +31 -0
- cloudmap-1.0.0rc1/tests/test_adapters.py +51 -0
- cloudmap-1.0.0rc1/tests/test_arg_rows.py +188 -0
- cloudmap-1.0.0rc1/tests/test_ask.py +372 -0
- cloudmap-1.0.0rc1/tests/test_azure.py +18 -0
- cloudmap-1.0.0rc1/tests/test_cli_exports.py +227 -0
- cloudmap-1.0.0rc1/tests/test_containerapps.py +161 -0
- cloudmap-1.0.0rc1/tests/test_drawio_xml.py +137 -0
- cloudmap-1.0.0rc1/tests/test_enrich.py +95 -0
- cloudmap-1.0.0rc1/tests/test_estate.py +39 -0
- cloudmap-1.0.0rc1/tests/test_fixtures_safe.py +70 -0
- cloudmap-1.0.0rc1/tests/test_golden_orders.py +80 -0
- cloudmap-1.0.0rc1/tests/test_graph.py +53 -0
- cloudmap-1.0.0rc1/tests/test_html.py +40 -0
- cloudmap-1.0.0rc1/tests/test_ingest_paging.py +274 -0
- cloudmap-1.0.0rc1/tests/test_interactive_wizard.py +349 -0
- cloudmap-1.0.0rc1/tests/test_llm.py +46 -0
- cloudmap-1.0.0rc1/tests/test_scrub.py +184 -0
- cloudmap-1.0.0rc1/tests/test_trust.py +319 -0
- cloudmap-1.0.0rc1/uv.lock +339 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
# 3.9 is the floor the README promises; 3.13 catches what is about to break.
|
|
15
|
+
python-version: ["3.9", "3.13"]
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
- run: pip install -e ".[dev]"
|
|
22
|
+
- run: ruff check .
|
|
23
|
+
- run: pytest
|
|
24
|
+
# The suite includes tests/test_fixtures_safe.py, which fails if a fixture
|
|
25
|
+
# ever carries a credential or an unscrubbed GUID. That guard is the reason
|
|
26
|
+
# CI runs on pull requests too.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
pypi-publish:
|
|
9
|
+
name: Upload release to PyPI
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment:
|
|
12
|
+
name: pypi
|
|
13
|
+
url: https://pypi.org/p/cloudmap
|
|
14
|
+
permissions:
|
|
15
|
+
id-token: write
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
- name: Build package
|
|
22
|
+
run: |
|
|
23
|
+
pip install build
|
|
24
|
+
python -m build
|
|
25
|
+
- name: Publish package distributions to PyPI
|
|
26
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.pyc
|
|
3
|
+
.venv/
|
|
4
|
+
venv/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.ruff_cache/
|
|
10
|
+
|
|
11
|
+
# generated diagram output
|
|
12
|
+
*.blast.drawio
|
|
13
|
+
*.mmd
|
|
14
|
+
out/
|
|
15
|
+
|
|
16
|
+
# never commit anything pulled from a live cloud
|
|
17
|
+
live/
|
|
18
|
+
*.live.json
|
|
19
|
+
.team/
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
> Written in English to match the rest of the repo (README, code, comments).
|
|
4
|
+
> `file.py:NN` references are indicative - they drift as the code moves. Trust the
|
|
5
|
+
> function and file names; grep for the symbol rather than jumping to the line.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
cloudmap is a local-first CLI (Python 3.9+, zero dependencies — `pyproject.toml:14`)
|
|
10
|
+
that takes the **name of one Azure resource** and produces its full **dependency
|
|
11
|
+
graph (blast radius)** as an **editable draw.io diagram** (plus Mermaid and JSON).
|
|
12
|
+
It is a clean four-stage pipeline: **ingest** (fixture or live `az`) → **extract**
|
|
13
|
+
(properties / hostnames / secrets → typed edges) → **graph** (blast-radius BFS with
|
|
14
|
+
hub boundaries) → **render**. The core value is that Azure Resource Graph has no
|
|
15
|
+
"dependencies" table, so `extract/extractors.py` infers dependencies and
|
|
16
|
+
**verifies** each one — anything referenced but not verified becomes an explicit
|
|
17
|
+
`external` node instead of being silently dropped.
|
|
18
|
+
|
|
19
|
+
## Architecture
|
|
20
|
+
|
|
21
|
+
```mermaid
|
|
22
|
+
flowchart TD
|
|
23
|
+
CLI["__main__.py → cli.main()<br/>argparse: trace"] --> TRACE["_cmd_trace (cli.py:49)"]
|
|
24
|
+
|
|
25
|
+
TRACE -->|"--from"| FIX["ingest/fixture.py<br/>load_fixture()"]
|
|
26
|
+
TRACE -->|"--live"| AZ["ingest/azure.py<br/>query_live() :106"]
|
|
27
|
+
|
|
28
|
+
AZ --> GUARD["_guard() :50<br/>ALLOW_SUB pin"]
|
|
29
|
+
AZ --> SUBS["_target_subscriptions() :74<br/>tenant-wide"]
|
|
30
|
+
AZ --> PAGE["_graph_paged() :88<br/>type-filtered KQL, skip_token"]
|
|
31
|
+
|
|
32
|
+
FIX --> BUILD["graph.build_graph() :34"]
|
|
33
|
+
AZ --> BUILD
|
|
34
|
+
BUILD --> EXTRACT["extract_edges() :182<br/>Resolver + typed edges"]
|
|
35
|
+
|
|
36
|
+
TRACE --> ENRICH["(live, web app)<br/>enrich_webapp() azure.py:144<br/>show+appsettings+RBAC+diag<br/>+ secret resolution :113"]
|
|
37
|
+
ENRICH --> EXTERNAL["seed_external_dependencies() :279<br/>never-drop → external nodes"]
|
|
38
|
+
|
|
39
|
+
BUILD --> BLAST["blast_radius() graph.py<br/>BFS with direction consistency"]
|
|
40
|
+
EXTERNAL --> BLAST
|
|
41
|
+
BLAST --> R1["render/drawio.py :44<br/>native Azure icons"]
|
|
42
|
+
BLAST --> R2["render/mermaid.py"]
|
|
43
|
+
BLAST --> R3["render/json_out.py"]
|
|
44
|
+
BLAST --> R4["render/html.py<br/>self-contained viewer"]
|
|
45
|
+
|
|
46
|
+
CLI --> ASK["_cmd_ask (cli.py)<br/>argparse: ask"]
|
|
47
|
+
R3 -.->|"saved map, reloaded"| LOAD["adapters/load_graph()<br/>auto-detect + keep meta"]
|
|
48
|
+
ASK --> LOAD
|
|
49
|
+
LOAD --> INTENT["ask/intent.py<br/>rules first, model only as<br/>validated fallback (--llm)"]
|
|
50
|
+
INTENT --> Q["ask/queries.py<br/>impact / depends / paths /<br/>shared / guesses / summary<br/>COMPUTED from edges"]
|
|
51
|
+
Q --> NARR["ask/narration.py (--explain)<br/>prose from the computed facts"]
|
|
52
|
+
INTENT -.-> LM["local_model.py<br/>the only model call, localhost"]
|
|
53
|
+
NARR -.-> LM
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## File Map
|
|
57
|
+
|
|
58
|
+
| File | Role | Why it's built this way |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `cloudmap/model.py` | `Node`/`Edge`/`Graph` dataclasses | Provider-neutral core. The `external`+`note` fields (`model.py:31-32`) exist for the "never silent drop" principle — a referenced-but-unverified target still needs a place in the model. |
|
|
61
|
+
| `cloudmap/ingest/fixture.py` | Load synthetic/captured JSON | Accepts both a bare list and `{"data":[...]}` (`:11-13`) so the same file works as a fixture and as captured `az graph` output. |
|
|
62
|
+
| `cloudmap/ingest/azure.py` | Live `az` ingest + enrichment | The most sensitive file, so it concentrates every guard (`HARD_DENY_HINTS` :20, `_guard` :50). Kept separate from extraction so the dependency logic stays pure and testable without Azure. |
|
|
63
|
+
| `cloudmap/extract/extractors.py` | **The heart**: properties/hosts/secrets → edges | The project's IP. `Resolver` (:97) builds indexes (by_id/by_host/by_principal/kv_by_name/…); `_DOMAIN_KIND` (:27) maps service domains to edge kinds. |
|
|
64
|
+
| `cloudmap/graph.py` | Build graph + blast-radius BFS + the high-level collapse | Direction consistency in `blast_radius` (never reverse once you have stepped) is why a shared VNet/ASP does not pull in unrelated apps — a central design choice. `collapse_high_level` folds instances into one box per type and must carry their kinds AND evidence across, or the default view would show unfalsifiable arrows. |
|
|
65
|
+
| `cloudmap/render/drawio.py` | `.drawio` with Azure2 icons | `AZURE_ICON` (:14) = verified paths; falls back to a box, external nodes render dashed (`:64`) so an icon is never broken. |
|
|
66
|
+
| `cloudmap/render/{mermaid,json_out}.py` | Secondary outputs | Quick preview + machine-readable inventory. |
|
|
67
|
+
| `cloudmap/ask/queries.py` | **The Ask layer's heart**: the six queries, each computed by traversal | An answer must be auditable, so it is derived from edges, never generated. `_trust()` grades a whole path by its weakest hop, and distinguishes *passing through* an unverified node (whole finding becomes a guess) from *ending* at one (the reference is proven; the target is flagged). |
|
|
68
|
+
| `cloudmap/ask/intent.py` | Question → one query | Rules first so the common phrasings need no model at all (including the "rotate / restart / decommission" verbs). The model is a fallback that may only name a query from a fixed list and a resource, both validated against the graph — it routes, it never answers. |
|
|
69
|
+
| `cloudmap/ask/narration.py` | Optional prose (`--explain`) | Handed the computed facts only, and printed *below* them, so drifting prose is visibly a narration disagreeing with the facts, not a wrong answer. |
|
|
70
|
+
| `cloudmap/local_model.py` | The single outbound model call | One module = one auditable promise: the call goes to localhost (ollama) and failure returns an empty value, because cloudmap must be fully useful with no model installed. |
|
|
71
|
+
| `cloudmap/cli.py` | Orchestration + argparse | `_cmd_trace` wires the trace stages together (live enrichment + external merge live here); `_cmd_ask` loads a saved map and prints the computed answer, proof lines included. |
|
|
72
|
+
| `fixtures/contoso.json` | 100% synthetic estate | Fixture-first development → zero cloud contact in tests. |
|
|
73
|
+
| `tests/test_graph.py` | 5 unit tests | Lock in the hub-boundary behaviour + edge-kinds against the fixture. |
|
|
74
|
+
| `.gitignore` | Safety | `live/`, `*.blast.drawio` ignored → nothing from a real cloud ends up in the repo. |
|
|
75
|
+
|
|
76
|
+
## Execution Flow
|
|
77
|
+
|
|
78
|
+
**Fixture path** (`cloudmap trace contoso-web --from fixtures/contoso.json`):
|
|
79
|
+
|
|
80
|
+
1. `__main__.py:6` → `sys.exit(main())`.
|
|
81
|
+
2. `cli.py:15` `main()` — argparse defines the `trace` subcommand and flags
|
|
82
|
+
(`--from/--live/--resolve-secrets/--single-sub/--direction`).
|
|
83
|
+
3. `cli.py:49` `_cmd_trace` — fixture branch → `load_fixture()` (`fixture.py:9`)
|
|
84
|
+
returns a list of resource dicts.
|
|
85
|
+
4. `graph.py:34` `build_graph()` → builds `Node`s and calls `extract_edges()`.
|
|
86
|
+
5. `extract_edges()` — constructs a `Resolver` and, per node, derives edges:
|
|
87
|
+
`serverFarmId`→hosted-on, `virtualNetworkSubnetId`→vnet-integration, private
|
|
88
|
+
endpoints, role assignments, and for config-bearing workloads →
|
|
89
|
+
`_config_edges()` matching hostnames/vault refs/IK. Web apps add the
|
|
90
|
+
`linuxFxVersion` image; container apps add `environmentId`, `registries`,
|
|
91
|
+
`secrets[].keyVaultUrl` and `template.containers[].image`.
|
|
92
|
+
6. `find_seeds()` — exact name match, else substring; >1 → ambiguity exit.
|
|
93
|
+
7. `blast_radius()` — BFS from the seed in both directions, but **never reversing**
|
|
94
|
+
once it has stepped one way. That single rule is what stops a shared plan or
|
|
95
|
+
VNet from bridging the seed into unrelated apps.
|
|
96
|
+
8. `to_drawio()` — layered layout by hop-distance, Azure icon or box/dashed;
|
|
97
|
+
`_print_summary` prints the edges, plus any blind spot the scan left.
|
|
98
|
+
|
|
99
|
+
**Live path** (`--live --allow-live --resolve-secrets`) — additionally in `_cmd_trace`:
|
|
100
|
+
|
|
101
|
+
9. `query_live()` → `_guard()` (`CLOUDMAP_ALLOW_SUBSCRIPTION` ==
|
|
102
|
+
active sub) → `_target_subscriptions()` (all Enabled) → `_graph_paged()`
|
|
103
|
+
(type-filtered KQL, `skip_token` paging, warns when it hits the cap).
|
|
104
|
+
10. `_enrich_live()` (`cli.py`) picks which web apps to deep-enrich via
|
|
105
|
+
`_enrichment_targets()`, then `enrich_webapps()` runs them concurrently:
|
|
106
|
+
`az webapp show` (identity/vnet/image), appsettings/connection-strings; with
|
|
107
|
+
`--resolve-secrets`, `_maybe_resolve()` → `_resolve_secret()` substitutes
|
|
108
|
+
`@Microsoft.KeyVault(...)` **in-memory**; + role assignments + diagnostics.
|
|
109
|
+
11. Rebuild the graph, then `seed_external_dependencies()` adds dashed external
|
|
110
|
+
nodes for anything the seed references but the scan never resolved, plus
|
|
111
|
+
diagnostics edges, then `_dedupe`. Whatever was *not* enriched is recorded as
|
|
112
|
+
a blind spot in `meta` so the artifact — and every `ask` answer drawn from it —
|
|
113
|
+
repeats it.
|
|
114
|
+
|
|
115
|
+
## Design Decisions
|
|
116
|
+
|
|
117
|
+
- **extract vs ingest separation** (`extractors.py` knows nothing about `az`). All
|
|
118
|
+
dependency logic is pure Python over dicts, so the 5 tests run without Azure.
|
|
119
|
+
- **Direction consistency** (`blast_radius`). Real-world testing showed shared ASP/VNet
|
|
120
|
+
connect dozens of unrelated apps. The rule "from the seed go both ways, but never
|
|
121
|
+
reverse afterwards" solves it without a list of which types count as hubs. The most
|
|
122
|
+
opinionated part, and correctly a single line.
|
|
123
|
+
- **Enrichment is scoped, and the gap is declared** (`_enrichment_targets`). Config-level
|
|
124
|
+
edges only exist for apps that were deep-enriched, so enriching just the seed makes
|
|
125
|
+
the graph asymmetric. `auto` enriches every app when the seed is shared infrastructure
|
|
126
|
+
(the only way to learn its dependents) and just the seed when the seed is itself an
|
|
127
|
+
app. Whatever is skipped becomes a `blind_spot` in `meta` — an empty upward answer
|
|
128
|
+
must never be mistaken for "nothing depends on this".
|
|
129
|
+
- **Scrub preserves structure, not identity** (`scrub.py`). The substitution is global
|
|
130
|
+
and consistent so a reference and its target stay correlated; `tests/test_scrub.py`
|
|
131
|
+
asserts the graph shape is byte-identical before and after, which is what makes a
|
|
132
|
+
committed real capture worth anything.
|
|
133
|
+
- **Never silent drop** (`extractors.py:279` + `model.py:31`). `extract_edges` drops
|
|
134
|
+
unresolved targets (noise at tenant scale), but the seed-scoped pass resurfaces them
|
|
135
|
+
as external — a clean separation of responsibility across the two functions.
|
|
136
|
+
- **Security-by-design in ingest**: `_guard()` (`azure.py:50`) demands an explicit env
|
|
137
|
+
var equal to the exact active sub id. Secrets are resolved in-memory only (`_resolve_secret` :113) and `.gitignore`
|
|
138
|
+
keeps `live/` out of the repo.
|
|
139
|
+
- **Verified icon paths** (`drawio.py:14`) — image shapes over mxgraph stencils, likely
|
|
140
|
+
because the azure2 SVGs ship inside draw.io (the repo ships no icon assets).
|
|
141
|
+
|
|
142
|
+
## Open Questions / Risks
|
|
143
|
+
|
|
144
|
+
- **`az graph --skip-token`** (`azure.py:98`): works here but depends on the
|
|
145
|
+
resource-graph extension version; another version may require a `--skip` fallback.
|
|
146
|
+
- **Deep-enrich only for `microsoft.web/sites`**: AKS or SQL as a seed still get ARM
|
|
147
|
+
topology only. Container apps need no enrichment (Resource Graph returns their
|
|
148
|
+
template), but an AKS workload's real dependencies live in Kubernetes manifests,
|
|
149
|
+
which nothing here reads.
|
|
150
|
+
- **Cost of `--enrich all`**: one `az` round-trip per app, eight at a time. On a
|
|
151
|
+
tenant with hundreds of apps the default `auto` keeps this off the common path, but
|
|
152
|
+
tracing a shared Key Vault in a large tenant is genuinely slow.
|
|
153
|
+
- **No icons for Container Apps**: `AZURE_ICON` / `AZURE_SVG` entries are only added
|
|
154
|
+
once the asset path is verified against the azure2 set, so container apps currently
|
|
155
|
+
render as a labelled box rather than a wrong icon.
|
|
156
|
+
- **Secret resolution reachability**: `_resolve_secret` calls `az keyvault secret show`;
|
|
157
|
+
if the vault is behind a private endpoint without connectivity it fails silently
|
|
158
|
+
(try/except) and the data-plane dependency shows as external rather than resolved.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# The neutral graph format
|
|
2
|
+
|
|
3
|
+
cloudmap's core does not know about any specific cloud. Everything flows through
|
|
4
|
+
one **neutral** shape: `Node` / `Edge` / `Graph` (see `cloudmap/model.py`). An
|
|
5
|
+
*adapter* turns a raw cloud export into this shape; every renderer reads it.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
raw export --(adapter)--> neutral Graph --(renderer)--> drawio | mermaid | json
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Adding a cloud = adding one adapter. That is the whole anti-vendor-lock story:
|
|
12
|
+
the value (graph, blast-radius, trust, AI) lives in the cloud-agnostic core.
|
|
13
|
+
|
|
14
|
+
## Inputs cloudmap can read (`--from`, auto-detected)
|
|
15
|
+
|
|
16
|
+
The loader (`cloudmap/adapters/load_graph`) sniffs the file - no flags:
|
|
17
|
+
|
|
18
|
+
1. **Raw Azure Resource Graph** - a JSON list of resources, or the `az`
|
|
19
|
+
shape `{"data": [ ... ]}`. Each item has `id`, `type`, `properties`, etc.
|
|
20
|
+
Handled by the **Azure adapter**, which runs the extraction rules.
|
|
21
|
+
2. **A neutral cloudmap graph** - a file cloudmap itself wrote with `--json`
|
|
22
|
+
(has top-level `nodes` and `edges`). Loaded straight back into a `Graph`, no
|
|
23
|
+
re-extraction. This is what makes a saved map re-openable (the viewer and
|
|
24
|
+
`cloudmap ask` build on it).
|
|
25
|
+
|
|
26
|
+
`seed` and `meta` survive the round-trip too (`Graph.meta`), which is what lets a
|
|
27
|
+
question answered from a saved map carry the same caveats the map itself carries -
|
|
28
|
+
an incomplete artifact cannot quietly become a confident answer.
|
|
29
|
+
|
|
30
|
+
## Output shape (`--json`)
|
|
31
|
+
|
|
32
|
+
```jsonc
|
|
33
|
+
{
|
|
34
|
+
"seed": "<node id the trace started from>",
|
|
35
|
+
"meta": {
|
|
36
|
+
"complete": true, // false if truncated OR a live read failed
|
|
37
|
+
"truncated": false, // scan hit the pagination cap -> missing data
|
|
38
|
+
"read_gaps": [], // human-readable list of things we could not read
|
|
39
|
+
"external_unverified": 0, // nodes referenced but not found in scanned scope
|
|
40
|
+
"model_edges": 0 // edges proposed by the LLM (guesses), not rules
|
|
41
|
+
},
|
|
42
|
+
"nodes": [
|
|
43
|
+
{
|
|
44
|
+
"id": "<stable id>", // ARM id for Azure; group key at high level
|
|
45
|
+
"name": "webapp-orders-dev",
|
|
46
|
+
"type": "microsoft.web/sites",
|
|
47
|
+
"resourceGroup": "rg-orders-dev",
|
|
48
|
+
"location": "westeurope",
|
|
49
|
+
"hops": 0, // distance from the seed
|
|
50
|
+
"external": false, // true = referenced but unverified (dashed box)
|
|
51
|
+
"note": "" // why it is external / how it was discovered
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"edges": [
|
|
55
|
+
{
|
|
56
|
+
"source": "<node id>",
|
|
57
|
+
"target": "<node id>",
|
|
58
|
+
"kind": "hosted-on", // relationship label(s), "; "-joined if merged
|
|
59
|
+
"origin": "extracted", // "extracted" = verified by a rule | "model" = LLM guess
|
|
60
|
+
"evidence": "properties.serverFarmId" // the proof behind this edge
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Trust fields (the point of the format)
|
|
67
|
+
|
|
68
|
+
- **`origin`** - the single most important field. `extracted` means a
|
|
69
|
+
deterministic rule found concrete proof. `model` means the local LLM proposed
|
|
70
|
+
it; renderers draw it dashed so a guess never looks like a fact. A model edge
|
|
71
|
+
can only ADD a new target - it never overrides an extracted edge.
|
|
72
|
+
- **`evidence`** - *why* the edge exists (which property / setting / rule). Lets a
|
|
73
|
+
reviewer audit the map instead of trusting it.
|
|
74
|
+
- **`meta.complete`** - the artifact admits when it is partial (truncated scan or
|
|
75
|
+
a read that failed for lack of permission).
|
|
76
|
+
|
|
77
|
+
## The adapter contract
|
|
78
|
+
|
|
79
|
+
An adapter is anything that turns raw input into a `Graph`:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
raw (list | dict) -> cloudmap.model.Graph
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- `adapters/` — `AzureAdapter` (raw Azure Resource Graph → Graph, via the
|
|
86
|
+
extraction rules) and the neutral loader (cloudmap graph JSON → Graph).
|
|
87
|
+
- The Azure specifics live in `ingest/azure.py` (live pull) and
|
|
88
|
+
`extract/extractors.py` (property rules). A future `TerraformAdapter` /
|
|
89
|
+
`AwsAdapter` slots in beside them without touching the core.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thanasis Katsaounis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cloudmap
|
|
3
|
+
Version: 1.0.0rc1
|
|
4
|
+
Summary: Trace the blast radius of your Azure resources with style.
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
7
|
+
Classifier: Environment :: Console
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Requires-Dist: questionary>=2.0.0
|
|
14
|
+
Requires-Dist: rich>=13.0.0
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# cloudmap
|
|
18
|
+
|
|
19
|
+
[](https://github.com/KatsaounisThanasis/cloudmap/actions/workflows/ci.yml) [](https://badge.fury.io/py/cloudmap)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
Give it the name of one Azure resource. Get back its **full dependency graph**
|
|
23
|
+
(the blast radius) as an **editable draw.io diagram with native Azure icons** -
|
|
24
|
+
plus Mermaid and JSON.
|
|
25
|
+
|
|
26
|
+
Azure Resource Graph has no "dependencies" table. The relationships that matter
|
|
27
|
+
- what an App Service is hosted on, which Key Vault it reads, which subnet it
|
|
28
|
+
integrates with, which managed identity has which role where, what a Private
|
|
29
|
+
Endpoint fronts, which App Gateway routes to it - are buried inside each
|
|
30
|
+
resource's `properties`. cloudmap reads them out, correlates them into one
|
|
31
|
+
graph, and draws it.
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
cloudmap trace contoso-web --from fixtures/contoso.json -o contoso-web.drawio
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
Seed: contoso-web (microsoft.web/sites)
|
|
39
|
+
Blast radius: 9 resources (0 external/unverified), 9 dependencies
|
|
40
|
+
draw.io: contoso-web.drawio
|
|
41
|
+
|
|
42
|
+
contoso-web --hosted-on--> App Service Plan
|
|
43
|
+
contoso-web --vnet-integration--> Virtual Network
|
|
44
|
+
contoso-web --reads-secret; role: Key Vault Secrets User--> Key Vault
|
|
45
|
+
contoso-web --connects-to--> SQL Server
|
|
46
|
+
contoso-web --connects-to--> Storage
|
|
47
|
+
contoso-web --sends-telemetry--> App Insights
|
|
48
|
+
App Insights --uses-workspace--> Log Analytics
|
|
49
|
+
App Gateway --in-subnet--> Virtual Network
|
|
50
|
+
App Gateway --routes-to--> contoso-web
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
And here is the rich dependency tree generated directly in your terminal:
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
Blast radius: 15 resources (0 external), 14 dependencies
|
|
57
|
+
╭────────────────────────────── Dependency Graph ──────────────────────────────╮
|
|
58
|
+
│ 🌐 app-spa-frontend │
|
|
59
|
+
│ ├── --calls--> 🌐 app-auth-service │
|
|
60
|
+
│ │ ├── --reads-secret--> 🔐 kv-core-prod │
|
|
61
|
+
│ │ └── --connects-to--> 🗄️ cosmos-auth │
|
|
62
|
+
│ └── --calls--> 🌐 app-api-gateway │
|
|
63
|
+
│ ├── --calls--> 📦 capp-payment-service │
|
|
64
|
+
│ │ └── --reads-secret--> 🔐 kv-payments-prod │
|
|
65
|
+
│ ├── --calls--> 🌐 app-inventory-api │
|
|
66
|
+
│ │ ├── --connects-to--> 🗄️ pg-inventory-prod │
|
|
67
|
+
│ │ └── --connects-to--> 📦 stinventoryprod │
|
|
68
|
+
│ ├── --connects-to--> 🗄️ redis-gateway │
|
|
69
|
+
│ └── --calls--> 🌐 app-orders-api │
|
|
70
|
+
│ ├── --connects-to--> 🗄️ redis-orders │
|
|
71
|
+
│ ├── --connects-to--> 📦 sb-enterprise │
|
|
72
|
+
│ └── --connects-to--> 🗄️ sql-orders-prod │
|
|
73
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
(That is the default **high-level** view - resources grouped by type. Add
|
|
77
|
+
`--level detail` to see every instance with its real name.)
|
|
78
|
+
|
|
79
|
+
Open the `.drawio` file in [draw.io](https://app.diagrams.net) / the desktop app
|
|
80
|
+
/ the VS Code extension and edit it like any hand-drawn diagram - or open the
|
|
81
|
+
self-contained `--html` viewer straight from disk (no server, no install) and
|
|
82
|
+
click a resource to focus its blast radius:
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
### Interactive HTML Viewer 🎨
|
|
86
|
+
The `--html` output generates a **single, self-contained HTML file** (no server, no CDN, no internet required) that includes:
|
|
87
|
+
- **Dark Mode Toggle** for better readability.
|
|
88
|
+
- **Color-Coded Edges** by relationship type (e.g., 🟡 *Security*, 🟢 *Data*, 🔵 *Network*).
|
|
89
|
+
- **Export to SVG / PNG** buttons for high-resolution snapshots.
|
|
90
|
+
- **Resource Group Filters** & Smart Search.
|
|
91
|
+
- **Direct Azure Portal Links** to jump straight to the resource in your cloud.
|
|
92
|
+
|
|
93
|
+

|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
## Why
|
|
97
|
+
|
|
98
|
+
- **Live-cloud tools upload your data.** cloudmap runs locally and reads only
|
|
99
|
+
what you point it at. Nothing leaves your machine.
|
|
100
|
+
- **Existing OSS is siloed** - Terraform-only or Kubernetes-only. cloudmap works
|
|
101
|
+
from Azure's own inventory (Resource Graph) and correlates across services.
|
|
102
|
+
- **Impact analysis, onboarding, change reviews.** "What breaks if I touch this?"
|
|
103
|
+
in one diagram instead of ten portal blades.
|
|
104
|
+
|
|
105
|
+
## Real-World Scenarios (Why you need this)
|
|
106
|
+
|
|
107
|
+
### 1. The "Safe to Delete?" Scenario (FinOps / Cloud Cost Cleanup)
|
|
108
|
+
A developer spots an expensive Azure SQL Database (`sql-orders-dev`) that looks orphaned in the portal. They want to delete it to save $500/month.
|
|
109
|
+
By running `cloudmap trace sql-orders-dev --direction up`, the tool deep-enriches the connection strings of all Web Apps in the subscription. The map instantly reveals that a *production* Web App is mistakenly pointing to this dev database! The engineer just avoided a catastrophic outage.
|
|
110
|
+
|
|
111
|
+
### 2. The "Incident Response / Root Cause" Scenario (SRE)
|
|
112
|
+
At 3:00 AM, alerts fire because `payment-api` (an AKS cluster) is failing. The team is blind.
|
|
113
|
+
Running the CloudMap interactive wizard on `payment-api` generates an HTML dependency graph in 5 seconds. It shows the cluster depends on a Key Vault (`kv-pay`). Checking the vault reveals someone changed its firewall rules 10 minutes ago. Cloudmap provides the exact *Evidence* ("found in Kubernetes secret X"), identifying the root cause instantly.
|
|
114
|
+
|
|
115
|
+
### 3. The "Compliance & Auditor Review" Scenario (Security)
|
|
116
|
+
An auditor asks: *"Which systems have access to the Storage Account containing PII customer data?"*
|
|
117
|
+
Instead of manually clicking through 50 IAM screens in the Azure Portal, you run `cloudmap trace pii-storage --direction up --csv pii-audit.csv`. In seconds, you hand the auditor a clean spreadsheet showing exactly which Web Apps and AKS clusters have Managed Identity RBAC access to the storage, complete with the exact Role Assignments as proof.
|
|
118
|
+
|
|
119
|
+
## How it works
|
|
120
|
+
|
|
121
|
+
1. **Ingest** - a JSON fixture (default) or live `az graph query` (opt-in, guarded).
|
|
122
|
+
2. **Extract** - per-type rules turn `properties` into typed edges
|
|
123
|
+
(`hosted-on`, `reads-secret`, `private-link-to`, `role: ...`, `routes-to`, ...).
|
|
124
|
+
3. **Blast radius** - walk the graph from your seed with *direction consistency*:
|
|
125
|
+
from the seed it goes both ways (what it depends on **and** what depends on
|
|
126
|
+
it), but once it steps in one direction it never reverses. That single rule
|
|
127
|
+
keeps a shared resource (App Service Plan, VNet, Key Vault) from bridging your
|
|
128
|
+
seed to unrelated apps sitting on the same thing.
|
|
129
|
+
4. **Render** - draw.io (Azure icons) + Mermaid + JSON + a self-contained HTML viewer.
|
|
130
|
+
5. **Ask** - query the saved map in plain language (`cloudmap ask`); the answers are
|
|
131
|
+
computed from the graph, and a local model may only route the question or narrate
|
|
132
|
+
the result.
|
|
133
|
+
|
|
134
|
+
## Usage
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
cloudmap trace <name> --from <fixture.json> [options]
|
|
138
|
+
|
|
139
|
+
--level high|detail high = architecture view grouped by type (default);
|
|
140
|
+
one box per type, so instance names are not in the map
|
|
141
|
+
detail = every instance with its real name
|
|
142
|
+
--direction both|down|up both = full blast radius (default)
|
|
143
|
+
down = only what it depends on
|
|
144
|
+
up = only what depends on it
|
|
145
|
+
--max-hops N limit traversal depth
|
|
146
|
+
-o FILE draw.io output (default: <name>.blast.drawio)
|
|
147
|
+
--mermaid FILE also write Mermaid
|
|
148
|
+
--json FILE also write the graph as JSON
|
|
149
|
+
--html FILE also write a self-contained interactive HTML viewer
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Try it with the bundled synthetic estate:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
cloudmap trace contoso-web --from fixtures/contoso.json --mermaid out.mmd --json out.json
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Ask a map questions
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
cloudmap ask <map.json> "<question>"
|
|
162
|
+
|
|
163
|
+
--explain also narrate the answer with a LOCAL model (ollama)
|
|
164
|
+
--llm if no built-in rule understands the phrasing, let a LOCAL model
|
|
165
|
+
pick the query (its choice is validated against the map)
|
|
166
|
+
--max-hops N limit traversal depth
|
|
167
|
+
--json print the answer as JSON (for scripting)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Instance names only exist in a `--level detail` map; the default high-level map
|
|
171
|
+
groups by type, so ask it about a group (`"what breaks if I touch Key Vault"`) or
|
|
172
|
+
trace with `--level detail` first:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
$ cloudmap trace contoso-web --from fixtures/contoso.json --level detail --json out.json
|
|
176
|
+
$ cloudmap ask out.json "what breaks if I touch contoso-kv"
|
|
177
|
+
Query: impact · subject: contoso-kv
|
|
178
|
+
2 resource(s) depend on contoso-kv - changing it can break them.
|
|
179
|
+
|
|
180
|
+
contoso-web (Web App) 1 hop(s)
|
|
181
|
+
contoso-web --reads-secret; role: Key Vault Secrets User--> contoso-kv
|
|
182
|
+
proof: app config references host contoso-kv.vault.azure.net; Key Vault
|
|
183
|
+
reference to vault contoso-kv; RBAC role assignment
|
|
184
|
+
contoso-agw (App Gateway) 2 hop(s)
|
|
185
|
+
...
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Questions it answers: what breaks if I touch X · what does X depend on · how does
|
|
189
|
+
X reach Y · what is shared in this map · what should I not trust · explain this map.
|
|
190
|
+
|
|
191
|
+
**The answers are computed, not generated.** "What breaks if I touch X" is a graph
|
|
192
|
+
traversal, so that is how it is answered - the numbers and names come from the
|
|
193
|
+
edges. A local model is optional and can only do two things: pick which query an
|
|
194
|
+
unusual phrasing meant (`--llm`, and its pick is validated against the map), or put
|
|
195
|
+
the already-computed facts into prose (`--explain`). It never supplies a fact, so it
|
|
196
|
+
cannot promote a guess to one. Every finding shows the hops behind it and the proof
|
|
197
|
+
of each hop, and a finding that leans on a model-proposed edge is marked `[GUESS]`.
|
|
198
|
+
If the map itself says it is incomplete, every answer from it repeats that warning.
|
|
199
|
+
|
|
200
|
+
### Capture a real export (so the tests can be wrong)
|
|
201
|
+
|
|
202
|
+
A fixture you wrote yourself can only confirm what you already believe. `capture`
|
|
203
|
+
saves what Azure actually returned, scrubs it, and gives you something that can
|
|
204
|
+
contradict the extractors.
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
cloudmap capture --allow-live --single-sub -o fixtures/captured_real.json
|
|
208
|
+
cloudmap scrub raw-export.json -o fixtures/captured_real.json # for a file you already have
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The scrub is a **global, consistent** pseudonymisation, not a field-by-field
|
|
212
|
+
blanking: `kv-payments` becomes `kv-1` everywhere at once, so the app setting that
|
|
213
|
+
references `kv-payments.vault.azure.net` still points at the same vault
|
|
214
|
+
afterwards. What survives on purpose: service domain suffixes (the rules read them
|
|
215
|
+
to decide what an edge means), built-in role GUIDs (public Azure constants) and
|
|
216
|
+
private IP ranges (that is the topology). What does not: names, resource groups,
|
|
217
|
+
subscription and principal GUIDs, e-mails, public IPs, and any
|
|
218
|
+
password / key / SAS fragment, which is redacted rather than renamed.
|
|
219
|
+
|
|
220
|
+
The mapping is never written to disk - it is the re-identification key. Counts are
|
|
221
|
+
printed, the mapping is not. **A scrubber is not a proof: read the file before you
|
|
222
|
+
commit it.** `--no-scrub` exists for local debugging and writes credentials to
|
|
223
|
+
disk; keep those files named `*.live.json` so `.gitignore` catches them.
|
|
224
|
+
|
|
225
|
+
### Live Azure (opt-in)
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
cloudmap trace my-app --live --allow-live
|
|
229
|
+
|
|
230
|
+
--single-sub query only the active subscription (default: every enabled
|
|
231
|
+
subscription in the tenant)
|
|
232
|
+
--enrich MODE which web apps to deep-enrich for the dependencies that only
|
|
233
|
+
exist in app config. auto (default) = the seed alone when the
|
|
234
|
+
seed is a web app, every app in scope when it is not;
|
|
235
|
+
all | seed | none
|
|
236
|
+
--resolve-secrets read KV secret values in-memory to see through KV-backed
|
|
237
|
+
connection strings (never printed or written)
|
|
238
|
+
--llm let a LOCAL model (ollama) propose extra edges, each
|
|
239
|
+
verified against scanned resources before it is trusted
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Why `--enrich` matters.** A Key Vault reference or a connection string lives in an
|
|
243
|
+
app's settings, which Resource Graph does not return - so that edge exists only once
|
|
244
|
+
that app has been deep-enriched. Enriching only the seed would make the graph
|
|
245
|
+
asymmetric: tracing an app finds the vault it reads, but tracing the vault would never
|
|
246
|
+
find the app. Since "what breaks if I touch this" is usually asked about shared
|
|
247
|
+
infrastructure, `auto` enriches every app in scope whenever the seed is *not* an app.
|
|
248
|
+
Anything left un-enriched is reported as a **blind spot** on the map and repeated by
|
|
249
|
+
every `ask` answer drawn from it, so an empty result never passes for "nothing depends
|
|
250
|
+
on this".
|
|
251
|
+
|
|
252
|
+
Live mode is opt-in, not sandboxed: `--allow-live` is the deliberate switch, and
|
|
253
|
+
cloudmap reads whatever subscription `az` is pointed at. The read is read-only, but
|
|
254
|
+
it is a read of live infrastructure, so point it on purpose. As an optional guard
|
|
255
|
+
against a stale `az` context silently redirecting a scan, pin the subscription you
|
|
256
|
+
mean - if set, cloudmap refuses to run against any other active subscription:
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
export CLOUDMAP_ALLOW_SUBSCRIPTION=<subscription-id> # optional
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
If a live read fails (e.g. missing RBAC), cloudmap **reports the gap** instead of
|
|
263
|
+
silently dropping edges, and warns when a scan is truncated - so you know when the
|
|
264
|
+
picture is incomplete. Fixtures are always the default.
|
|
265
|
+
**Do not point this at data you are not authorized to read.**
|
|
266
|
+
|
|
267
|
+
## Install
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
git clone https://github.com/KatsaounisThanasis/cloudmap && cd cloudmap
|
|
271
|
+
pip install -e . # or just: python -m cloudmap trace ...
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
No third-party dependencies - Python 3.9+ standard library only.
|
|
275
|
+
|
|
276
|
+
Supported today: App Service / Functions, Container Apps (+ environments), AKS,
|
|
277
|
+
App Gateway, API Management, Key Vault, Storage, SQL / PostgreSQL / MySQL / Cosmos,
|
|
278
|
+
Redis, Service Bus, Event Hub, Cognitive Search, Azure OpenAI, Container Registry,
|
|
279
|
+
Log Analytics, App Insights, VNets, Private Endpoints and managed identities.
|
|
280
|
+
|
|
281
|
+
## Roadmap
|
|
282
|
+
|
|
283
|
+
- Slice 2: Terraform state ingestor + drift overlay (desired vs actual).
|
|
284
|
+
- Slice 3: AKS / Kubernetes workload correlation.
|
|
285
|
+
- Resource-group and application (tag-based) seeds.
|
|
286
|
+
- More edge extractors and Azure icon mappings (Container Apps still render as a
|
|
287
|
+
labelled box - an icon is only added once its azure2 asset path is verified).
|
|
288
|
+
|
|
289
|
+
## License
|
|
290
|
+
|
|
291
|
+
MIT
|