plexavo 0.2.0__tar.gz → 0.2.1__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.
- {plexavo-0.2.0 → plexavo-0.2.1}/PKG-INFO +1 -1
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/__init__.py +1 -1
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/auth.py +9 -1
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/interactive.py +42 -12
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo.egg-info/PKG-INFO +1 -1
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo.egg-info/SOURCES.txt +1 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/pyproject.toml +1 -1
- plexavo-0.2.1/tests/test_auth_offline.py +91 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/LICENSE +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/README.md +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/aws_profile_setup.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/checks/__init__.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/checks/encryption.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/checks/iam.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/checks/iam_hygiene.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/checks/logging.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/checks/network.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/checks/storage.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/checks/usage.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/cli.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/findings.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/principals.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/report/__init__.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/report/ai_narration.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/report/fonts/DejaVuSans-Bold.ttf +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/report/fonts/DejaVuSans-BoldOblique.ttf +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/report/fonts/DejaVuSans-Oblique.ttf +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/report/fonts/DejaVuSans.ttf +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/report/fonts/GEIST-FONT-LICENSE.txt +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/report/html_report.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/report/pdf.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/report/templates/report.html.j2 +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo/scoring.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo.egg-info/dependency_links.txt +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo.egg-info/entry_points.txt +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo.egg-info/requires.txt +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/plexavo.egg-info/top_level.txt +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/setup.cfg +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/tests/test_ai_narration_offline.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/tests/test_encryption_offline.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/tests/test_iam_hygiene_offline.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/tests/test_iam_offline.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/tests/test_logging_offline.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/tests/test_network_offline.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/tests/test_report_offline.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/tests/test_scoring.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/tests/test_storage_offline.py +0 -0
- {plexavo-0.2.0 → plexavo-0.2.1}/tests/test_usage_offline.py +0 -0
|
@@ -11,7 +11,7 @@ your own credentials, and nothing leaves your machine.
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import boto3
|
|
14
|
-
from botocore.exceptions import NoCredentialsError, ProfileNotFound
|
|
14
|
+
from botocore.exceptions import ClientError, NoCredentialsError, ProfileNotFound
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def get_local_session(profile_name: str | None = None, region: str | None = None) -> boto3.Session:
|
|
@@ -50,6 +50,14 @@ def get_local_session(profile_name: str | None = None, region: str | None = None
|
|
|
50
50
|
f"{f'--profile {profile_name} ' if profile_name else ''}"
|
|
51
51
|
"to set them up."
|
|
52
52
|
) from e
|
|
53
|
+
except ClientError as e:
|
|
54
|
+
code = e.response.get("Error", {}).get("Code", "Unknown")
|
|
55
|
+
raise RuntimeError(
|
|
56
|
+
f"AWS rejected these credentials ({code}). The access key may "
|
|
57
|
+
"be invalid, deactivated, or deleted — run `aws configure "
|
|
58
|
+
f"{f'--profile {profile_name} ' if profile_name else ''}"
|
|
59
|
+
"` to update them."
|
|
60
|
+
) from e
|
|
53
61
|
|
|
54
62
|
return boto3.Session(profile_name=profile_name, region_name=resolved_region)
|
|
55
63
|
|
|
@@ -16,10 +16,10 @@ import os
|
|
|
16
16
|
|
|
17
17
|
import boto3
|
|
18
18
|
from rich import box
|
|
19
|
-
from rich.
|
|
20
|
-
from rich.console import Console
|
|
19
|
+
from rich.console import Console, Group
|
|
21
20
|
from rich.panel import Panel
|
|
22
21
|
from rich.prompt import Confirm, IntPrompt, Prompt
|
|
22
|
+
from rich.table import Table
|
|
23
23
|
from rich.text import Text
|
|
24
24
|
|
|
25
25
|
from plexavo import __version__
|
|
@@ -42,27 +42,57 @@ FEATURES = [
|
|
|
42
42
|
"100% local scanning — your AWS credentials never leave this machine",
|
|
43
43
|
]
|
|
44
44
|
|
|
45
|
-
def
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
def _gradient_wordmark(word: str, min_shade: int = 90, max_shade: int = 255) -> Text:
|
|
46
|
+
"""Letter-spaced word with a symmetric grey -> white -> grey shimmer,
|
|
47
|
+
one shade per letter (a smooth peak at the center, not a linear ramp),
|
|
48
|
+
e.g. "P L E X A V O" fading brightest at the middle letter."""
|
|
49
|
+
text = Text()
|
|
50
|
+
n = len(word)
|
|
51
|
+
for i, ch in enumerate(word):
|
|
52
|
+
t = 1 - abs((i / (n - 1)) * 2 - 1) if n > 1 else 1.0
|
|
53
|
+
shade = round(min_shade + t * (max_shade - min_shade))
|
|
54
|
+
text.append(ch, style=f"bold #{shade:02x}{shade:02x}{shade:02x}")
|
|
55
|
+
if i != n - 1:
|
|
56
|
+
text.append(" ")
|
|
57
|
+
return text
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
CLOUD_ART = (
|
|
61
|
+
" .--. \n"
|
|
62
|
+
" .-( ). \n"
|
|
63
|
+
" (___.__)__) "
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _header_row() -> Table:
|
|
68
|
+
"""Wordmark pinned to the left corner, a small ASCII cloud floating
|
|
69
|
+
in the opposite corner — a quiet logomark, not a centerpiece."""
|
|
70
|
+
wordmark = _gradient_wordmark("PLEXAVO")
|
|
71
|
+
wordmark.append(" ")
|
|
72
|
+
wordmark.append(f"v{__version__}", style="grey62")
|
|
73
|
+
|
|
74
|
+
grid = Table.grid(expand=True)
|
|
75
|
+
grid.add_column(ratio=1)
|
|
76
|
+
grid.add_column(justify="right")
|
|
77
|
+
grid.add_row(wordmark, Text(CLOUD_ART, style="grey50"))
|
|
78
|
+
return grid
|
|
79
|
+
|
|
50
80
|
|
|
81
|
+
def _splash_panel() -> Panel:
|
|
51
82
|
body = Text()
|
|
52
|
-
body.append_text(header)
|
|
53
|
-
body.append("\n")
|
|
54
83
|
body.append("AWS Security Reimagined", style="italic grey74")
|
|
55
84
|
body.append("\n")
|
|
56
85
|
body.append(WEBSITE, style="underline grey62")
|
|
57
86
|
body.append("\n\n")
|
|
58
|
-
body.append("
|
|
87
|
+
body.append("─" * 50, style="grey42")
|
|
88
|
+
body.append("\n\n")
|
|
59
89
|
for i, feature in enumerate(FEATURES):
|
|
60
|
-
body.append(f"
|
|
90
|
+
body.append(f" ▸ {feature}", style="grey66")
|
|
61
91
|
if i != len(FEATURES) - 1:
|
|
62
92
|
body.append("\n")
|
|
63
93
|
|
|
64
94
|
return Panel(
|
|
65
|
-
|
|
95
|
+
Group(_header_row(), body),
|
|
66
96
|
border_style="grey50",
|
|
67
97
|
box=box.ROUNDED,
|
|
68
98
|
padding=(1, 4),
|
|
@@ -34,6 +34,7 @@ plexavo/report/fonts/DejaVuSans.ttf
|
|
|
34
34
|
plexavo/report/fonts/GEIST-FONT-LICENSE.txt
|
|
35
35
|
plexavo/report/templates/report.html.j2
|
|
36
36
|
tests/test_ai_narration_offline.py
|
|
37
|
+
tests/test_auth_offline.py
|
|
37
38
|
tests/test_encryption_offline.py
|
|
38
39
|
tests/test_iam_hygiene_offline.py
|
|
39
40
|
tests/test_iam_offline.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "plexavo"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Open-source AWS misconfiguration scanner — runs with your own local AWS credentials, nothing sent to anyone else."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "AGPL-3.0-or-later" }
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Offline tests for auth.py. No real AWS calls — boto3.Session.client is
|
|
2
|
+
monkeypatched to raise the errors AWS itself would raise, to prove
|
|
3
|
+
get_local_session converts them into a clear RuntimeError instead of
|
|
4
|
+
letting them crash the caller.
|
|
5
|
+
|
|
6
|
+
Run: python test_auth_offline.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import boto3
|
|
10
|
+
from botocore.exceptions import ClientError, NoCredentialsError
|
|
11
|
+
from plexavo.auth import get_local_session
|
|
12
|
+
|
|
13
|
+
failures = 0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def assert_true(cond, msg):
|
|
17
|
+
global failures
|
|
18
|
+
status = "PASS" if cond else "FAIL"
|
|
19
|
+
print(f"[{status}] {msg}")
|
|
20
|
+
if not cond:
|
|
21
|
+
failures += 1
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _RaisingClient:
|
|
25
|
+
def __init__(self, exc):
|
|
26
|
+
self._exc = exc
|
|
27
|
+
|
|
28
|
+
def get_caller_identity(self):
|
|
29
|
+
raise self._exc
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _patch_client(monkeypatch_exc):
|
|
33
|
+
original_client = boto3.Session.client
|
|
34
|
+
|
|
35
|
+
def fake_client(self, service_name, *args, **kwargs):
|
|
36
|
+
if service_name == "sts":
|
|
37
|
+
return _RaisingClient(monkeypatch_exc)
|
|
38
|
+
return original_client(self, service_name, *args, **kwargs)
|
|
39
|
+
|
|
40
|
+
return original_client, fake_client
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# --- InvalidClientTokenId (deactivated/deleted access key) surfaces as a clear RuntimeError ---
|
|
44
|
+
original_client, fake_client = _patch_client(
|
|
45
|
+
ClientError(
|
|
46
|
+
{"Error": {"Code": "InvalidClientTokenId", "Message": "The security token included in the request is invalid."}},
|
|
47
|
+
"GetCallerIdentity",
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
boto3.Session.region_name = property(lambda self: "us-east-1")
|
|
51
|
+
boto3.Session.client = fake_client
|
|
52
|
+
try:
|
|
53
|
+
raised = None
|
|
54
|
+
try:
|
|
55
|
+
# profile_name=None: boto3.Session() itself always succeeds
|
|
56
|
+
# regardless of what's in this machine's real ~/.aws config —
|
|
57
|
+
# only the (monkeypatched) sts call should fail.
|
|
58
|
+
get_local_session(profile_name=None)
|
|
59
|
+
except RuntimeError as e:
|
|
60
|
+
raised = e
|
|
61
|
+
except Exception as e:
|
|
62
|
+
raised = e
|
|
63
|
+
|
|
64
|
+
assert_true(isinstance(raised, RuntimeError), "ClientError (InvalidClientTokenId) is converted to RuntimeError, not left to crash")
|
|
65
|
+
assert_true(raised is not None and "InvalidClientTokenId" in str(raised), "RuntimeError message names the AWS error code")
|
|
66
|
+
finally:
|
|
67
|
+
boto3.Session.client = original_client
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# --- NoCredentialsError still produces the pre-existing clear message ---
|
|
71
|
+
original_client, fake_client = _patch_client(NoCredentialsError())
|
|
72
|
+
boto3.Session.client = fake_client
|
|
73
|
+
try:
|
|
74
|
+
raised = None
|
|
75
|
+
try:
|
|
76
|
+
get_local_session(profile_name=None)
|
|
77
|
+
except RuntimeError as e:
|
|
78
|
+
raised = e
|
|
79
|
+
except Exception as e:
|
|
80
|
+
raised = e
|
|
81
|
+
|
|
82
|
+
assert_true(isinstance(raised, RuntimeError), "NoCredentialsError still converts to RuntimeError (regression check)")
|
|
83
|
+
assert_true(raised is not None and "No AWS credentials found" in str(raised), "NoCredentialsError message unchanged")
|
|
84
|
+
finally:
|
|
85
|
+
boto3.Session.client = original_client
|
|
86
|
+
del boto3.Session.region_name
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
print(f"\n{'ALL PASSED' if failures == 0 else f'{failures} FAILURE(S)'}")
|
|
90
|
+
import sys
|
|
91
|
+
sys.exit(1 if failures else 0)
|
|
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
|