cloudwire 0.2.12__tar.gz → 0.2.13__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.
- {cloudwire-0.2.12 → cloudwire-0.2.13}/PKG-INFO +10 -11
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/__init__.py +1 -1
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/errors.py +3 -3
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/graph_store.py +8 -2
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/main.py +2 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scan_jobs.py +3 -1
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanner.py +6 -5
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/cli.py +1 -19
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire.egg-info/PKG-INFO +10 -11
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire.egg-info/requires.txt +0 -2
- {cloudwire-0.2.12 → cloudwire-0.2.13}/pyproject.toml +4 -4
- {cloudwire-0.2.12 → cloudwire-0.2.13}/LICENSE +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/README.md +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/__init__.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/aws_clients.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/hcl_parser.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/models.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/routes/__init__.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/routes/scan.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/routes/tags.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/routes/terraform.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/__init__.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/_utils.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/apigateway.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/appsync.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/cloudfront.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/cognito.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/dynamodb.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/ec2.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/ecs.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/elasticache.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/eventbridge.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/glue.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/iam.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/kinesis.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/lambda_.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/rds.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/redshift.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/route53.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/s3.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/sns.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/sqs.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/stepfunctions.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/scanners/vpc.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/services.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/app/terraform_parser.py +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/static/assets/index-BlarXbuP.css +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/static/assets/index-CJlbfCBD.js +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/static/favicon.svg +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire/static/index.html +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire.egg-info/SOURCES.txt +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire.egg-info/dependency_links.txt +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire.egg-info/entry_points.txt +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/cloudwire.egg-info/top_level.txt +0 -0
- {cloudwire-0.2.12 → cloudwire-0.2.13}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: cloudwire
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.13
|
|
4
4
|
Summary: Scan and visualize your AWS infrastructure as an interactive graph
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/Himanshu-370/cloudwire
|
|
@@ -24,21 +24,20 @@ Classifier: Topic :: System :: Systems Administration
|
|
|
24
24
|
Requires-Python: >=3.9
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
License-File: LICENSE
|
|
27
|
+
Requires-Dist: fastapi>=0.100
|
|
28
|
+
Requires-Dist: uvicorn>=0.20
|
|
29
|
+
Requires-Dist: boto3>=1.26
|
|
30
|
+
Requires-Dist: botocore>=1.29
|
|
31
|
+
Requires-Dist: networkx>=2.6
|
|
32
|
+
Requires-Dist: pydantic>=2.0
|
|
33
|
+
Requires-Dist: click>=8.0
|
|
34
|
+
Requires-Dist: python-multipart>=0.0.5
|
|
35
|
+
Requires-Dist: python-hcl2>=4.3
|
|
27
36
|
Provides-Extra: dev
|
|
28
37
|
Requires-Dist: twine; extra == "dev"
|
|
29
38
|
Requires-Dist: build; extra == "dev"
|
|
30
39
|
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
31
40
|
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
32
|
-
Provides-Extra: dependencies
|
|
33
|
-
Requires-Dist: fastapi>=0.100; extra == "dependencies"
|
|
34
|
-
Requires-Dist: uvicorn>=0.20; extra == "dependencies"
|
|
35
|
-
Requires-Dist: boto3>=1.26; extra == "dependencies"
|
|
36
|
-
Requires-Dist: botocore>=1.29; extra == "dependencies"
|
|
37
|
-
Requires-Dist: networkx>=2.6; extra == "dependencies"
|
|
38
|
-
Requires-Dist: pydantic>=2.0; extra == "dependencies"
|
|
39
|
-
Requires-Dist: click>=8.0; extra == "dependencies"
|
|
40
|
-
Requires-Dist: python-multipart>=0.0.5; extra == "dependencies"
|
|
41
|
-
Requires-Dist: python-hcl2>=4.3; extra == "dependencies"
|
|
42
41
|
|
|
43
42
|
# CloudWire
|
|
44
43
|
|
|
@@ -5,4 +5,4 @@ from importlib.metadata import version, PackageNotFoundError
|
|
|
5
5
|
try:
|
|
6
6
|
__version__ = version("cloudwire")
|
|
7
7
|
except PackageNotFoundError:
|
|
8
|
-
__version__ = "0.2.
|
|
8
|
+
__version__ = "0.2.13" # fallback when running from source without pip install
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
-
from typing import Any, Dict, Optional
|
|
6
|
+
from typing import Any, Dict, NoReturn, Optional
|
|
7
7
|
|
|
8
8
|
from botocore.exceptions import (
|
|
9
9
|
BotoCoreError,
|
|
@@ -63,8 +63,8 @@ def friendly_exception_message(exc: Exception) -> str:
|
|
|
63
63
|
return "Unexpected server error."
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
def handle_tagging_error(exc: Exception, region: str, operation: str):
|
|
67
|
-
"""Convert AWS errors from tagging API to APIError."""
|
|
66
|
+
def handle_tagging_error(exc: Exception, region: str, operation: str) -> NoReturn:
|
|
67
|
+
"""Convert AWS errors from tagging API to APIError. Always raises."""
|
|
68
68
|
from fastapi import status
|
|
69
69
|
|
|
70
70
|
logger.warning("Tag API error in %s (region=%s): %s: %s", operation, region, type(exc).__name__, exc)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import copy
|
|
3
4
|
import logging
|
|
4
5
|
from datetime import datetime, timezone
|
|
5
6
|
from threading import Lock
|
|
@@ -73,9 +74,14 @@ class GraphStore:
|
|
|
73
74
|
return payload
|
|
74
75
|
|
|
75
76
|
def get_graph_payload(self) -> Dict[str, Any]:
|
|
77
|
+
"""Return a deep copy of the cached graph payload.
|
|
78
|
+
|
|
79
|
+
The internal cache is never exposed directly — callers receive an
|
|
80
|
+
independent copy they can safely mutate without corrupting the cache.
|
|
81
|
+
"""
|
|
76
82
|
with self._lock:
|
|
77
83
|
if self._cached_payload is not None:
|
|
78
|
-
return self._cached_payload
|
|
84
|
+
return copy.deepcopy(self._cached_payload)
|
|
79
85
|
nodes = [self._serialize_node(node_id, attrs) for node_id, attrs in self.graph.nodes(data=True)]
|
|
80
86
|
edges = [
|
|
81
87
|
self._serialize_edge(source, target, attrs)
|
|
@@ -86,7 +92,7 @@ class GraphStore:
|
|
|
86
92
|
metadata["edge_count"] = len(edges)
|
|
87
93
|
payload = {"nodes": nodes, "edges": edges, "metadata": metadata}
|
|
88
94
|
self._cached_payload = payload
|
|
89
|
-
return payload
|
|
95
|
+
return copy.deepcopy(payload)
|
|
90
96
|
|
|
91
97
|
def iter_nodes_by_service(self, service: str) -> List[Tuple[str, Dict[str, Any]]]:
|
|
92
98
|
"""Return a snapshot of (node_id, attrs_copy) pairs for a given service."""
|
|
@@ -83,6 +83,8 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|
|
83
83
|
response = await call_next(request)
|
|
84
84
|
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
85
85
|
response.headers["X-Frame-Options"] = "DENY"
|
|
86
|
+
# 'unsafe-inline' for styles is required by the React/Tailwind frontend.
|
|
87
|
+
# Acceptable for a localhost-only tool with no user-generated content.
|
|
86
88
|
response.headers["Content-Security-Policy"] = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';"
|
|
87
89
|
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
|
88
90
|
return response
|
|
@@ -355,7 +355,9 @@ class ScanJobStore:
|
|
|
355
355
|
"finished_at": job.finished_at,
|
|
356
356
|
"error": job.error,
|
|
357
357
|
}
|
|
358
|
-
graph_store = job.graph_store
|
|
358
|
+
graph_store = job.graph_store # stable reference, set at construction and never reassigned
|
|
359
|
+
# Called outside the job store lock to avoid holding it during serialization.
|
|
360
|
+
# Thread-safe because GraphStore has its own internal lock.
|
|
359
361
|
graph_payload = graph_store.get_graph_payload()
|
|
360
362
|
metadata = graph_payload.get("metadata", {})
|
|
361
363
|
return {
|
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
-
import re
|
|
6
5
|
from concurrent.futures import FIRST_COMPLETED, Future, ThreadPoolExecutor, wait
|
|
7
6
|
from dataclasses import dataclass
|
|
8
7
|
from threading import Lock
|
|
@@ -51,9 +50,6 @@ def _sanitize_exc(exc: Exception) -> str:
|
|
|
51
50
|
from .scanners._utils import _safe_list
|
|
52
51
|
|
|
53
52
|
|
|
54
|
-
_ARN_PATTERN = re.compile(r"^arn:aws:[a-z0-9-]+:")
|
|
55
|
-
|
|
56
|
-
|
|
57
53
|
@dataclass
|
|
58
54
|
class ScanExecutionOptions:
|
|
59
55
|
mode: ScanMode = "quick"
|
|
@@ -679,7 +675,9 @@ class AWSGraphScanner(
|
|
|
679
675
|
|
|
680
676
|
# Infer parent→child edges from ARN hierarchy.
|
|
681
677
|
# E.g., arn:...:cluster/j-ABC is parent of arn:...:cluster/j-ABC/step/s-DEF
|
|
682
|
-
|
|
678
|
+
# Capped at 1000 ARNs to avoid O(n²) blowup with large resource sets.
|
|
679
|
+
_MAX_ARNS_FOR_HIERARCHY = 1000
|
|
680
|
+
if 1 < len(discovered_arns) <= _MAX_ARNS_FOR_HIERARCHY:
|
|
683
681
|
sorted_arns = sorted(discovered_arns)
|
|
684
682
|
for i, arn in enumerate(sorted_arns):
|
|
685
683
|
for j in range(i + 1, len(sorted_arns)):
|
|
@@ -693,6 +691,9 @@ class AWSGraphScanner(
|
|
|
693
691
|
elif not candidate.startswith(arn[:arn.rfind("/") + 1] if "/" in arn else arn[:arn.rfind(":") + 1]):
|
|
694
692
|
# No more potential children — different prefix
|
|
695
693
|
break
|
|
694
|
+
elif len(discovered_arns) > _MAX_ARNS_FOR_HIERARCHY:
|
|
695
|
+
logger.info("%s: skipping ARN hierarchy inference (%d ARNs exceeds cap of %d)",
|
|
696
|
+
service_name, len(discovered_arns), _MAX_ARNS_FOR_HIERARCHY)
|
|
696
697
|
|
|
697
698
|
discovered = len(discovered_arns)
|
|
698
699
|
if discovered == 0:
|
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
-
import socket
|
|
7
6
|
import sys
|
|
8
7
|
import threading
|
|
9
8
|
import webbrowser
|
|
10
9
|
|
|
11
|
-
_REQUIRED_PACKAGES = ["click", "uvicorn", "fastapi", "boto3", "pydantic", "networkx"]
|
|
10
|
+
_REQUIRED_PACKAGES = ["click", "uvicorn", "fastapi", "boto3", "pydantic", "networkx", "multipart", "hcl2"]
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
def _check_dependencies() -> None:
|
|
@@ -40,17 +39,6 @@ import uvicorn # noqa: E402
|
|
|
40
39
|
from . import __version__ # noqa: E402
|
|
41
40
|
|
|
42
41
|
|
|
43
|
-
def _port_is_available(host: str, port: int) -> bool:
|
|
44
|
-
"""Return True if the port is free to bind on the given host."""
|
|
45
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
46
|
-
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
47
|
-
try:
|
|
48
|
-
sock.bind((host, port))
|
|
49
|
-
return True
|
|
50
|
-
except OSError:
|
|
51
|
-
return False
|
|
52
|
-
|
|
53
|
-
|
|
54
42
|
def _check_for_update(current: str, result: list) -> None:
|
|
55
43
|
"""Fetch the latest version from PyPI and store it in result[0]. Runs in a background thread."""
|
|
56
44
|
try:
|
|
@@ -109,12 +97,6 @@ def main(port: int, host: str, profile: str | None, region: str, no_browser: boo
|
|
|
109
97
|
# Only set region if not already in environment
|
|
110
98
|
os.environ.setdefault("AWS_DEFAULT_REGION", region)
|
|
111
99
|
|
|
112
|
-
# Port conflict check — fail fast with a clear message
|
|
113
|
-
if not _port_is_available(host, port):
|
|
114
|
-
raise click.ClickException(
|
|
115
|
-
f"Port {port} is already in use. Try a different port with --port <number>."
|
|
116
|
-
)
|
|
117
|
-
|
|
118
100
|
# Start background version check — finishes before uvicorn prints its own output
|
|
119
101
|
_update_result: list = []
|
|
120
102
|
_update_thread = threading.Thread(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: cloudwire
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.13
|
|
4
4
|
Summary: Scan and visualize your AWS infrastructure as an interactive graph
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/Himanshu-370/cloudwire
|
|
@@ -24,21 +24,20 @@ Classifier: Topic :: System :: Systems Administration
|
|
|
24
24
|
Requires-Python: >=3.9
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
License-File: LICENSE
|
|
27
|
+
Requires-Dist: fastapi>=0.100
|
|
28
|
+
Requires-Dist: uvicorn>=0.20
|
|
29
|
+
Requires-Dist: boto3>=1.26
|
|
30
|
+
Requires-Dist: botocore>=1.29
|
|
31
|
+
Requires-Dist: networkx>=2.6
|
|
32
|
+
Requires-Dist: pydantic>=2.0
|
|
33
|
+
Requires-Dist: click>=8.0
|
|
34
|
+
Requires-Dist: python-multipart>=0.0.5
|
|
35
|
+
Requires-Dist: python-hcl2>=4.3
|
|
27
36
|
Provides-Extra: dev
|
|
28
37
|
Requires-Dist: twine; extra == "dev"
|
|
29
38
|
Requires-Dist: build; extra == "dev"
|
|
30
39
|
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
31
40
|
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
32
|
-
Provides-Extra: dependencies
|
|
33
|
-
Requires-Dist: fastapi>=0.100; extra == "dependencies"
|
|
34
|
-
Requires-Dist: uvicorn>=0.20; extra == "dependencies"
|
|
35
|
-
Requires-Dist: boto3>=1.26; extra == "dependencies"
|
|
36
|
-
Requires-Dist: botocore>=1.29; extra == "dependencies"
|
|
37
|
-
Requires-Dist: networkx>=2.6; extra == "dependencies"
|
|
38
|
-
Requires-Dist: pydantic>=2.0; extra == "dependencies"
|
|
39
|
-
Requires-Dist: click>=8.0; extra == "dependencies"
|
|
40
|
-
Requires-Dist: python-multipart>=0.0.5; extra == "dependencies"
|
|
41
|
-
Requires-Dist: python-hcl2>=4.3; extra == "dependencies"
|
|
42
41
|
|
|
43
42
|
# CloudWire
|
|
44
43
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cloudwire"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.13"
|
|
8
8
|
description = "Scan and visualize your AWS infrastructure as an interactive graph"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -25,9 +25,6 @@ classifiers = [
|
|
|
25
25
|
"Topic :: Internet :: WWW/HTTP",
|
|
26
26
|
"Topic :: System :: Systems Administration",
|
|
27
27
|
]
|
|
28
|
-
[project.optional-dependencies]
|
|
29
|
-
dev = ["twine", "build", "ruff>=0.4", "mypy>=1.0"]
|
|
30
|
-
|
|
31
28
|
dependencies = [
|
|
32
29
|
"fastapi>=0.100",
|
|
33
30
|
"uvicorn>=0.20",
|
|
@@ -40,6 +37,9 @@ dependencies = [
|
|
|
40
37
|
"python-hcl2>=4.3",
|
|
41
38
|
]
|
|
42
39
|
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
dev = ["twine", "build", "ruff>=0.4", "mypy>=1.0"]
|
|
42
|
+
|
|
43
43
|
[project.scripts]
|
|
44
44
|
cloudwire = "cloudwire.cli:main"
|
|
45
45
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|