cloudwire 0.2.1__tar.gz → 0.2.3__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 (51) hide show
  1. {cloudwire-0.2.1/cloudwire.egg-info → cloudwire-0.2.3}/PKG-INFO +35 -14
  2. cloudwire-0.2.1/PKG-INFO → cloudwire-0.2.3/README.md +26 -40
  3. {cloudwire-0.2.1 → cloudwire-0.2.3}/cloudwire/__init__.py +1 -1
  4. {cloudwire-0.2.1 → cloudwire-0.2.3}/cloudwire/app/graph_store.py +63 -17
  5. {cloudwire-0.2.1 → cloudwire-0.2.3}/cloudwire/app/main.py +214 -22
  6. {cloudwire-0.2.1 → cloudwire-0.2.3}/cloudwire/app/models.py +24 -16
  7. cloudwire-0.2.3/cloudwire/app/scanner.py +703 -0
  8. cloudwire-0.2.3/cloudwire/app/scanners/__init__.py +4 -0
  9. cloudwire-0.2.3/cloudwire/app/scanners/_utils.py +35 -0
  10. cloudwire-0.2.3/cloudwire/app/scanners/apigateway.py +274 -0
  11. cloudwire-0.2.3/cloudwire/app/scanners/appsync.py +93 -0
  12. cloudwire-0.2.3/cloudwire/app/scanners/cloudfront.py +84 -0
  13. cloudwire-0.2.3/cloudwire/app/scanners/cognito.py +74 -0
  14. cloudwire-0.2.3/cloudwire/app/scanners/dynamodb.py +80 -0
  15. cloudwire-0.2.3/cloudwire/app/scanners/ec2.py +71 -0
  16. cloudwire-0.2.3/cloudwire/app/scanners/ecs.py +100 -0
  17. cloudwire-0.2.3/cloudwire/app/scanners/elasticache.py +40 -0
  18. cloudwire-0.2.3/cloudwire/app/scanners/eventbridge.py +76 -0
  19. cloudwire-0.2.3/cloudwire/app/scanners/glue.py +198 -0
  20. cloudwire-0.2.3/cloudwire/app/scanners/iam.py +28 -0
  21. cloudwire-0.2.3/cloudwire/app/scanners/kinesis.py +32 -0
  22. cloudwire-0.2.3/cloudwire/app/scanners/lambda_.py +326 -0
  23. cloudwire-0.2.3/cloudwire/app/scanners/rds.py +84 -0
  24. cloudwire-0.2.3/cloudwire/app/scanners/redshift.py +50 -0
  25. cloudwire-0.2.3/cloudwire/app/scanners/route53.py +140 -0
  26. cloudwire-0.2.3/cloudwire/app/scanners/s3.py +80 -0
  27. cloudwire-0.2.3/cloudwire/app/scanners/sns.py +55 -0
  28. cloudwire-0.2.3/cloudwire/app/scanners/sqs.py +93 -0
  29. cloudwire-0.2.3/cloudwire/app/scanners/stepfunctions.py +151 -0
  30. cloudwire-0.2.3/cloudwire/app/scanners/vpc.py +247 -0
  31. cloudwire-0.2.3/cloudwire/app/services.py +144 -0
  32. {cloudwire-0.2.1 → cloudwire-0.2.3}/cloudwire/cli.py +38 -3
  33. cloudwire-0.2.3/cloudwire/static/assets/index-C2eRAgEx.js +40 -0
  34. cloudwire-0.2.3/cloudwire/static/assets/index-Dkst_1um.css +1 -0
  35. {cloudwire-0.2.1 → cloudwire-0.2.3}/cloudwire/static/index.html +2 -2
  36. cloudwire-0.2.1/README.md → cloudwire-0.2.3/cloudwire.egg-info/PKG-INFO +61 -6
  37. cloudwire-0.2.3/cloudwire.egg-info/SOURCES.txt +44 -0
  38. cloudwire-0.2.3/cloudwire.egg-info/requires.txt +13 -0
  39. {cloudwire-0.2.1 → cloudwire-0.2.3}/pyproject.toml +9 -8
  40. cloudwire-0.2.1/cloudwire/app/scanner.py +0 -2771
  41. cloudwire-0.2.1/cloudwire/static/assets/index-IhO1P1Kx.js +0 -40
  42. cloudwire-0.2.1/cloudwire/static/assets/index-ojHsU5ur.css +0 -1
  43. cloudwire-0.2.1/cloudwire.egg-info/SOURCES.txt +0 -20
  44. cloudwire-0.2.1/cloudwire.egg-info/requires.txt +0 -13
  45. {cloudwire-0.2.1 → cloudwire-0.2.3}/cloudwire/app/__init__.py +0 -0
  46. {cloudwire-0.2.1 → cloudwire-0.2.3}/cloudwire/app/scan_jobs.py +0 -0
  47. {cloudwire-0.2.1 → cloudwire-0.2.3}/cloudwire/static/favicon.svg +0 -0
  48. {cloudwire-0.2.1 → cloudwire-0.2.3}/cloudwire.egg-info/dependency_links.txt +0 -0
  49. {cloudwire-0.2.1 → cloudwire-0.2.3}/cloudwire.egg-info/entry_points.txt +0 -0
  50. {cloudwire-0.2.1 → cloudwire-0.2.3}/cloudwire.egg-info/top_level.txt +0 -0
  51. {cloudwire-0.2.1 → cloudwire-0.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudwire
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Scan and visualize your AWS infrastructure as an interactive graph
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/hisingh_gwre/cloudwire
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.9
16
16
  Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
19
20
  Classifier: Topic :: Internet :: WWW/HTTP
20
21
  Classifier: Topic :: System :: Systems Administration
21
22
  Requires-Python: >=3.9
@@ -24,20 +25,24 @@ Provides-Extra: dev
24
25
  Requires-Dist: twine; extra == "dev"
25
26
  Requires-Dist: build; extra == "dev"
26
27
  Provides-Extra: dependencies
27
- Requires-Dist: fastapi>=0.115; extra == "dependencies"
28
- Requires-Dist: uvicorn[standard]>=0.34; extra == "dependencies"
29
- Requires-Dist: boto3>=1.37; extra == "dependencies"
30
- Requires-Dist: botocore>=1.37; extra == "dependencies"
31
- Requires-Dist: networkx>=3.4; extra == "dependencies"
32
- Requires-Dist: pydantic>=2.11; extra == "dependencies"
33
- Requires-Dist: click>=8.1; extra == "dependencies"
28
+ Requires-Dist: fastapi>=0.100; extra == "dependencies"
29
+ Requires-Dist: uvicorn>=0.20; extra == "dependencies"
30
+ Requires-Dist: boto3>=1.26; extra == "dependencies"
31
+ Requires-Dist: botocore>=1.29; extra == "dependencies"
32
+ Requires-Dist: networkx>=2.6; extra == "dependencies"
33
+ Requires-Dist: pydantic>=2.0; extra == "dependencies"
34
+ Requires-Dist: click>=8.0; extra == "dependencies"
34
35
 
35
- # Cloudwire
36
+ # CloudWire
36
37
 
37
38
  Scan your AWS account and visualize resource dependencies as an interactive graph — directly in your browser, running entirely on your local machine.
38
39
 
39
40
  No data leaves your system. AWS credentials never leave your terminal. The graph is built locally using your existing credential chain (`~/.aws/credentials`, `aws sso login`, `saml2aws`, `aws-vault` — all work out of the box).
40
41
 
42
+ <p align="center">
43
+ <img src="docs/cloudgraph.svg" alt="CloudWire — AWS infrastructure graph visualization" width="100%">
44
+ </p>
45
+
41
46
  ---
42
47
 
43
48
  ## Install
@@ -58,7 +63,9 @@ That's it. The browser opens automatically at `http://localhost:8080`.
58
63
  - Dark hacker-aesthetic graph canvas with animated data flow
59
64
  - 24 AWS services with dedicated icons, colors, and role badges
60
65
  - Edges represent real relationships — API integrations, event triggers, IAM policy inference, env var references
61
- - Sequential left-to-right flow layout with START/END badges showing where data enters and exits
66
+ - Four layout modes Circular (default), Flow, Swimlane switchable from the graph toolbar
67
+ - VPC network topology with CloudMapper-style diagrams — internet anchors, SG rule edges with port labels, AZ grouping, collapsible containers
68
+ - Tag-based scanning — discover and scan resources by AWS tags with searchable multi-select dropdowns
62
69
  - Click any node to inspect its attributes, incoming/outgoing edges, and resource-specific tooltip
63
70
  - Search, filter by service, highlight upstream/downstream blast radius, find shortest path
64
71
  - Permission errors surfaced clearly — see exactly which IAM policies are missing
@@ -91,6 +98,7 @@ That's it. The browser opens automatically at `http://localhost:8080`.
91
98
  | AppSync | Dedicated — GraphQL APIs |
92
99
  | Secrets Manager | Dedicated |
93
100
  | KMS | Dedicated |
101
+ | VPC Network | Dedicated — VPCs, subnets, security groups, IGWs, NAT GWs, route tables; scoped to referenced VPCs |
94
102
  | ELB | Discovered via CloudFront, Route 53, ECS edges |
95
103
  | Everything else | Generic (tagged resources only) |
96
104
 
@@ -108,7 +116,15 @@ cloudwire/ # Python package (the distributable unit)
108
116
  └── app/ # FastAPI backend
109
117
  ├── main.py # App factory, API routes (/api/*), static serving
110
118
  ├── models.py # Pydantic request/response models
111
- ├── scanner.py # boto3 AWS scannerone function per service
119
+ ├── services.py # Canonical service registrysingle source of truth
120
+ ├── scanner.py # Scan orchestrator, shared helpers, mixin composition
121
+ ├── scanners/ # Per-service scanner modules (mixin classes)
122
+ │ ├── _utils.py # Shared constants (ARN pattern, env var conventions)
123
+ │ ├── apigateway.py # REST + HTTP APIs, integrations, authorizers
124
+ │ ├── lambda_.py # Functions, event sources, IAM policy inference
125
+ │ ├── vpc.py # VPCs, subnets, SGs, IGWs, NATs, route tables
126
+ │ ├── glue.py # Jobs, crawlers, triggers
127
+ │ └── ... # 16 more service scanners (one per AWS service)
112
128
  ├── scan_jobs.py # Async job store with progress tracking
113
129
  └── graph_store.py # networkx graph with thread-safe mutations
114
130
 
@@ -117,12 +133,17 @@ frontend/ # React + Vite source (compiled into cloudwire/s
117
133
  │ ├── pages/CloudWirePage.jsx # Main page — orchestrates all state
118
134
  │ ├── components/
119
135
  │ │ ├── graph/ # GraphCanvas, GraphNode, GraphEdge, Minimap, Legend
120
- │ │ └── layout/ # TopBar, ServiceSidebar, InspectorPanel
136
+ │ │ ├── layout/ # TopBar, ServiceSidebar, InspectorPanel, TagFilterBar
137
+ │ │ └── ErrorBoundary.jsx # React error boundary for graceful pane crashes
121
138
  │ ├── hooks/
122
139
  │ │ ├── useScanPolling.js # Scan lifecycle, polling, graph data state
140
+ │ │ ├── useTagDiscovery.js # Tag-based resource discovery
141
+ │ │ ├── useGraphPipeline.js # Graph filtering, clustering, layout pipeline
142
+ │ │ ├── usePathFinder.js # Shortest-path mode state management
143
+ │ │ ├── useClickOutside.js # Shared click-outside hook
123
144
  │ │ └── useGraphViewport.js # Pan/zoom viewport state
124
145
  │ ├── lib/
125
- │ │ ├── graphTransforms.js # Layout algorithms (circular, flow, swimlane)
146
+ │ │ ├── graphTransforms.js # Layout algorithms, annotations, container collapse
126
147
  │ │ ├── serviceVisuals.jsx # Service icon + color map
127
148
  │ │ └── awsRegions.js # AWS region list
128
149
  │ └── styles/graph.css # All UI styles
@@ -172,7 +193,7 @@ This starts the FastAPI backend on `:8000` (with `--reload`) and the Vite dev se
172
193
 
173
194
  | Area | Where to edit |
174
195
  |------|--------------|
175
- | Add a new AWS service scanner | `cloudwire/app/scanner.py`add a `_scan_<service>` method and register it in `self.service_scanners` |
196
+ | Add a new AWS service scanner | `cloudwire/app/scanners/`create a mixin class, import it in `scanner.py`, add to the class bases and `service_scanners` dict |
176
197
  | Change graph layout | `frontend/src/lib/graphTransforms.js` |
177
198
  | Add a new UI component | `frontend/src/components/` |
178
199
  | Change API routes | `cloudwire/app/main.py` — all routes are under the `/api` prefix |
@@ -1,43 +1,13 @@
1
- Metadata-Version: 2.4
2
- Name: cloudwire
3
- Version: 0.2.1
4
- Summary: Scan and visualize your AWS infrastructure as an interactive graph
5
- License-Expression: MIT
6
- Project-URL: Homepage, https://github.com/hisingh_gwre/cloudwire
7
- Project-URL: Issues, https://github.com/hisingh_gwre/cloudwire/issues
8
- Keywords: aws,cloud,visualization,graph,infrastructure
9
- Classifier: Development Status :: 3 - Alpha
10
- Classifier: Environment :: Console
11
- Classifier: Environment :: Web Environment
12
- Classifier: Intended Audience :: Developers
13
- Classifier: Intended Audience :: System Administrators
14
- Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.9
16
- Classifier: Programming Language :: Python :: 3.10
17
- Classifier: Programming Language :: Python :: 3.11
18
- Classifier: Programming Language :: Python :: 3.12
19
- Classifier: Topic :: Internet :: WWW/HTTP
20
- Classifier: Topic :: System :: Systems Administration
21
- Requires-Python: >=3.9
22
- Description-Content-Type: text/markdown
23
- Provides-Extra: dev
24
- Requires-Dist: twine; extra == "dev"
25
- Requires-Dist: build; extra == "dev"
26
- Provides-Extra: dependencies
27
- Requires-Dist: fastapi>=0.115; extra == "dependencies"
28
- Requires-Dist: uvicorn[standard]>=0.34; extra == "dependencies"
29
- Requires-Dist: boto3>=1.37; extra == "dependencies"
30
- Requires-Dist: botocore>=1.37; extra == "dependencies"
31
- Requires-Dist: networkx>=3.4; extra == "dependencies"
32
- Requires-Dist: pydantic>=2.11; extra == "dependencies"
33
- Requires-Dist: click>=8.1; extra == "dependencies"
34
-
35
- # Cloudwire
1
+ # CloudWire
36
2
 
37
3
  Scan your AWS account and visualize resource dependencies as an interactive graph — directly in your browser, running entirely on your local machine.
38
4
 
39
5
  No data leaves your system. AWS credentials never leave your terminal. The graph is built locally using your existing credential chain (`~/.aws/credentials`, `aws sso login`, `saml2aws`, `aws-vault` — all work out of the box).
40
6
 
7
+ <p align="center">
8
+ <img src="docs/cloudgraph.svg" alt="CloudWire — AWS infrastructure graph visualization" width="100%">
9
+ </p>
10
+
41
11
  ---
42
12
 
43
13
  ## Install
@@ -58,7 +28,9 @@ That's it. The browser opens automatically at `http://localhost:8080`.
58
28
  - Dark hacker-aesthetic graph canvas with animated data flow
59
29
  - 24 AWS services with dedicated icons, colors, and role badges
60
30
  - Edges represent real relationships — API integrations, event triggers, IAM policy inference, env var references
61
- - Sequential left-to-right flow layout with START/END badges showing where data enters and exits
31
+ - Four layout modes Circular (default), Flow, Swimlane switchable from the graph toolbar
32
+ - VPC network topology with CloudMapper-style diagrams — internet anchors, SG rule edges with port labels, AZ grouping, collapsible containers
33
+ - Tag-based scanning — discover and scan resources by AWS tags with searchable multi-select dropdowns
62
34
  - Click any node to inspect its attributes, incoming/outgoing edges, and resource-specific tooltip
63
35
  - Search, filter by service, highlight upstream/downstream blast radius, find shortest path
64
36
  - Permission errors surfaced clearly — see exactly which IAM policies are missing
@@ -91,6 +63,7 @@ That's it. The browser opens automatically at `http://localhost:8080`.
91
63
  | AppSync | Dedicated — GraphQL APIs |
92
64
  | Secrets Manager | Dedicated |
93
65
  | KMS | Dedicated |
66
+ | VPC Network | Dedicated — VPCs, subnets, security groups, IGWs, NAT GWs, route tables; scoped to referenced VPCs |
94
67
  | ELB | Discovered via CloudFront, Route 53, ECS edges |
95
68
  | Everything else | Generic (tagged resources only) |
96
69
 
@@ -108,7 +81,15 @@ cloudwire/ # Python package (the distributable unit)
108
81
  └── app/ # FastAPI backend
109
82
  ├── main.py # App factory, API routes (/api/*), static serving
110
83
  ├── models.py # Pydantic request/response models
111
- ├── scanner.py # boto3 AWS scannerone function per service
84
+ ├── services.py # Canonical service registrysingle source of truth
85
+ ├── scanner.py # Scan orchestrator, shared helpers, mixin composition
86
+ ├── scanners/ # Per-service scanner modules (mixin classes)
87
+ │ ├── _utils.py # Shared constants (ARN pattern, env var conventions)
88
+ │ ├── apigateway.py # REST + HTTP APIs, integrations, authorizers
89
+ │ ├── lambda_.py # Functions, event sources, IAM policy inference
90
+ │ ├── vpc.py # VPCs, subnets, SGs, IGWs, NATs, route tables
91
+ │ ├── glue.py # Jobs, crawlers, triggers
92
+ │ └── ... # 16 more service scanners (one per AWS service)
112
93
  ├── scan_jobs.py # Async job store with progress tracking
113
94
  └── graph_store.py # networkx graph with thread-safe mutations
114
95
 
@@ -117,12 +98,17 @@ frontend/ # React + Vite source (compiled into cloudwire/s
117
98
  │ ├── pages/CloudWirePage.jsx # Main page — orchestrates all state
118
99
  │ ├── components/
119
100
  │ │ ├── graph/ # GraphCanvas, GraphNode, GraphEdge, Minimap, Legend
120
- │ │ └── layout/ # TopBar, ServiceSidebar, InspectorPanel
101
+ │ │ ├── layout/ # TopBar, ServiceSidebar, InspectorPanel, TagFilterBar
102
+ │ │ └── ErrorBoundary.jsx # React error boundary for graceful pane crashes
121
103
  │ ├── hooks/
122
104
  │ │ ├── useScanPolling.js # Scan lifecycle, polling, graph data state
105
+ │ │ ├── useTagDiscovery.js # Tag-based resource discovery
106
+ │ │ ├── useGraphPipeline.js # Graph filtering, clustering, layout pipeline
107
+ │ │ ├── usePathFinder.js # Shortest-path mode state management
108
+ │ │ ├── useClickOutside.js # Shared click-outside hook
123
109
  │ │ └── useGraphViewport.js # Pan/zoom viewport state
124
110
  │ ├── lib/
125
- │ │ ├── graphTransforms.js # Layout algorithms (circular, flow, swimlane)
111
+ │ │ ├── graphTransforms.js # Layout algorithms, annotations, container collapse
126
112
  │ │ ├── serviceVisuals.jsx # Service icon + color map
127
113
  │ │ └── awsRegions.js # AWS region list
128
114
  │ └── styles/graph.css # All UI styles
@@ -172,7 +158,7 @@ This starts the FastAPI backend on `:8000` (with `--reload`) and the Vite dev se
172
158
 
173
159
  | Area | Where to edit |
174
160
  |------|--------------|
175
- | Add a new AWS service scanner | `cloudwire/app/scanner.py`add a `_scan_<service>` method and register it in `self.service_scanners` |
161
+ | Add a new AWS service scanner | `cloudwire/app/scanners/`create a mixin class, import it in `scanner.py`, add to the class bases and `service_scanners` dict |
176
162
  | Change graph layout | `frontend/src/lib/graphTransforms.js` |
177
163
  | Add a new UI component | `frontend/src/components/` |
178
164
  | Change API routes | `cloudwire/app/main.py` — all routes are under the `/api` prefix |
@@ -1,3 +1,3 @@
1
1
  """CloudWire — scan and visualize your AWS infrastructure."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.2.3"
@@ -1,11 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  from datetime import datetime, timezone
4
5
  from threading import Lock
5
- from typing import Any, Dict, List, Set
6
+ from typing import Any, Dict, List, Set, Tuple
6
7
 
7
8
  import networkx as nx
8
9
 
10
+ logger = logging.getLogger(__name__)
11
+
9
12
 
10
13
  class GraphStore:
11
14
  def __init__(self) -> None:
@@ -71,6 +74,27 @@ class GraphStore:
71
74
  metadata["edge_count"] = len(edges)
72
75
  return {"nodes": nodes, "edges": edges, "metadata": metadata}
73
76
 
77
+ def iter_nodes_by_service(self, service: str) -> List[Tuple[str, Dict[str, Any]]]:
78
+ """Return a snapshot of (node_id, attrs_copy) pairs for a given service."""
79
+ with self._lock:
80
+ return [
81
+ (nid, dict(attrs))
82
+ for nid, attrs in self.graph.nodes(data=True)
83
+ if attrs.get("service") == service
84
+ ]
85
+
86
+ def snapshot_graph(self) -> "nx.DiGraph":
87
+ """Return a shallow copy of the graph for read-only traversal."""
88
+ with self._lock:
89
+ return self.graph.copy()
90
+
91
+ def batch_update_nodes(self, updates: List[Tuple[str, Dict[str, Any]]]) -> None:
92
+ """Apply attribute updates to multiple nodes atomically."""
93
+ with self._lock:
94
+ for node_id, attrs in updates:
95
+ if self.graph.has_node(node_id):
96
+ self.graph.nodes[node_id].update(attrs)
97
+
74
98
  def _node_matches_arns(self, node_id: str, attrs: Dict[str, Any], allowed_arns: Set[str]) -> bool:
75
99
  """Check if a node matches any of the allowed ARNs.
76
100
 
@@ -91,39 +115,46 @@ class GraphStore:
91
115
  return True
92
116
  return False
93
117
 
94
- def filter_by_arns(self, allowed_arns: Set[str]) -> int:
118
+ def filter_by_arns(self, allowed_arns: Set[str]) -> Dict[str, int]:
95
119
  """Remove nodes that don't match the allowed ARNs, preserving neighbors.
96
120
 
97
121
  Keeps:
98
122
  - Nodes whose ARN matches the allowed set (the "seed" nodes)
99
- - Direct neighbors of seed nodes (1-hop) so connected context is visible
100
- - VPC infrastructure ancestors of kept nodes (so VPC containers, IGWs,
101
- route tables, and Internet anchor nodes remain for topology context)
102
- - Nodes without any ARN-like attribute (synthetic/connector nodes)
103
- Returns the number of nodes removed.
123
+ - Direct neighbors of seed nodes (1-hop), marked ``kept_by_proximity=True``
124
+ - VPC infrastructure ancestors of kept nodes
125
+ Returns dict with ``seeds``, ``neighbors``, ``removed``, ``total`` counts.
104
126
  """
105
127
  with self._lock:
128
+ total = self.graph.number_of_nodes()
129
+ logger.debug(
130
+ "filter_by_arns: %d graph nodes, %d allowed ARNs",
131
+ total, len(allowed_arns),
132
+ )
106
133
  # Phase 1: identify seed nodes (directly matched by ARN)
107
134
  seed_ids: Set[str] = set()
108
- no_arn_ids: Set[str] = set()
109
135
  for node_id, attrs in self.graph.nodes(data=True):
110
136
  if self._node_matches_arns(node_id, attrs, allowed_arns):
111
137
  seed_ids.add(node_id)
112
- elif not attrs.get("real_arn") and not attrs.get("arn"):
113
- no_arn_ids.add(node_id)
138
+ # Note: nodes without arn/real_arn that have NO edges are
139
+ # phantom inferred nodes — they will be removed unless they
140
+ # are neighbors of seeds. This avoids polluting the tag graph
141
+ # with Lambda env-var inferred phantom nodes.
114
142
 
115
143
  # Phase 2: expand to direct neighbors of seeds (1-hop)
116
- keep_ids = set(seed_ids) | no_arn_ids
144
+ # Mark them so the frontend can visually distinguish proximity nodes
145
+ neighbor_ids: Set[str] = set()
117
146
  for seed_id in seed_ids:
118
147
  for neighbor in self.graph.predecessors(seed_id):
119
- keep_ids.add(neighbor)
148
+ if neighbor not in seed_ids:
149
+ neighbor_ids.add(neighbor)
120
150
  for neighbor in self.graph.successors(seed_id):
121
- keep_ids.add(neighbor)
151
+ if neighbor not in seed_ids:
152
+ neighbor_ids.add(neighbor)
153
+
154
+ keep_ids = seed_ids | neighbor_ids
122
155
 
123
156
  # Phase 3: walk VPC infrastructure ancestors so topology context
124
157
  # (VPC → subnet → resource, IGW → VPC, RTB → subnet) stays intact.
125
- # For any kept VPC infra node, also keep its predecessors/successors
126
- # that are VPC infra, up the containment chain.
127
158
  vpc_frontier = [
128
159
  nid for nid in keep_ids
129
160
  if self.graph.nodes[nid].get("service") == "vpc"
@@ -136,6 +167,7 @@ class GraphStore:
136
167
  attrs = self.graph.nodes[neighbor]
137
168
  if attrs.get("service") == "vpc":
138
169
  keep_ids.add(neighbor)
170
+ neighbor_ids.add(neighbor)
139
171
  visited.add(neighbor)
140
172
  vpc_frontier.append(neighbor)
141
173
  for neighbor in self.graph.successors(nid):
@@ -143,17 +175,31 @@ class GraphStore:
143
175
  attrs = self.graph.nodes[neighbor]
144
176
  if attrs.get("service") == "vpc":
145
177
  keep_ids.add(neighbor)
178
+ neighbor_ids.add(neighbor)
146
179
  visited.add(neighbor)
147
180
  vpc_frontier.append(neighbor)
148
181
 
149
- # Phase 4: remove everything else
182
+ # Phase 4: mark proximity nodes and remove everything else
183
+ for nid in neighbor_ids:
184
+ if self.graph.has_node(nid):
185
+ self.graph.nodes[nid]["kept_by_proximity"] = True
186
+
150
187
  nodes_to_remove = [
151
188
  node_id for node_id in self.graph.nodes()
152
189
  if node_id not in keep_ids
153
190
  ]
154
191
  for node_id in nodes_to_remove:
155
192
  self.graph.remove_node(node_id)
156
- return len(nodes_to_remove)
193
+ logger.debug(
194
+ "filter_by_arns: seeds=%d, neighbors=%d, kept=%d, removed=%d",
195
+ len(seed_ids), len(neighbor_ids), len(keep_ids), len(nodes_to_remove),
196
+ )
197
+ return {
198
+ "seeds": len(seed_ids),
199
+ "neighbors": len(neighbor_ids),
200
+ "removed": len(nodes_to_remove),
201
+ "total": total,
202
+ }
157
203
 
158
204
  def get_resource_payload(self, resource_id: str) -> Dict[str, Any]:
159
205
  with self._lock: