cloudmap 1.0.0__py3-none-any.whl
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/__init__.py +3 -0
- cloudmap/__main__.py +6 -0
- cloudmap/adapters/__init__.py +91 -0
- cloudmap/ask/__init__.py +111 -0
- cloudmap/ask/intent.py +133 -0
- cloudmap/ask/narration.py +54 -0
- cloudmap/ask/queries.py +296 -0
- cloudmap/cli.py +534 -0
- cloudmap/extract/__init__.py +0 -0
- cloudmap/extract/extractors.py +653 -0
- cloudmap/extract/llm.py +89 -0
- cloudmap/graph.py +208 -0
- cloudmap/ingest/__init__.py +0 -0
- cloudmap/ingest/azure.py +380 -0
- cloudmap/ingest/fixture.py +15 -0
- cloudmap/interactive.py +227 -0
- cloudmap/local_model.py +49 -0
- cloudmap/model.py +43 -0
- cloudmap/render/__init__.py +0 -0
- cloudmap/render/azure_icons.py +72 -0
- cloudmap/render/csv_export.py +47 -0
- cloudmap/render/drawio.py +149 -0
- cloudmap/render/html.py +579 -0
- cloudmap/render/json_out.py +52 -0
- cloudmap/render/mermaid.py +25 -0
- cloudmap/scrub.py +300 -0
- cloudmap-1.0.0.dist-info/METADATA +340 -0
- cloudmap-1.0.0.dist-info/RECORD +31 -0
- cloudmap-1.0.0.dist-info/WHEEL +4 -0
- cloudmap-1.0.0.dist-info/entry_points.txt +2 -0
- cloudmap-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import io
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def to_csv(graph, seed_id, meta=None):
|
|
6
|
+
"""Render the graph as a flat CSV for auditing and spreadsheet analysis."""
|
|
7
|
+
output = io.StringIO()
|
|
8
|
+
writer = csv.writer(output)
|
|
9
|
+
|
|
10
|
+
# Write header
|
|
11
|
+
writer.writerow([
|
|
12
|
+
"Source Name",
|
|
13
|
+
"Source Type",
|
|
14
|
+
"Dependency Kind",
|
|
15
|
+
"Target Name",
|
|
16
|
+
"Target Type",
|
|
17
|
+
"Trust Level",
|
|
18
|
+
"Evidence",
|
|
19
|
+
"Source ID",
|
|
20
|
+
"Target ID"
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
for edge in graph.edges:
|
|
24
|
+
source_node = graph.nodes[edge.source]
|
|
25
|
+
target_node = graph.nodes.get(edge.target)
|
|
26
|
+
|
|
27
|
+
target_name = target_node.name if target_node else edge.target.split("/")[-1]
|
|
28
|
+
target_type = target_node.type if target_node else "unknown"
|
|
29
|
+
target_external = target_node.external if target_node else True
|
|
30
|
+
|
|
31
|
+
trust_level = "GUESS (LLM)" if edge.origin == "model" else "Verified"
|
|
32
|
+
if target_external:
|
|
33
|
+
trust_level += " (Unverified Target)"
|
|
34
|
+
|
|
35
|
+
writer.writerow([
|
|
36
|
+
source_node.name,
|
|
37
|
+
source_node.type,
|
|
38
|
+
edge.kind,
|
|
39
|
+
target_name,
|
|
40
|
+
target_type,
|
|
41
|
+
trust_level,
|
|
42
|
+
edge.evidence,
|
|
43
|
+
source_node.id,
|
|
44
|
+
edge.target
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
return output.getvalue()
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Render a graph as an editable .drawio file using native Azure2 icons.
|
|
2
|
+
|
|
3
|
+
Icons are referenced as draw.io's built-in Azure2 image shapes
|
|
4
|
+
(`img/lib/azure2/<category>/<Icon>.svg`) - we emit the style string, draw.io
|
|
5
|
+
supplies the SVG, so no icon assets are shipped in this repo. Any resource type
|
|
6
|
+
without a mapped icon falls back to a labelled rounded box (never a broken
|
|
7
|
+
image). Entries marked (best-effort) may need a filename tweak if the icon
|
|
8
|
+
renders broken in your draw.io build.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from xml.sax.saxutils import quoteattr
|
|
12
|
+
|
|
13
|
+
# Verified against jgraph/drawio src/main/webapp/img/lib/azure2/ (folder + filename).
|
|
14
|
+
AZURE_ICON = {
|
|
15
|
+
"microsoft.web/sites": "compute/App_Services.svg",
|
|
16
|
+
"microsoft.web/serverfarms": "app_services/App_Service_Plans.svg",
|
|
17
|
+
"microsoft.keyvault/vaults": "security/Key_Vaults.svg",
|
|
18
|
+
"microsoft.containerservice/managedclusters": "compute/Kubernetes_Services.svg",
|
|
19
|
+
"microsoft.storage/storageaccounts": "storage/Storage_Accounts.svg",
|
|
20
|
+
"microsoft.network/virtualnetworks": "networking/Virtual_Networks.svg",
|
|
21
|
+
"microsoft.network/applicationgateways": "networking/Application_Gateways.svg",
|
|
22
|
+
"microsoft.network/privateendpoints": "networking/Private_Endpoint.svg",
|
|
23
|
+
"microsoft.managedidentity/userassignedidentities": "identity/Managed_Identities.svg",
|
|
24
|
+
"microsoft.containerregistry/registries": "containers/Container_Registries.svg",
|
|
25
|
+
"microsoft.operationalinsights/workspaces": "analytics/Log_Analytics_Workspaces.svg",
|
|
26
|
+
"microsoft.sql/servers": "databases/SQL_Server.svg",
|
|
27
|
+
"microsoft.insights/components": "devops/Application_Insights.svg",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
ICON_STYLE = ("shape=image;html=1;image=img/lib/azure2/{path};"
|
|
31
|
+
"verticalLabelPosition=bottom;verticalAlign=top;aspect=fixed;fontSize=11;")
|
|
32
|
+
BOX_STYLE = "rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=11;"
|
|
33
|
+
EXT_STYLE = ("rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#999999;"
|
|
34
|
+
"dashed=1;fontColor=#666666;fontSize=11;")
|
|
35
|
+
SEED_EXTRA = "strokeColor=#d79b00;strokeWidth=3;"
|
|
36
|
+
EDGE_STYLE = ("edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;endArrow=block;"
|
|
37
|
+
"fontSize=10;fontColor=#555555;")
|
|
38
|
+
# Model-proposed edges are drawn dashed + red so a guess never looks like a fact.
|
|
39
|
+
EDGE_MODEL_STYLE = EDGE_STYLE + "dashed=1;strokeColor=#b85450;fontColor=#b85450;"
|
|
40
|
+
# RBAC edges are drawn orange/thick to highlight security access paths
|
|
41
|
+
EDGE_RBAC_STYLE = EDGE_STYLE + "strokeColor=#d79b00;fontColor=#b07d00;strokeWidth=2;"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _short_type(t):
|
|
45
|
+
return t.split("/")[-1] if "/" in t else t
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def to_drawio(graph, seed_id):
|
|
49
|
+
dist = graph.distances or {n: 0 for n in graph.nodes}
|
|
50
|
+
|
|
51
|
+
layers = {}
|
|
52
|
+
for nid in graph.nodes:
|
|
53
|
+
layers.setdefault(dist.get(nid, 0), []).append(nid)
|
|
54
|
+
|
|
55
|
+
xstep, ystep = 240, 120
|
|
56
|
+
pos = {}
|
|
57
|
+
|
|
58
|
+
# Calculate max height to vertically center smaller layers
|
|
59
|
+
max_rows = max((len(col) for col in layers.values()), default=0)
|
|
60
|
+
max_height = max_rows * ystep
|
|
61
|
+
|
|
62
|
+
for layer in sorted(layers):
|
|
63
|
+
col = sorted(layers[layer], key=lambda i: graph.nodes[i].name)
|
|
64
|
+
layer_height = len(col) * ystep
|
|
65
|
+
y_offset = (max_height - layer_height) // 2
|
|
66
|
+
|
|
67
|
+
for row, nid in enumerate(col):
|
|
68
|
+
pos[nid] = (60 + layer * xstep, 60 + y_offset + row * ystep)
|
|
69
|
+
|
|
70
|
+
cells, idmap = [], {}
|
|
71
|
+
for i, nid in enumerate(graph.nodes):
|
|
72
|
+
cid = f"n{i}"
|
|
73
|
+
idmap[nid] = cid
|
|
74
|
+
n = graph.nodes[nid]
|
|
75
|
+
label = f"{n.name} ({_short_type(n.type)})"
|
|
76
|
+
icon = None if n.external else AZURE_ICON.get(n.type)
|
|
77
|
+
if icon:
|
|
78
|
+
style, w, h = ICON_STYLE.format(path=icon), 48, 48
|
|
79
|
+
elif n.external:
|
|
80
|
+
style, w, h = EXT_STYLE, 180, 50
|
|
81
|
+
else:
|
|
82
|
+
style, w, h = BOX_STYLE, 170, 50
|
|
83
|
+
if nid == seed_id:
|
|
84
|
+
style += SEED_EXTRA
|
|
85
|
+
x, y = pos[nid]
|
|
86
|
+
|
|
87
|
+
attrs = [
|
|
88
|
+
f'id="{cid}"',
|
|
89
|
+
f'label={quoteattr(label)}',
|
|
90
|
+
f'type={quoteattr(n.type)}',
|
|
91
|
+
]
|
|
92
|
+
if n.resource_group:
|
|
93
|
+
attrs.append(f'ResourceGroup={quoteattr(n.resource_group)}')
|
|
94
|
+
if n.location:
|
|
95
|
+
attrs.append(f'Location={quoteattr(n.location)}')
|
|
96
|
+
if n.subscription:
|
|
97
|
+
attrs.append(f'Subscription={quoteattr(n.subscription)}')
|
|
98
|
+
if n.note:
|
|
99
|
+
attrs.append(f'Note={quoteattr(n.note)}')
|
|
100
|
+
if not str(nid).startswith("type::") and not str(nid).startswith("ext::"):
|
|
101
|
+
attrs.append(f'ARM_ID={quoteattr(str(nid))}')
|
|
102
|
+
|
|
103
|
+
attr_str = " ".join(attrs)
|
|
104
|
+
|
|
105
|
+
cells.append(
|
|
106
|
+
f' <object {attr_str}>\n'
|
|
107
|
+
f' <mxCell style={quoteattr(style)} vertex="1" parent="1">\n'
|
|
108
|
+
f' <mxGeometry x="{x}" y="{y}" width="{w}" height="{h}" as="geometry"/>\n'
|
|
109
|
+
f' </mxCell>\n'
|
|
110
|
+
f' </object>'
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
for j, e in enumerate(graph.edges):
|
|
114
|
+
s, t = idmap.get(e.source), idmap.get(e.target)
|
|
115
|
+
if not (s and t):
|
|
116
|
+
continue
|
|
117
|
+
model = e.origin == "model"
|
|
118
|
+
rbac = "role:" in e.kind.lower()
|
|
119
|
+
|
|
120
|
+
if model:
|
|
121
|
+
style = EDGE_MODEL_STYLE
|
|
122
|
+
elif rbac:
|
|
123
|
+
style = EDGE_RBAC_STYLE
|
|
124
|
+
else:
|
|
125
|
+
style = EDGE_STYLE
|
|
126
|
+
|
|
127
|
+
label = e.kind + (" (model)" if model else "")
|
|
128
|
+
cells.append(
|
|
129
|
+
f' <mxCell id="e{j}" value={quoteattr(label)} '
|
|
130
|
+
f'style={quoteattr(style)} edge="1" parent="1" '
|
|
131
|
+
f'source="{s}" target="{t}"><mxGeometry relative="1" as="geometry"/></mxCell>'
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
body = "\n".join(cells)
|
|
135
|
+
return (
|
|
136
|
+
'<mxfile host="cloudmap">\n'
|
|
137
|
+
' <diagram id="cloudmap" name="Blast radius">\n'
|
|
138
|
+
' <mxGraphModel dx="800" dy="600" grid="1" gridSize="10" guides="1" '
|
|
139
|
+
'tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" '
|
|
140
|
+
'pageWidth="1600" pageHeight="1200" math="0" shadow="0">\n'
|
|
141
|
+
' <root>\n'
|
|
142
|
+
' <mxCell id="0"/>\n'
|
|
143
|
+
' <mxCell id="1" parent="0"/>\n'
|
|
144
|
+
f'{body}\n'
|
|
145
|
+
' </root>\n'
|
|
146
|
+
' </mxGraphModel>\n'
|
|
147
|
+
' </diagram>\n'
|
|
148
|
+
'</mxfile>\n'
|
|
149
|
+
)
|