cloudmap 1.0.0__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 (72) hide show
  1. cloudmap-1.0.0/.github/workflows/ci.yml +26 -0
  2. cloudmap-1.0.0/.github/workflows/publish.yml +26 -0
  3. cloudmap-1.0.0/.gitignore +22 -0
  4. cloudmap-1.0.0/ARCHITECTURE.md +158 -0
  5. cloudmap-1.0.0/FORMAT.md +89 -0
  6. cloudmap-1.0.0/LICENSE +21 -0
  7. cloudmap-1.0.0/PKG-INFO +340 -0
  8. cloudmap-1.0.0/PLAN.md +117 -0
  9. cloudmap-1.0.0/README.md +306 -0
  10. cloudmap-1.0.0/cloudmap/__init__.py +3 -0
  11. cloudmap-1.0.0/cloudmap/__main__.py +6 -0
  12. cloudmap-1.0.0/cloudmap/adapters/__init__.py +91 -0
  13. cloudmap-1.0.0/cloudmap/ask/__init__.py +111 -0
  14. cloudmap-1.0.0/cloudmap/ask/intent.py +133 -0
  15. cloudmap-1.0.0/cloudmap/ask/narration.py +54 -0
  16. cloudmap-1.0.0/cloudmap/ask/queries.py +296 -0
  17. cloudmap-1.0.0/cloudmap/cli.py +534 -0
  18. cloudmap-1.0.0/cloudmap/extract/__init__.py +0 -0
  19. cloudmap-1.0.0/cloudmap/extract/extractors.py +653 -0
  20. cloudmap-1.0.0/cloudmap/extract/llm.py +89 -0
  21. cloudmap-1.0.0/cloudmap/graph.py +208 -0
  22. cloudmap-1.0.0/cloudmap/ingest/__init__.py +0 -0
  23. cloudmap-1.0.0/cloudmap/ingest/azure.py +380 -0
  24. cloudmap-1.0.0/cloudmap/ingest/fixture.py +15 -0
  25. cloudmap-1.0.0/cloudmap/interactive.py +227 -0
  26. cloudmap-1.0.0/cloudmap/local_model.py +49 -0
  27. cloudmap-1.0.0/cloudmap/model.py +43 -0
  28. cloudmap-1.0.0/cloudmap/render/__init__.py +0 -0
  29. cloudmap-1.0.0/cloudmap/render/azure_icons.py +72 -0
  30. cloudmap-1.0.0/cloudmap/render/csv_export.py +47 -0
  31. cloudmap-1.0.0/cloudmap/render/drawio.py +149 -0
  32. cloudmap-1.0.0/cloudmap/render/html.py +579 -0
  33. cloudmap-1.0.0/cloudmap/render/json_out.py +52 -0
  34. cloudmap-1.0.0/cloudmap/render/mermaid.py +25 -0
  35. cloudmap-1.0.0/cloudmap/scrub.py +300 -0
  36. cloudmap-1.0.0/docs/social-preview.html +91 -0
  37. cloudmap-1.0.0/docs/social-preview.png +0 -0
  38. cloudmap-1.0.0/estate-viewer.png +0 -0
  39. cloudmap-1.0.0/fixtures/acme_orders.json +87 -0
  40. cloudmap-1.0.0/fixtures/contoso.json +216 -0
  41. cloudmap-1.0.0/fixtures/estate.json +373 -0
  42. cloudmap-1.0.0/pyproject.toml +60 -0
  43. cloudmap-1.0.0/tests/complex_mock_demo/01_input_complex_random.json +80 -0
  44. cloudmap-1.0.0/tests/complex_mock_demo/04_scrubbed_output.json +85 -0
  45. cloudmap-1.0.0/tests/complex_mock_demo/06_trace_output.json +93 -0
  46. cloudmap-1.0.0/tests/complex_mock_demo/07_trace_output.html +369 -0
  47. cloudmap-1.0.0/tests/complex_mock_demo/08_trace_output.csv +5 -0
  48. cloudmap-1.0.0/tests/complex_mock_demo/09_trace_output.drawio +39 -0
  49. cloudmap-1.0.0/tests/complex_mock_demo/10_input_enterprise_architecture.json +294 -0
  50. cloudmap-1.0.0/tests/complex_mock_demo/12_enterprise_trace.html +504 -0
  51. cloudmap-1.0.0/tests/complex_mock_demo/13_enterprise_trace.json +263 -0
  52. cloudmap-1.0.0/tests/complex_mock_demo/14_enterprise_scrubbed.json +298 -0
  53. cloudmap-1.0.0/tests/complex_mock_demo/README.md +47 -0
  54. cloudmap-1.0.0/tests/test_adapters.py +51 -0
  55. cloudmap-1.0.0/tests/test_arg_rows.py +188 -0
  56. cloudmap-1.0.0/tests/test_ask.py +372 -0
  57. cloudmap-1.0.0/tests/test_azure.py +20 -0
  58. cloudmap-1.0.0/tests/test_cli_exports.py +227 -0
  59. cloudmap-1.0.0/tests/test_containerapps.py +161 -0
  60. cloudmap-1.0.0/tests/test_drawio_xml.py +137 -0
  61. cloudmap-1.0.0/tests/test_enrich.py +95 -0
  62. cloudmap-1.0.0/tests/test_estate.py +39 -0
  63. cloudmap-1.0.0/tests/test_fixtures_safe.py +70 -0
  64. cloudmap-1.0.0/tests/test_golden_orders.py +80 -0
  65. cloudmap-1.0.0/tests/test_graph.py +53 -0
  66. cloudmap-1.0.0/tests/test_html.py +40 -0
  67. cloudmap-1.0.0/tests/test_ingest_paging.py +274 -0
  68. cloudmap-1.0.0/tests/test_interactive_wizard.py +380 -0
  69. cloudmap-1.0.0/tests/test_llm.py +46 -0
  70. cloudmap-1.0.0/tests/test_scrub.py +184 -0
  71. cloudmap-1.0.0/tests/test_trust.py +319 -0
  72. cloudmap-1.0.0/uv.lock +338 -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,22 @@
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
+ # live-run output is written to a folder named after the traced resource
21
+ nbg-*/
22
+ nbg-*
@@ -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.
cloudmap-1.0.0/LICENSE ADDED
@@ -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.