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.
Files changed (97) hide show
  1. cloudmap-1.3.4/.github/ISSUE_TEMPLATE/bug_report.yml +43 -0
  2. cloudmap-1.3.4/.github/ISSUE_TEMPLATE/config.yml +5 -0
  3. cloudmap-1.3.4/.github/ISSUE_TEMPLATE/feature_request.yml +20 -0
  4. cloudmap-1.3.4/.github/workflows/ci.yml +51 -0
  5. cloudmap-1.3.4/.gitignore +30 -0
  6. cloudmap-1.3.4/ARCHITECTURE.md +179 -0
  7. {cloudmap-1.3.2 → cloudmap-1.3.4}/PKG-INFO +195 -51
  8. {cloudmap-1.3.2 → cloudmap-1.3.4}/README.md +194 -50
  9. cloudmap-1.3.4/SECURITY.md +108 -0
  10. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/__init__.py +1 -1
  11. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/cli.py +146 -22
  12. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/extract/extractors.py +65 -6
  13. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/ingest/azure.py +70 -2
  14. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/render/csv_export.py +7 -1
  15. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/render/html.py +12 -2
  16. cloudmap-1.3.4/tests/test_artifact_bytes.py +84 -0
  17. cloudmap-1.3.4/tests/test_az_invocation.py +92 -0
  18. cloudmap-1.3.4/tests/test_deterministic_output.py +61 -0
  19. cloudmap-1.3.4/tests/test_hidden_config_types.py +96 -0
  20. cloudmap-1.3.4/tests/test_hostile_names.py +189 -0
  21. cloudmap-1.3.4/tests/test_large_graphs.py +125 -0
  22. cloudmap-1.3.4/tests/test_output_encoding.py +67 -0
  23. cloudmap-1.3.4/tests/test_portal_links.py +74 -0
  24. cloudmap-1.3.4/tests/test_sovereign_clouds.py +112 -0
  25. cloudmap-1.3.2/.github/workflows/ci.yml +0 -26
  26. cloudmap-1.3.2/.gitignore +0 -22
  27. cloudmap-1.3.2/ARCHITECTURE.md +0 -158
  28. cloudmap-1.3.2/SECURITY.md +0 -46
  29. {cloudmap-1.3.2 → cloudmap-1.3.4}/.github/workflows/publish.yml +0 -0
  30. {cloudmap-1.3.2 → cloudmap-1.3.4}/CONTRIBUTING.md +0 -0
  31. {cloudmap-1.3.2 → cloudmap-1.3.4}/FORMAT.md +0 -0
  32. {cloudmap-1.3.2 → cloudmap-1.3.4}/LICENSE +0 -0
  33. {cloudmap-1.3.2 → cloudmap-1.3.4}/PLAN.md +0 -0
  34. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/__main__.py +0 -0
  35. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/adapters/__init__.py +0 -0
  36. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/ask/__init__.py +0 -0
  37. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/ask/intent.py +0 -0
  38. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/ask/narration.py +0 -0
  39. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/ask/queries.py +0 -0
  40. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/data/contoso.json +0 -0
  41. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/extract/__init__.py +0 -0
  42. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/extract/llm.py +0 -0
  43. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/graph.py +0 -0
  44. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/ingest/__init__.py +0 -0
  45. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/ingest/fixture.py +0 -0
  46. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/interactive.py +0 -0
  47. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/local_model.py +0 -0
  48. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/model.py +0 -0
  49. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/render/__init__.py +0 -0
  50. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/render/azure_icons.py +0 -0
  51. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/render/drawio.py +0 -0
  52. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/render/json_out.py +0 -0
  53. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/render/mermaid.py +0 -0
  54. {cloudmap-1.3.2 → cloudmap-1.3.4}/cloudmap/scrub.py +0 -0
  55. {cloudmap-1.3.2 → cloudmap-1.3.4}/docs/social-preview.html +0 -0
  56. {cloudmap-1.3.2 → cloudmap-1.3.4}/docs/social-preview.png +0 -0
  57. {cloudmap-1.3.2 → cloudmap-1.3.4}/estate-viewer.png +0 -0
  58. {cloudmap-1.3.2 → cloudmap-1.3.4}/fixtures/acme_orders.json +0 -0
  59. {cloudmap-1.3.2 → cloudmap-1.3.4}/fixtures/contoso.json +0 -0
  60. {cloudmap-1.3.2 → cloudmap-1.3.4}/fixtures/estate.json +0 -0
  61. {cloudmap-1.3.2 → cloudmap-1.3.4}/pyproject.toml +0 -0
  62. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/01_input_complex_random.json +0 -0
  63. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/04_scrubbed_output.json +0 -0
  64. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/06_trace_output.json +0 -0
  65. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/07_trace_output.html +0 -0
  66. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/08_trace_output.csv +0 -0
  67. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/09_trace_output.drawio +0 -0
  68. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/10_input_enterprise_architecture.json +0 -0
  69. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/12_enterprise_trace.html +0 -0
  70. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/13_enterprise_trace.json +0 -0
  71. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/14_enterprise_scrubbed.json +0 -0
  72. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/complex_mock_demo/README.md +0 -0
  73. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/data/azure_resource_types.txt +0 -0
  74. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_adapters.py +0 -0
  75. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_arg_rows.py +0 -0
  76. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_ask.py +0 -0
  77. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_azure.py +0 -0
  78. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_cli_exports.py +0 -0
  79. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_common_types.py +0 -0
  80. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_containerapps.py +0 -0
  81. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_containment_vs_association.py +0 -0
  82. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_drawio_xml.py +0 -0
  83. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_enrich.py +0 -0
  84. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_estate.py +0 -0
  85. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_fixtures_safe.py +0 -0
  86. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_golden_orders.py +0 -0
  87. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_graph.py +0 -0
  88. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_html.py +0 -0
  89. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_ingest_paging.py +0 -0
  90. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_interactive_wizard.py +0 -0
  91. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_llm.py +0 -0
  92. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_local_model.py +0 -0
  93. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_render_text.py +0 -0
  94. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_scrub.py +0 -0
  95. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_trust.py +0 -0
  96. {cloudmap-1.3.2 → cloudmap-1.3.4}/tests/test_type_agnostic.py +0 -0
  97. {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,5 @@
1
+ blank_issues_enabled: true
2
+ contact_links:
3
+ - name: Security issue
4
+ url: https://github.com/KatsaounisThanasis/cloudmap/security/advisories/new
5
+ about: Anything that could expose a user's infrastructure data - report it privately, never as a public issue.
@@ -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.