cloudmap 1.3.2__tar.gz → 1.3.4__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.3.4/.github/ISSUE_TEMPLATE/bug_report.yml +43 -0
- cloudmap-1.3.4/.github/ISSUE_TEMPLATE/config.yml +5 -0
- cloudmap-1.3.4/.github/ISSUE_TEMPLATE/feature_request.yml +20 -0
- cloudmap-1.3.4/.github/workflows/ci.yml +51 -0
- cloudmap-1.3.4/.gitignore +30 -0
- cloudmap-1.3.4/ARCHITECTURE.md +179 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/PKG-INFO +195 -51
- {cloudmap-1.3.2 → cloudmap-1.3.4}/README.md +194 -50
- cloudmap-1.3.4/SECURITY.md +108 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/__init__.py +1 -1
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/cli.py +146 -22
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/extract/extractors.py +65 -6
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/ingest/azure.py +70 -2
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/render/csv_export.py +7 -1
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/render/html.py +12 -2
- cloudmap-1.3.4/tests/test_artifact_bytes.py +84 -0
- cloudmap-1.3.4/tests/test_az_invocation.py +92 -0
- cloudmap-1.3.4/tests/test_deterministic_output.py +61 -0
- cloudmap-1.3.4/tests/test_hidden_config_types.py +96 -0
- cloudmap-1.3.4/tests/test_hostile_names.py +189 -0
- cloudmap-1.3.4/tests/test_large_graphs.py +125 -0
- cloudmap-1.3.4/tests/test_output_encoding.py +67 -0
- cloudmap-1.3.4/tests/test_portal_links.py +74 -0
- cloudmap-1.3.4/tests/test_sovereign_clouds.py +112 -0
- cloudmap-1.3.2/.github/workflows/ci.yml +0 -26
- cloudmap-1.3.2/.gitignore +0 -22
- cloudmap-1.3.2/ARCHITECTURE.md +0 -158
- cloudmap-1.3.2/SECURITY.md +0 -46
- {cloudmap-1.3.2 → cloudmap-1.3.4}/.github/workflows/publish.yml +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/CONTRIBUTING.md +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/FORMAT.md +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/LICENSE +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/PLAN.md +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/__main__.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/adapters/__init__.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/ask/__init__.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/ask/intent.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/ask/narration.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/ask/queries.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/data/contoso.json +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/extract/__init__.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/extract/llm.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/graph.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/ingest/__init__.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/ingest/fixture.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/interactive.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/local_model.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/model.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/render/__init__.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/render/azure_icons.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/render/drawio.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/render/json_out.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/render/mermaid.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/scrub.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/docs/social-preview.html +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/docs/social-preview.png +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/estate-viewer.png +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/fixtures/acme_orders.json +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/fixtures/contoso.json +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/fixtures/estate.json +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/pyproject.toml +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/01_input_complex_random.json +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/04_scrubbed_output.json +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/06_trace_output.json +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/07_trace_output.html +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/08_trace_output.csv +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/09_trace_output.drawio +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/10_input_enterprise_architecture.json +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/12_enterprise_trace.html +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/13_enterprise_trace.json +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/14_enterprise_scrubbed.json +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/README.md +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/data/azure_resource_types.txt +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_adapters.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_arg_rows.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_ask.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_azure.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_cli_exports.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_common_types.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_containerapps.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_containment_vs_association.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_drawio_xml.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_enrich.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_estate.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_fixtures_safe.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_golden_orders.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_graph.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_html.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_ingest_paging.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_interactive_wizard.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_llm.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_local_model.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_render_text.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_scrub.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_trust.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_type_agnostic.py +0 -0
- {cloudmap-1.3.2 → cloudmap-1.3.4}/uv.lock +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Something broke, or the map is wrong
|
|
3
|
+
labels: [bug]
|
|
4
|
+
body:
|
|
5
|
+
- type: input
|
|
6
|
+
id: version
|
|
7
|
+
attributes:
|
|
8
|
+
label: cloudmap version
|
|
9
|
+
description: "`pip show cloudmap` or `cloudmap --help` header"
|
|
10
|
+
placeholder: "1.3.2"
|
|
11
|
+
validations:
|
|
12
|
+
required: true
|
|
13
|
+
- type: input
|
|
14
|
+
id: command
|
|
15
|
+
attributes:
|
|
16
|
+
label: The exact command you ran
|
|
17
|
+
placeholder: "cloudmap trace my-app --live --allow-live --single-sub"
|
|
18
|
+
validations:
|
|
19
|
+
required: true
|
|
20
|
+
- type: textarea
|
|
21
|
+
id: what-happened
|
|
22
|
+
attributes:
|
|
23
|
+
label: What happened, and what did you expect
|
|
24
|
+
description: Paste the output. If the map itself is wrong (a missing or bogus edge), say which edge.
|
|
25
|
+
validations:
|
|
26
|
+
required: true
|
|
27
|
+
- type: textarea
|
|
28
|
+
id: fixture
|
|
29
|
+
attributes:
|
|
30
|
+
label: Minimal synthetic fixture (for wrong-map bugs)
|
|
31
|
+
description: >
|
|
32
|
+
The perfect report reproduces the wrong edge with a minimal synthetic
|
|
33
|
+
fixture - see fixtures/contoso.json for the shape. NEVER paste a real
|
|
34
|
+
capture; run `cloudmap scrub` on it first and read the file before
|
|
35
|
+
posting.
|
|
36
|
+
render: json
|
|
37
|
+
- type: checkboxes
|
|
38
|
+
id: scrubbed
|
|
39
|
+
attributes:
|
|
40
|
+
label: Safety check
|
|
41
|
+
options:
|
|
42
|
+
- label: Anything I pasted above contains no real resource names, subscription ids or credentials.
|
|
43
|
+
required: true
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: A resource type mapped poorly, a missing edge kind, a workflow that needs a flag
|
|
3
|
+
labels: [enhancement]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: problem
|
|
7
|
+
attributes:
|
|
8
|
+
label: The situation
|
|
9
|
+
description: What were you trying to learn from the map, and what did it give you instead?
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: proposal
|
|
14
|
+
attributes:
|
|
15
|
+
label: What you would expect
|
|
16
|
+
description: >
|
|
17
|
+
If it is a missing dependency for a resource type: which property of
|
|
18
|
+
which type expresses it? (That is usually a small extractor rule plus a
|
|
19
|
+
fixture test.) Keep in mind the rules in CONTRIBUTING.md - read-only,
|
|
20
|
+
local-first, every edge carries evidence.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ${{ matrix.os }}
|
|
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
|
+
# Windows and macOS are here because the package claims OS Independent and
|
|
16
|
+
# an ubuntu-only matrix let a Windows-breaking bug ship: `az` installs as
|
|
17
|
+
# az.cmd there, and subprocess without shell=True never finds it. macOS is
|
|
18
|
+
# the cheap third data point for path and encoding assumptions.
|
|
19
|
+
include:
|
|
20
|
+
- {os: ubuntu-latest, python-version: "3.9"}
|
|
21
|
+
- {os: ubuntu-latest, python-version: "3.13"}
|
|
22
|
+
- {os: windows-latest, python-version: "3.13"}
|
|
23
|
+
- {os: macos-latest, python-version: "3.13"}
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: ${{ matrix.python-version }}
|
|
29
|
+
- run: pip install -e ".[dev]"
|
|
30
|
+
- run: ruff check .
|
|
31
|
+
- run: pytest
|
|
32
|
+
# The suite includes tests/test_fixtures_safe.py, which fails if a fixture
|
|
33
|
+
# ever carries a credential or an unscrubbed GUID. That guard is the reason
|
|
34
|
+
# CI runs on pull requests too.
|
|
35
|
+
|
|
36
|
+
# A smoke test of the path a stranger actually takes first, on every OS:
|
|
37
|
+
# writing all five artifacts to disk exercises the file encodings and the
|
|
38
|
+
# path joining that a pure-unit suite never touches.
|
|
39
|
+
- name: demo trace writes every artifact
|
|
40
|
+
run: >
|
|
41
|
+
cloudmap trace contoso-web --from demo -d smoke
|
|
42
|
+
--json smoke/x.json --html smoke/x.html --csv smoke/x.csv --mermaid smoke/x.mmd
|
|
43
|
+
- name: the artifacts exist and are not empty
|
|
44
|
+
shell: python
|
|
45
|
+
run: |
|
|
46
|
+
import pathlib, sys
|
|
47
|
+
missing = [str(p) for p in [pathlib.Path("smoke", f"x.{e}")
|
|
48
|
+
for e in ("json", "html", "csv", "mmd")]
|
|
49
|
+
if not pathlib.Path(p).exists() or pathlib.Path(p).stat().st_size == 0]
|
|
50
|
+
if missing:
|
|
51
|
+
sys.exit("empty or missing: " + ", ".join(missing))
|
|
@@ -0,0 +1,30 @@
|
|
|
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/
|
|
20
|
+
|
|
21
|
+
# A live trace writes artifacts named after the resource it traced, so their
|
|
22
|
+
# names cannot be known in advance. Point --out-dir at live/ or out/ above.
|
|
23
|
+
# These root-anchored rules are the belt-and-braces for the ones that land in
|
|
24
|
+
# the repo root anyway - no tracked file at the root has these extensions, and
|
|
25
|
+
# the committed sample artifacts live under tests/ and docs/, which are unaffected.
|
|
26
|
+
/*.drawio
|
|
27
|
+
/*.json
|
|
28
|
+
/*.html
|
|
29
|
+
/*.csv
|
|
30
|
+
/*.mmd
|
|
@@ -0,0 +1,179 @@
|
|
|
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+; two runtime deps, `rich` and
|
|
10
|
+
`questionary`, both for the terminal UI) that takes the **name of one Azure
|
|
11
|
+
resource** and produces its full **dependency graph (blast radius)** as an
|
|
12
|
+
**editable draw.io diagram**, plus an interactive HTML viewer, Mermaid, JSON and
|
|
13
|
+
CSV. It is a clean four-stage pipeline: **ingest** (fixture or live `az`) →
|
|
14
|
+
**extract** (properties / hostnames / RBAC / secrets → typed edges) → **graph**
|
|
15
|
+
(direction-consistent blast-radius BFS) → **render**. The core value is that
|
|
16
|
+
Azure Resource Graph has no "dependencies" table, so `extract/extractors.py`
|
|
17
|
+
infers dependencies and **verifies** each one - anything referenced but not
|
|
18
|
+
verified becomes an explicit `external` node instead of being silently dropped.
|
|
19
|
+
|
|
20
|
+
## Architecture
|
|
21
|
+
|
|
22
|
+
```mermaid
|
|
23
|
+
flowchart TD
|
|
24
|
+
CLI["__main__.py → cli.main()<br/>argparse: trace / capture / scrub / ask"] --> TRACE["_cmd_trace"]
|
|
25
|
+
CLI -->|"no args, tty"| WIZ["interactive.py<br/>wizard: sub → rg → resource"]
|
|
26
|
+
WIZ --> TRACE
|
|
27
|
+
|
|
28
|
+
TRACE -->|"--from (or 'demo')"| FIX["adapters/load_graph()<br/>raw export or neutral map"]
|
|
29
|
+
TRACE -->|"--live"| AZ["ingest/azure.py<br/>query_live()"]
|
|
30
|
+
|
|
31
|
+
AZ --> GUARD["_guard()<br/>optional ALLOW_SUB pin"]
|
|
32
|
+
AZ --> SUBS["_target_subscriptions()<br/>tenant-wide by default"]
|
|
33
|
+
AZ --> PAGE["_graph_paged()<br/>ALL types, skip_token paging"]
|
|
34
|
+
|
|
35
|
+
FIX --> BUILD["graph.build_graph()"]
|
|
36
|
+
AZ --> BUILD
|
|
37
|
+
BUILD --> EXTRACT["extract_edges()<br/>typed rules + 4 generic passes"]
|
|
38
|
+
|
|
39
|
+
TRACE --> ENRICH["_enrich_live() per seed type<br/>enrich_webapps() / enrich_aks_clusters()<br/>appsettings+RBAC+diag, secrets in-memory"]
|
|
40
|
+
ENRICH --> EXTERNAL["seed_external_dependencies()<br/>never-drop → external nodes"]
|
|
41
|
+
|
|
42
|
+
BUILD --> BLAST["blast_radius() graph.py<br/>BFS, direction consistency"]
|
|
43
|
+
EXTERNAL --> BLAST
|
|
44
|
+
BLAST --> COLLAPSE["collapse_high_level()<br/>singletons keep their name"]
|
|
45
|
+
COLLAPSE --> R1["render/drawio.py<br/>native Azure icons"]
|
|
46
|
+
COLLAPSE --> R2["render/mermaid.py"]
|
|
47
|
+
COLLAPSE --> R3["render/json_out.py<br/>meta: truncated/gaps/blind spots"]
|
|
48
|
+
COLLAPSE --> R4["render/html.py<br/>self-contained viewer"]
|
|
49
|
+
COLLAPSE --> R5["render/csv_export.py<br/>edge list + evidence"]
|
|
50
|
+
|
|
51
|
+
CLI --> ASK["_cmd_ask"]
|
|
52
|
+
R3 -.->|"saved map, reloaded"| FIX
|
|
53
|
+
ASK --> INTENT["ask/intent.py<br/>rules first, model only as<br/>validated fallback (--llm)"]
|
|
54
|
+
INTENT --> Q["ask/queries.py<br/>impact / depends / paths /<br/>shared / guesses / summary<br/>COMPUTED from edges"]
|
|
55
|
+
Q --> NARR["ask/narration.py (--explain)<br/>prose from the computed facts"]
|
|
56
|
+
INTENT -.-> LM["local_model.py<br/>the only outbound call:<br/>ollama or OpenAI-compatible, local"]
|
|
57
|
+
NARR -.-> LM
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## File Map
|
|
61
|
+
|
|
62
|
+
| File | Role | Why it's built this way |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `cloudmap/model.py` | `Node`/`Edge`/`Graph` dataclasses | Provider-neutral core. The `external`+`note` fields exist for the "never silent drop" principle - a referenced-but-unverified target still needs a place in the model. |
|
|
65
|
+
| `cloudmap/ingest/fixture.py` | Load synthetic/captured JSON | Accepts both a bare list and `{"data":[...]}` so the same file works as a fixture and as captured `az graph` output. |
|
|
66
|
+
| `cloudmap/ingest/azure.py` | Live `az` ingest + enrichment | The most sensitive file, so it concentrates every guard: the single `subprocess.run` boundary with timeouts, the optional `CLOUDMAP_ALLOW_SUBSCRIPTION` pin (never injected into `az account` commands, so a mismatch is reported as a mismatch), `classify_gap()` so artifacts carry categories instead of raw stderr, and the rule that Kubernetes secret text never reaches an export. |
|
|
67
|
+
| `cloudmap/extract/extractors.py` | **The heart**: properties/hosts/RBAC/secrets → edges | Typed rules for ~17 services plus four generic passes that make "any type" true: ARM-id references (with containment property paths excluded, not whole types), host mentions, nested child-of, and generic host indexing (a host counts as identity only when it carries the resource's own name - two apps on one App Service scale unit share `ftpsHostName`). Observers (alert rules, dashboards) emit `observes` instead of vanishing. |
|
|
68
|
+
| `cloudmap/graph.py` | Build graph + blast-radius BFS + the high-level collapse | Direction consistency in `blast_radius` (never reverse once you have stepped, tracked per direction) is why a shared VNet/plan does not pull in unrelated apps. `collapse_high_level` folds instances into one box per type, keeps a singleton's real name, and carries kinds AND evidence across. |
|
|
69
|
+
| `cloudmap/interactive.py` | The zero-argument wizard | Offers every resource in the subscription (no type allowlist - the engine has none), delegates all `az` I/O to `ingest.azure`, and pins the chosen subscription before handing off to `_cmd_trace`. |
|
|
70
|
+
| `cloudmap/scrub.py` | Global consistent pseudonymisation for captures | Identifier positions are renamed consistently (a reference and its target stay correlated); credentials are redacted, and its docstrings carry the postmortems of the two leak classes that shipped and were caught. |
|
|
71
|
+
| `cloudmap/render/drawio.py` | `.drawio` with Azure2 icons | Verified icon paths; falls back to a box, external nodes render dashed, so an icon is never broken. |
|
|
72
|
+
| `cloudmap/render/html.py` | One self-contained interactive viewer | No server, no CDN; the INCOMPLETE badge opens its "why" in the side panel, grouped ×N boxes list their instances on click. |
|
|
73
|
+
| `cloudmap/render/{mermaid,json_out,csv_export}.py` | Secondary outputs | Preview, machine-readable inventory (with the honesty meta), and a flat evidence-bearing edge list for auditors. |
|
|
74
|
+
| `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. |
|
|
75
|
+
| `cloudmap/ask/intent.py` | Question → one query | Rules first; 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. |
|
|
76
|
+
| `cloudmap/ask/narration.py` | Optional prose (`--explain`) | Handed the computed facts only, printed *below* them. |
|
|
77
|
+
| `cloudmap/local_model.py` | The single outbound model call | One module = one auditable promise: localhost (ollama's API by default, any OpenAI-compatible server via `CLOUDMAP_LLM_URL`), and failure returns an empty value. |
|
|
78
|
+
| `cloudmap/cli.py` | Orchestration + argparse | `_cmd_trace` wires the stages; `_cmd_capture`/`_cmd_scrub` produce committable fixtures; `_cmd_ask` loads a saved map and prints the computed answer, proof lines included. |
|
|
79
|
+
| `cloudmap/data/contoso.json` | The packaged demo estate | `--from demo` must work for a pip-only install - the first command a stranger runs cannot depend on cloning the repo. |
|
|
80
|
+
| `fixtures/` | Synthetic + scrubbed-golden estates | Fixture-first development → zero cloud contact in tests. |
|
|
81
|
+
| `tests/` | 536 tests | Behaviour-named; includes a conformance sweep of all 4,687 published Azure resource types (`tests/data/azure_resource_types.txt`) through every dependency channel, a realistic-estate suite with the property names Azure genuinely returns, `test_fixtures_safe.py` (fails CI if a fixture ever carries a credential), `test_deterministic_output.py` (subprocesses with different hash seeds - the in-process suite cannot see that class of bug) and `test_sovereign_clouds.py`. |
|
|
82
|
+
| `.gitignore` | Safety | `live/`, `*.live.json`, `*.blast.drawio` and every artifact extension at the repo root are ignored → output from a real tenant cannot be committed by accident. |
|
|
83
|
+
|
|
84
|
+
## Execution Flow
|
|
85
|
+
|
|
86
|
+
**Fixture path** (`cloudmap trace contoso-web --from demo`):
|
|
87
|
+
|
|
88
|
+
1. `__main__.py` → `cli.main()` - argparse, or the wizard when run bare on a tty.
|
|
89
|
+
2. `_cmd_trace` - fixture branch → `adapters.load_graph()` (raw export or neutral
|
|
90
|
+
map, auto-detected; a reloaded capture keeps its truncated/gaps meta).
|
|
91
|
+
3. `graph.build_graph()` → builds `Node`s and calls `extract_edges()`.
|
|
92
|
+
4. `extract_edges()` - constructs a `Resolver` and derives edges: typed rules
|
|
93
|
+
(`serverFarmId`→hosted-on, VM `has-nic`/`uses-disk`, NIC `in-subnet`, config
|
|
94
|
+
hostnames/vault refs/IK, RBAC via system AND user-assigned identity
|
|
95
|
+
principals, `managedBy`), then the generic passes for every other shape.
|
|
96
|
+
5. `find_seeds()` - exact id, exact name, else substring; >1 → each match is
|
|
97
|
+
printed with its resource group and full id.
|
|
98
|
+
6. `blast_radius()` - BFS from the seed in both directions, never reversing.
|
|
99
|
+
7. Render: draw.io / HTML / Mermaid / JSON / CSV, `_print_summary` shows
|
|
100
|
+
"Depends on" and "What depends on it" as separate panels.
|
|
101
|
+
|
|
102
|
+
**Live path** (`--live --allow-live [--resolve-secrets]`) - additionally:
|
|
103
|
+
|
|
104
|
+
8. `query_live()` → `_guard()` (optional pin check) → `_target_subscriptions()`
|
|
105
|
+
→ `_graph_paged()` (ALL resource types + tenant-wide role assignments,
|
|
106
|
+
`skip_token` paging, loud warning at the page cap).
|
|
107
|
+
9. `_enrich_live()` picks targets by seed type: a workload seed enriches itself;
|
|
108
|
+
a config-referenced data service (Key Vault, SQL, storage, Redis, ...)
|
|
109
|
+
enriches every app in scope, because only their config reveals its
|
|
110
|
+
dependents; compute/network seeds stay seed-only - ARM already has their
|
|
111
|
+
relationships. `enrich_webapps()`/`enrich_aks_clusters()` run concurrently
|
|
112
|
+
(`CLOUDMAP_ENRICH_WORKERS`, default 12). Secrets resolve **in-memory** only.
|
|
113
|
+
10. Rebuild, `seed_external_dependencies()` resurfaces unresolved references as
|
|
114
|
+
dashed external nodes, and whatever was *not* enriched is recorded as a
|
|
115
|
+
blind spot in `meta` - so the artifact, and every `ask` answer drawn from
|
|
116
|
+
it, repeats it.
|
|
117
|
+
|
|
118
|
+
## Design Decisions
|
|
119
|
+
|
|
120
|
+
- **extract vs ingest separation** (`extractors.py` knows nothing about `az`).
|
|
121
|
+
All dependency logic is pure Python over dicts, so the full suite runs in ~2s
|
|
122
|
+
without Azure.
|
|
123
|
+
- **Direction consistency** (`blast_radius`). Real-world testing showed shared
|
|
124
|
+
plan/VNet connect dozens of unrelated apps. "From the seed go both ways, but
|
|
125
|
+
never reverse afterwards" solves it without a hub-type list; the visited set
|
|
126
|
+
is tracked per direction so a node reachable both ways loses neither cone.
|
|
127
|
+
- **Containment is not dependency** (`_CONTAINMENT_PATHS`). A VNet's
|
|
128
|
+
`subnets[].ipConfigurations` enumerate what is plugged INTO it; following them
|
|
129
|
+
made a shared VNet a hub. But its DDoS plan and peerings are real
|
|
130
|
+
dependencies, so the exclusion is on property paths, not on types - an
|
|
131
|
+
earlier type-wide exclusion silently lost all of those edges.
|
|
132
|
+
- **Generic before hand-written, resolver as the safety net.** A type with no
|
|
133
|
+
rule still maps through ARM ids, advertised hosts, child ids and RBAC; a
|
|
134
|
+
reference that names nothing scanned yields no edge, so the generic passes
|
|
135
|
+
cannot invent a dependency. This is what a 4,687-type conformance sweep pins.
|
|
136
|
+
- **Enrichment is scoped by seed type, and the gap is declared**
|
|
137
|
+
(`_enrichment_targets`). Tracing a VM must not deep-read 289 web apps to
|
|
138
|
+
learn what ARM already says; tracing a vault must, because config is the only
|
|
139
|
+
place its dependents exist. Whatever is skipped becomes a `blind_spot`.
|
|
140
|
+
- **Scrub preserves structure, not identity** (`scrub.py`), and a capture never
|
|
141
|
+
carries Kubernetes secret text at all - the field is stripped before any
|
|
142
|
+
export, under every flag combination.
|
|
143
|
+
- **Never silent drop** (`seed_external_dependencies` + `model.py`).
|
|
144
|
+
- **The model proposes, code verifies** (`extract/llm.py`): only proposals that
|
|
145
|
+
resolve to a scanned resource survive, drawn dashed and marked; `ask` may use
|
|
146
|
+
the model to route a phrasing or narrate a computed answer, never to state a
|
|
147
|
+
fact.
|
|
148
|
+
|
|
149
|
+
## Open Questions / Risks
|
|
150
|
+
|
|
151
|
+
- **Live validation breadth**: the engine is conformance-tested against every
|
|
152
|
+
published type, but has been exercised against one real tenant, in the public
|
|
153
|
+
cloud, on Linux. Property shapes elsewhere may surface new false-positive
|
|
154
|
+
classes like the shared `ftpsHostName` one (found live, fixed,
|
|
155
|
+
regression-pinned). The sovereign clouds are covered by construction rather
|
|
156
|
+
than by a live run: an edge's kind now comes from the resolved target's ARM
|
|
157
|
+
type, not from its DNS suffix (`extractors.kind_for_target`), so
|
|
158
|
+
`vault.usgovcloudapi.net` and `vault.azure.cn` need no table entry. Windows
|
|
159
|
+
and macOS are covered by CI rather than by a table of assumptions.
|
|
160
|
+
- **Deep config readers exist for App Service and AKS only** - the two common
|
|
161
|
+
types whose config Resource Graph withholds. Checked, not assumed: Logic Apps,
|
|
162
|
+
Container Apps, ML workspaces, Cognitive Services and API Management all return
|
|
163
|
+
theirs in `properties`. What remains hidden is a different shape - child
|
|
164
|
+
resources a provider never registered with Resource Graph (Data Factory and
|
|
165
|
+
Synapse linked services, Automation runbooks, Stream Analytics inputs) and
|
|
166
|
+
data planes with their own API (Databricks secret scopes). Those types are
|
|
167
|
+
listed in `cli._CONFIG_HIDDEN_TYPES` and declared as a blind spot on every
|
|
168
|
+
map that contains one, live or fixture, so an empty result cannot pass for an
|
|
169
|
+
answer. A reader for them is roadmap, not present.
|
|
170
|
+
- **Cost of `--enrich all`** on large tenants: one `az` round-trip per app,
|
|
171
|
+
`CLOUDMAP_ENRICH_WORKERS` (default 12) at a time. No cache between runs yet -
|
|
172
|
+
every trace pays the scan again.
|
|
173
|
+
- **`az graph --skip-token`** depends on the resource-graph extension version.
|
|
174
|
+
- **`az aks command invoke`** starts a short-lived pod in the cluster to run
|
|
175
|
+
its read-only kubectl - documented in the README as the one asterisk on
|
|
176
|
+
"read-only".
|
|
177
|
+
- **Secret resolution reachability**: a vault behind an unreachable private
|
|
178
|
+
endpoint fails the in-memory resolve and the dependency shows as external
|
|
179
|
+
rather than resolved.
|