plexavo 0.2.5__tar.gz → 0.2.7__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.5 → plexavo-0.2.7}/PKG-INFO +11 -3
- {plexavo-0.2.5 → plexavo-0.2.7}/README.md +10 -2
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/__init__.py +1 -1
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/checks/storage.py +35 -2
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/cli.py +2 -2
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/report/ai_narration.py +29 -3
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo.egg-info/PKG-INFO +11 -3
- {plexavo-0.2.5 → plexavo-0.2.7}/pyproject.toml +1 -1
- {plexavo-0.2.5 → plexavo-0.2.7}/tests/test_ai_narration_offline.py +14 -2
- {plexavo-0.2.5 → plexavo-0.2.7}/tests/test_storage_offline.py +28 -1
- {plexavo-0.2.5 → plexavo-0.2.7}/LICENSE +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/__main__.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/auth.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/aws_profile_setup.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/checks/__init__.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/checks/encryption.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/checks/iam.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/checks/iam_hygiene.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/checks/logging.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/checks/network.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/checks/usage.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/findings.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/interactive.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/principals.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/report/__init__.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/report/fonts/DejaVuSans-Bold.ttf +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/report/fonts/DejaVuSans-BoldOblique.ttf +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/report/fonts/DejaVuSans-Oblique.ttf +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/report/fonts/DejaVuSans.ttf +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/report/fonts/GEIST-FONT-LICENSE.txt +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/report/html_report.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/report/pdf.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/report/templates/report.html.j2 +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo/scoring.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo.egg-info/SOURCES.txt +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo.egg-info/dependency_links.txt +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo.egg-info/entry_points.txt +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo.egg-info/requires.txt +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/plexavo.egg-info/top_level.txt +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/setup.cfg +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/tests/test_auth_offline.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/tests/test_cli_entrypoint.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/tests/test_encryption_offline.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/tests/test_iam_hygiene_offline.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/tests/test_iam_offline.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/tests/test_logging_offline.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/tests/test_network_offline.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/tests/test_report_offline.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/tests/test_scoring.py +0 -0
- {plexavo-0.2.5 → plexavo-0.2.7}/tests/test_usage_offline.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plexavo
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Open-source AWS misconfiguration scanner — runs with your own local AWS credentials, nothing sent to anyone else.
|
|
5
5
|
Author: Kavee
|
|
6
6
|
License: AGPL-3.0-or-later
|
|
@@ -53,14 +53,14 @@ can read, and it's entirely optional. See [Cost](#cost).
|
|
|
53
53
|
|
|
54
54
|
## What it checks
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
32 checks across 6 categories, run against real AWS accounts:
|
|
57
57
|
|
|
58
58
|
- **IAM**: privilege escalation paths, wildcard admin, cross-account
|
|
59
59
|
trust, root usage, dormant credentials
|
|
60
60
|
- **Network**: security groups and RDS instances exposed to the
|
|
61
61
|
internet
|
|
62
62
|
- **Storage**: public S3 buckets, via ACLs, bucket policies, or missing
|
|
63
|
-
Block Public Access
|
|
63
|
+
Block Public Access; buckets with no access logging configured
|
|
64
64
|
- **Encryption**: unencrypted EBS volumes, RDS instances, S3 buckets
|
|
65
65
|
- **Logging**: CloudTrail coverage and encryption, GuardDuty status
|
|
66
66
|
- **Usage**: permissions granted but never used, roles nobody has
|
|
@@ -167,6 +167,14 @@ uv pip install -e .
|
|
|
167
167
|
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the pattern used to add a
|
|
168
168
|
new check.
|
|
169
169
|
|
|
170
|
+
## Break Plexavo
|
|
171
|
+
|
|
172
|
+
Think you can make Plexavo miss something, or give confusing guidance?
|
|
173
|
+
[Report it](../../issues/new?template=break-plexavo.yml). Every
|
|
174
|
+
confirmed, genuinely new finding gets fixed and shipped, and you get a
|
|
175
|
+
permanent credit in the [Hall of Bugs](HALL_OF_BUGS.md). No bounty,
|
|
176
|
+
public credit only.
|
|
177
|
+
|
|
170
178
|
## Security
|
|
171
179
|
|
|
172
180
|
Found a vulnerability in the tool itself, not a misconfiguration in your
|
|
@@ -24,14 +24,14 @@ can read, and it's entirely optional. See [Cost](#cost).
|
|
|
24
24
|
|
|
25
25
|
## What it checks
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
32 checks across 6 categories, run against real AWS accounts:
|
|
28
28
|
|
|
29
29
|
- **IAM**: privilege escalation paths, wildcard admin, cross-account
|
|
30
30
|
trust, root usage, dormant credentials
|
|
31
31
|
- **Network**: security groups and RDS instances exposed to the
|
|
32
32
|
internet
|
|
33
33
|
- **Storage**: public S3 buckets, via ACLs, bucket policies, or missing
|
|
34
|
-
Block Public Access
|
|
34
|
+
Block Public Access; buckets with no access logging configured
|
|
35
35
|
- **Encryption**: unencrypted EBS volumes, RDS instances, S3 buckets
|
|
36
36
|
- **Logging**: CloudTrail coverage and encryption, GuardDuty status
|
|
37
37
|
- **Usage**: permissions granted but never used, roles nobody has
|
|
@@ -138,6 +138,14 @@ uv pip install -e .
|
|
|
138
138
|
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the pattern used to add a
|
|
139
139
|
new check.
|
|
140
140
|
|
|
141
|
+
## Break Plexavo
|
|
142
|
+
|
|
143
|
+
Think you can make Plexavo miss something, or give confusing guidance?
|
|
144
|
+
[Report it](../../issues/new?template=break-plexavo.yml). Every
|
|
145
|
+
confirmed, genuinely new finding gets fixed and shipped, and you get a
|
|
146
|
+
permanent credit in the [Hall of Bugs](HALL_OF_BUGS.md). No bounty,
|
|
147
|
+
public credit only.
|
|
148
|
+
|
|
141
149
|
## Security
|
|
142
150
|
|
|
143
151
|
Found a vulnerability in the tool itself, not a misconfiguration in your
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
"""Category 3: Storage Exposure — checks 19-21
|
|
1
|
+
"""Category 3: Storage Exposure — checks 19-22. 19-21 are Critical
|
|
2
|
+
(public exposure); 22 is Medium (a visibility gap, not exposure itself).
|
|
2
3
|
|
|
3
4
|
Scope note: check_19 checks BUCKET-level PublicAccessBlock only, per the
|
|
4
5
|
blueprint's literal spec (GetBucketPublicAccessBlock). It does NOT check
|
|
@@ -127,8 +128,39 @@ def check_21_public_acl(s3, bucket_names: list) -> list[Finding]:
|
|
|
127
128
|
return findings
|
|
128
129
|
|
|
129
130
|
|
|
131
|
+
def check_22_access_logging_disabled(s3, bucket_names: list) -> list[Finding]:
|
|
132
|
+
"""STOR-22: bucket has no server access logging configured.
|
|
133
|
+
|
|
134
|
+
GetBucketLogging always returns 200, even when logging was never set
|
|
135
|
+
up — the response body simply omits the 'LoggingEnabled' key in that
|
|
136
|
+
case. Unlike check_19's PublicAccessBlock, there's no distinct
|
|
137
|
+
"not configured" exception to catch here, confirmed against the real
|
|
138
|
+
S3 service model before writing this.
|
|
139
|
+
|
|
140
|
+
Medium, not Critical: this is a forensics gap, not an exposure by
|
|
141
|
+
itself — a bucket with logging off isn't more reachable than one with
|
|
142
|
+
it on, it's just unreviewable after the fact if something happens."""
|
|
143
|
+
findings = []
|
|
144
|
+
for name in bucket_names:
|
|
145
|
+
config = s3.get_bucket_logging(Bucket=name)
|
|
146
|
+
if "LoggingEnabled" in config:
|
|
147
|
+
continue
|
|
148
|
+
findings.append(Finding(
|
|
149
|
+
check_id="STOR-22",
|
|
150
|
+
title="S3 Bucket Access Logging Not Enabled",
|
|
151
|
+
severity=Severity.MEDIUM,
|
|
152
|
+
resource_arn=f"arn:aws:s3:::{name}",
|
|
153
|
+
raw_detail=f"Bucket '{name}' has no server access logging configured. "
|
|
154
|
+
f"If this bucket is ever accessed, modified, or exfiltrated "
|
|
155
|
+
f"from, there is no record of who did what — investigation "
|
|
156
|
+
f"after the fact isn't possible.",
|
|
157
|
+
account_context=f"bucket={name}",
|
|
158
|
+
))
|
|
159
|
+
return findings
|
|
160
|
+
|
|
161
|
+
|
|
130
162
|
def run_all(session) -> list[Finding]:
|
|
131
|
-
"""Run STOR-19 through STOR-
|
|
163
|
+
"""Run STOR-19 through STOR-22 against every bucket in the account —
|
|
132
164
|
not just testbed buckets. A real scan must check everything."""
|
|
133
165
|
s3 = session.client("s3")
|
|
134
166
|
bucket_names = list_buckets(s3)
|
|
@@ -136,4 +168,5 @@ def run_all(session) -> list[Finding]:
|
|
|
136
168
|
findings += check_19_missing_public_access_block(s3, bucket_names)
|
|
137
169
|
findings += check_20_public_bucket_policy(s3, bucket_names)
|
|
138
170
|
findings += check_21_public_acl(s3, bucket_names)
|
|
171
|
+
findings += check_22_access_logging_disabled(s3, bucket_names)
|
|
139
172
|
return findings
|
|
@@ -83,7 +83,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
83
83
|
"key. No key set, or a call fails for any reason? That finding falls back to raw "
|
|
84
84
|
"detail automatically, the scan never stops because of it. Off by default — without "
|
|
85
85
|
"it, findings still get free, no-API-key-needed remediation text wherever a template "
|
|
86
|
-
"exists (
|
|
86
|
+
"exists (11 common check types); --explain always uses live AI instead, for every "
|
|
87
87
|
"finding, not just the non-templated ones.")
|
|
88
88
|
scan.add_argument("--explain-limit", type=int, default=25,
|
|
89
89
|
help="Safety cap on how many findings get a live AI call when --explain is passed, in "
|
|
@@ -155,7 +155,7 @@ def _run_scan(args) -> None:
|
|
|
155
155
|
stage("Running checks NET-01 through NET-04")
|
|
156
156
|
findings += network_checks.run_all(session)
|
|
157
157
|
|
|
158
|
-
stage("Running checks STOR-19 through STOR-
|
|
158
|
+
stage("Running checks STOR-19 through STOR-22")
|
|
159
159
|
findings += storage_checks.run_all(session)
|
|
160
160
|
|
|
161
161
|
stage("Running checks ENC-29 through ENC-31")
|
|
@@ -172,7 +172,7 @@ def _parse_sections(raw_text: str) -> tuple[str, str, str]:
|
|
|
172
172
|
|
|
173
173
|
|
|
174
174
|
# ---------------------------------------------------------------------------
|
|
175
|
-
# Templates for the
|
|
175
|
+
# Templates for the 11 most common, narratively-generic check types.
|
|
176
176
|
# Every specific fact (resource names, chain targets) already lives in
|
|
177
177
|
# the Finding's own fields, computed deterministically in Python — the
|
|
178
178
|
# template just substitutes them into fixed prose matching the same
|
|
@@ -373,6 +373,31 @@ def _template_enc29(f: Finding) -> Explanation:
|
|
|
373
373
|
)
|
|
374
374
|
|
|
375
375
|
|
|
376
|
+
def _template_log25(f: Finding) -> Explanation:
|
|
377
|
+
return Explanation(
|
|
378
|
+
impact=(
|
|
379
|
+
"GuardDuty is not enabled in this region, so there is no automated threat detection watching the "
|
|
380
|
+
"account. GuardDuty continuously analyzes CloudTrail, VPC flow logs, and DNS logs for known-bad "
|
|
381
|
+
"signals — an access key suddenly used from a new country or a Tor exit node, an EC2 instance "
|
|
382
|
+
"calling a known crypto-mining or malware domain, someone running account-wide recon with calls "
|
|
383
|
+
"like iam:ListUsers, ec2:DescribeInstances, and s3:ListAllMyBuckets in quick succession. With it "
|
|
384
|
+
"off, all of that happens with nothing raising an alert, so a compromise is usually only noticed "
|
|
385
|
+
"once it causes visible damage or a surprise bill."
|
|
386
|
+
),
|
|
387
|
+
how_to_fix=(
|
|
388
|
+
"Enable GuardDuty in this region, then confirm the detector is active:\n"
|
|
389
|
+
"aws guardduty create-detector --enable\n"
|
|
390
|
+
"aws guardduty list-detectors\n"
|
|
391
|
+
"GuardDuty is per-region, so repeat this in every region you run workloads in, or enable it for "
|
|
392
|
+
"the whole organization from your AWS Organizations management account. It is free for the first "
|
|
393
|
+
"30 days, then billed on the volume of logs analyzed (typically a few dollars a month for a small "
|
|
394
|
+
"account)."
|
|
395
|
+
),
|
|
396
|
+
next_step="Turn GuardDuty on in this region now: aws guardduty create-detector --enable",
|
|
397
|
+
source="template",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
376
401
|
COMMON_CHECK_TEMPLATES = {
|
|
377
402
|
"IAM-01": _template_iam01,
|
|
378
403
|
"IAM-06": _template_iam06,
|
|
@@ -384,18 +409,19 @@ COMMON_CHECK_TEMPLATES = {
|
|
|
384
409
|
"NET-02": _template_net02,
|
|
385
410
|
"STOR-19": _template_stor19,
|
|
386
411
|
"ENC-29": _template_enc29,
|
|
412
|
+
"LOG-25": _template_log25,
|
|
387
413
|
}
|
|
388
414
|
|
|
389
415
|
|
|
390
416
|
def explain_finding(finding: Finding, client=None, use_ai: bool = False) -> Explanation | None:
|
|
391
417
|
"""use_ai=False (default): route to the free template (zero API cost,
|
|
392
|
-
zero network) for the
|
|
418
|
+
zero network) for the 11 most common, narratively-generic check
|
|
393
419
|
types; every other check returns None (raw finding detail only — no
|
|
394
420
|
API call is ever attempted in this mode, regardless of whether a key
|
|
395
421
|
is configured). This is the always-on baseline — no flag needed to
|
|
396
422
|
get free remediation guidance where a template exists.
|
|
397
423
|
|
|
398
|
-
use_ai=True: every finding, including the
|
|
424
|
+
use_ai=True: every finding, including the 11 templated ones, is sent
|
|
399
425
|
to Claude instead — deliberately bypasses the template shortcut so
|
|
400
426
|
"AI narration on" always means fully AI-written content, never a mix
|
|
401
427
|
of template and AI.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plexavo
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Open-source AWS misconfiguration scanner — runs with your own local AWS credentials, nothing sent to anyone else.
|
|
5
5
|
Author: Kavee
|
|
6
6
|
License: AGPL-3.0-or-later
|
|
@@ -53,14 +53,14 @@ can read, and it's entirely optional. See [Cost](#cost).
|
|
|
53
53
|
|
|
54
54
|
## What it checks
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
32 checks across 6 categories, run against real AWS accounts:
|
|
57
57
|
|
|
58
58
|
- **IAM**: privilege escalation paths, wildcard admin, cross-account
|
|
59
59
|
trust, root usage, dormant credentials
|
|
60
60
|
- **Network**: security groups and RDS instances exposed to the
|
|
61
61
|
internet
|
|
62
62
|
- **Storage**: public S3 buckets, via ACLs, bucket policies, or missing
|
|
63
|
-
Block Public Access
|
|
63
|
+
Block Public Access; buckets with no access logging configured
|
|
64
64
|
- **Encryption**: unencrypted EBS volumes, RDS instances, S3 buckets
|
|
65
65
|
- **Logging**: CloudTrail coverage and encryption, GuardDuty status
|
|
66
66
|
- **Usage**: permissions granted but never used, roles nobody has
|
|
@@ -167,6 +167,14 @@ uv pip install -e .
|
|
|
167
167
|
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the pattern used to add a
|
|
168
168
|
new check.
|
|
169
169
|
|
|
170
|
+
## Break Plexavo
|
|
171
|
+
|
|
172
|
+
Think you can make Plexavo miss something, or give confusing guidance?
|
|
173
|
+
[Report it](../../issues/new?template=break-plexavo.yml). Every
|
|
174
|
+
confirmed, genuinely new finding gets fixed and shipped, and you get a
|
|
175
|
+
permanent credit in the [Hall of Bugs](HALL_OF_BUGS.md). No bounty,
|
|
176
|
+
public credit only.
|
|
177
|
+
|
|
170
178
|
## Security
|
|
171
179
|
|
|
172
180
|
Found a vulnerability in the tool itself, not a misconfiguration in your
|
|
@@ -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.7"
|
|
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" }
|
|
@@ -73,7 +73,7 @@ assert_true(_short_name("arn:aws:iam::111111111111:role/my-role") == "my-role",
|
|
|
73
73
|
assert_true(_short_name("arn:aws:s3:::my-bucket") == "my-bucket", "Extracts name after last : when no /")
|
|
74
74
|
assert_true(_short_name("i-0abc123") == "i-0abc123", "Bare ID with no separators returned as-is")
|
|
75
75
|
|
|
76
|
-
print("\n=== All
|
|
76
|
+
print("\n=== All 11 templates produce non-empty impact/next_step, with ZERO API contact ===")
|
|
77
77
|
for check_id in COMMON_CHECK_TEMPLATES:
|
|
78
78
|
finding = f(check_id)
|
|
79
79
|
result = explain_finding(finding, client=ExplodingClient())
|
|
@@ -81,7 +81,19 @@ for check_id in COMMON_CHECK_TEMPLATES:
|
|
|
81
81
|
assert_true(bool(result.impact.strip()), f"{check_id} impact is non-empty")
|
|
82
82
|
assert_true(bool(result.how_to_fix.strip()), f"{check_id} how_to_fix is non-empty")
|
|
83
83
|
assert_true(bool(result.next_step.strip()), f"{check_id} next_step is non-empty — this is new, every template must set it")
|
|
84
|
-
assert_true(len(COMMON_CHECK_TEMPLATES) ==
|
|
84
|
+
assert_true(len(COMMON_CHECK_TEMPLATES) == 11, f"Exactly 11 templated checks exist (got {len(COMMON_CHECK_TEMPLATES)})")
|
|
85
|
+
|
|
86
|
+
print("\n=== LOG-25 (GuardDuty): account-level finding, template does not depend on a resource name ===")
|
|
87
|
+
# LOG-25's real finding has resource_arn="account" — no per-resource
|
|
88
|
+
# identifier to substitute, unlike bucket/volume/user templates. The
|
|
89
|
+
# template must still produce complete, correct guidance.
|
|
90
|
+
finding = f("LOG-25", resource_arn="account", raw_detail="GuardDuty is not enabled in this region.")
|
|
91
|
+
result = explain_finding(finding, client=ExplodingClient())
|
|
92
|
+
assert_true(result.source == "template", f"LOG-25 routes to template (got source={result.source})")
|
|
93
|
+
assert_true("guardduty create-detector" in result.how_to_fix, "LOG-25 fix names the actual enable command")
|
|
94
|
+
assert_true("guardduty create-detector" in result.next_step, "LOG-25 next_step names the actual enable command")
|
|
95
|
+
assert_true("<PLACEHOLDER" not in result.how_to_fix and "<PLACEHOLDER" not in result.next_step,
|
|
96
|
+
"LOG-25 has no per-resource value to fill, so no placeholder should appear")
|
|
85
97
|
|
|
86
98
|
print("\n=== Confidence/evidence are copied from the Finding, not decided by the template ===")
|
|
87
99
|
finding = f("IAM-01", confidence="Likely — see note", evidence="Grant is scoped by a Condition block (aws:SourceIp)")
|
|
@@ -12,11 +12,12 @@ from plexavo.checks import storage
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class FakeS3:
|
|
15
|
-
def __init__(self, buckets, pab=None, policies=None, acls=None):
|
|
15
|
+
def __init__(self, buckets, pab=None, policies=None, acls=None, logging=None):
|
|
16
16
|
self._buckets = buckets
|
|
17
17
|
self._pab = pab or {}
|
|
18
18
|
self._policies = policies or {}
|
|
19
19
|
self._acls = acls or {}
|
|
20
|
+
self._logging = logging or {}
|
|
20
21
|
|
|
21
22
|
def list_buckets(self):
|
|
22
23
|
return {"Buckets": [{"Name": b} for b in self._buckets]}
|
|
@@ -34,6 +35,13 @@ class FakeS3:
|
|
|
34
35
|
def get_bucket_acl(self, Bucket):
|
|
35
36
|
return self._acls.get(Bucket, {"Grants": []})
|
|
36
37
|
|
|
38
|
+
def get_bucket_logging(self, Bucket):
|
|
39
|
+
# Real S3 behavior: always 200, 'LoggingEnabled' simply absent when
|
|
40
|
+
# logging was never configured — never raises.
|
|
41
|
+
if Bucket in self._logging:
|
|
42
|
+
return {"LoggingEnabled": self._logging[Bucket]}
|
|
43
|
+
return {}
|
|
44
|
+
|
|
37
45
|
|
|
38
46
|
FULL_PAB = {"BlockPublicAcls": True, "IgnorePublicAcls": True, "BlockPublicPolicy": True, "RestrictPublicBuckets": True}
|
|
39
47
|
|
|
@@ -53,6 +61,7 @@ def run_checks(s3, buckets):
|
|
|
53
61
|
findings += storage.check_19_missing_public_access_block(s3, buckets)
|
|
54
62
|
findings += storage.check_20_public_bucket_policy(s3, buckets)
|
|
55
63
|
findings += storage.check_21_public_acl(s3, buckets)
|
|
64
|
+
findings += storage.check_22_access_logging_disabled(s3, buckets)
|
|
56
65
|
return findings
|
|
57
66
|
|
|
58
67
|
|
|
@@ -114,5 +123,23 @@ s3 = FakeS3(["owner-only-bucket"], pab={"owner-only-bucket": FULL_PAB}, acls={
|
|
|
114
123
|
findings = run_checks(s3, ["owner-only-bucket"])
|
|
115
124
|
assert_true(not any(f.check_id == "STOR-21" for f in findings), "Does NOT fire on an owner-only ACL grant")
|
|
116
125
|
|
|
126
|
+
print("\n=== STOR-22: bucket has no access logging configured ===")
|
|
127
|
+
s3 = FakeS3(["no-logging-bucket"], pab={"no-logging-bucket": FULL_PAB})
|
|
128
|
+
findings = run_checks(s3, ["no-logging-bucket"])
|
|
129
|
+
assert_true(any(f.check_id == "STOR-22" for f in findings), "Fires when access logging was never configured")
|
|
130
|
+
|
|
131
|
+
print("\n=== FALSE POSITIVE GUARD: STOR-22 does not fire when access logging is enabled ===")
|
|
132
|
+
s3 = FakeS3(["logged-bucket"], pab={"logged-bucket": FULL_PAB},
|
|
133
|
+
logging={"logged-bucket": {"TargetBucket": "log-archive", "TargetPrefix": "logged-bucket/"}})
|
|
134
|
+
findings = run_checks(s3, ["logged-bucket"])
|
|
135
|
+
assert_true(not any(f.check_id == "STOR-22" for f in findings), "Does NOT fire when LoggingEnabled is present")
|
|
136
|
+
|
|
137
|
+
print("\n=== REGRESSION: reported bug (Hall of Bugs #1, found by ThePettyReviewer) — a bucket with logging genuinely off must be flagged ===")
|
|
138
|
+
s3 = FakeS3(["reported-bucket"], pab={"reported-bucket": FULL_PAB})
|
|
139
|
+
findings = run_checks(s3, ["reported-bucket"])
|
|
140
|
+
matched = [f for f in findings if f.check_id == "STOR-22"]
|
|
141
|
+
assert_true(bool(matched), "STOR-22 fires on a bucket with logging off (previously: no check existed at all, so nothing ever fired)")
|
|
142
|
+
assert_true(bool(matched) and matched[0].severity.value == "Medium", "Severity is Medium — a visibility gap, not exposure like STOR-19/20/21")
|
|
143
|
+
|
|
117
144
|
print(f"\n{'ALL PASSED' if failures == 0 else f'{failures} FAILURE(S)'}")
|
|
118
145
|
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
|