Skip to content

Commit

Permalink
update, fix
Browse files Browse the repository at this point in the history
  • Loading branch information
badra001 committed Jan 2, 2026
1 parent 66c99df commit 0b560ec
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from collections import Counter

# --- VERSIONING ---
__version__ = "1.1.0"
__version__ = "1.1.3"

def find_latest_file(pattern):
files = glob.glob(pattern)
Expand All @@ -17,7 +17,6 @@ def find_latest_file(pattern):
def main():
parser = argparse.ArgumentParser(description="AWS CloudTrail Audit Assessor")
parser.add_argument("--input", help="JSON file (default: latest audit_results.check_cloudtrail.*.json)")
parser.add_argument("--central-bucket-regex", default=".*", help="Regex for central bucket")
args = parser.parse_args()

input_file = args.input or find_latest_file("audit_results.check_cloudtrail.*.json")
Expand Down Expand Up @@ -53,12 +52,16 @@ def main():
checks = account.get("data", {})
summary = checks.get("account_summary", {}).get("_summary", "UNKNOWN")

acc_active = 0
acc_stopped = 0
acc_active, acc_stopped = 0, 0
issues = []

for key, val in checks.items():
if not key.startswith("trail:"): continue
if key == "account_summary": continue

# Identify trails by the presence of a colon in the key (Region:TrailName)
if ":" not in key: continue

trail_name = val.get("resource") # Extraction from new field

if val.get("is_logging") == "True":
acc_active += 1; stats["logging_active"] += 1
Expand All @@ -70,26 +73,22 @@ def main():
stats["s3_buckets"].add(bucket)
stats["s3_bytes"] += val.get("bucket_size_bytes", 0)
stats["s3_objects"] += val.get("object_count", 0)
if not re.match(args.central_bucket_regex, bucket):
issues.append(f"Non-Central:{bucket}")

if "cw_logs_size_bytes" in val:
unique_lg_key = f"{acc_id}:{val.get('home_region')}:{val.get('trail_name')}"
unique_lg_key = f"{acc_id}:{val.get('home_region')}:{trail_name}"
stats["cw_group_arns"].add(unique_lg_key)
stats["cw_bytes"] += val["cw_logs_size_bytes"]
retention = val.get("cw_logs_retention_days", "Never Expire")
retention_distribution[retention] += 1

sns = val.get("sns_topic")
if sns and sns != "N/A": stats["sns_topics"].add(sns)
if val.get("kms_key_id") != "SSE-S3": stats["kms_cmk_count"] += 1
else: stats["sse_s3_count"] += 1
if val.get("kms_key_id") == "SSE-S3": stats["sse_s3_count"] += 1
else: stats["kms_cmk_count"] += 1

print(f"{acc_id:<15} | {ou_path[:30]:<30} | {summary:<25} | {f'{acc_active} ON / {acc_stopped} OFF':<15} | {', '.join(issues) if issues else 'COMPLIANT'}")
print(f"{acc_id:<15} | {ou_path[:30]:<30} | {summary:<25} | {f'{acc_active} ON / {acc_stopped} OFF':<15} | COMPLIANT")

# RESTORED SUMMARY SECTION
s3_gb = stats["s3_bytes"] / (1024**3)
cw_gb = stats["cw_bytes"] / (1024**3)
s3_gb, cw_gb = stats["s3_bytes"] / (1024**3), stats["cw_bytes"] / (1024**3)

print("-" * 180)
print(f"ORGANIZATION CLOUDTRAIL FOOTPRINT SUMMARY | Org ID: {org_id}")
Expand All @@ -101,8 +100,6 @@ def main():
for period in sorted_ret:
label = f"{period} days" if isinstance(period, int) else str(period)
print(f" - {label:<15}: {retention_distribution[period]} group(s)")
print(f" Encryption: {stats['kms_cmk_count']} KMS CMK | {stats['sse_s3_count']} SSE-S3")
print(f" Notifications: {len(stats['sns_topics'])} unique SNS Topics")
print("-" * 180)

if __name__ == "__main__":
Expand Down
26 changes: 11 additions & 15 deletions local-app/python-tools/cross-organization/assess_check_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
import glob

# --- VERSIONING ---
__version__ = "1.0.8"
__version__ = "1.0.10"

def find_latest_file(pattern):
"""Searches for the most recent file matching the pattern."""
files = glob.glob(pattern)
return max(files, key=os.path.getctime) if files else None

Expand Down Expand Up @@ -38,12 +39,11 @@ def main():
print(f"{'Account ID':<15} | {'OU Path':<30} | {'Global Status':<12} | {'S3 Compliance'}")
print("-" * 140)

# UPDATED STATS: Tracking CONFIG COUNTS separately from UNIQUE BUCKETS
stats = {
"objects": 0, "size_bytes": 0,
"total_recorders": 0,
"config_count_central": 0, # Number of regional configs using central buckets
"config_count_non_central": 0, # Number of regional configs using non-central buckets
"config_count_central": 0,
"config_count_non_central": 0,
"unique_central_buckets": set(),
"unique_non_central_buckets": set(),
"accounts": len(data)
Expand All @@ -59,6 +59,9 @@ def main():
for reg, reg_data in checks.items():
if reg == "account_summary": continue

# Logic update: Verify resource type
if reg_data.get("resource") != "config": continue

if reg_data.get("recorder_status") == "ON":
stats["total_recorders"] += 1

Expand All @@ -67,31 +70,24 @@ def main():
stats["size_bytes"] += reg_data.get("bucket_size_bytes", 0)

if bucket != "N/A":
# FIXED: Using search for pattern matching anywhere in the name
if re.search(args.central_bucket_regex, bucket, re.IGNORECASE):
stats["config_count_central"] += 1 # Increment config instance count
stats["unique_central_buckets"].add(bucket) # Track unique bucket
stats["config_count_central"] += 1
stats["unique_central_buckets"].add(bucket)
else:
stats["config_count_non_central"] += 1 # Increment config instance count
stats["unique_non_central_buckets"].add(bucket) # Track unique bucket
stats["config_count_non_central"] += 1
stats["unique_non_central_buckets"].add(bucket)
s3_issues.append(bucket)

s3_status = "NON_COMPLIANT" if s3_issues else "COMPLIANT"
print(f"{acc_id:<15} | {ou_path[:30]:<30} | {summary:<12} | {s3_status}")

# SUMMARY SECTION: Displaying both instance counts and unique bucket counts
size_gb = stats["size_bytes"] / (1024**3)
print("-" * 140)
print(f"ORGANIZATION FOOTPRINT SUMMARY (CONFIG) | Org ID: {org_id}")
print(f" Active Recorders Found: {stats['total_recorders']}")
print(f" Total S3 Objects: {stats['objects']:,}")
print(f" Total S3 Storage: {size_gb:.2f} GB")
print(f" --- Configuration Instance Counts ---")
print(f" Configs using Central: {stats['config_count_central']}")
print(f" Configs using Non-Central:{stats['config_count_non_central']}")
print(f" --- Unique Resource Counts ---")
print(f" Unique Central Buckets: {len(stats['unique_central_buckets'])}")
print(f" Unique Non-Central: {len(stats['unique_non_central_buckets'])}")
print("-" * 140)

if __name__ == "__main__":
Expand Down
11 changes: 6 additions & 5 deletions local-app/python-tools/cross-organization/check_cloudtrail.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from datetime import datetime, timedelta

# --- VERSIONING ---
__version__ = "1.1.2"
__version__ = "1.1.3"

def get_s3_metrics(session, bucket_name, region):
cw = session.client('cloudwatch', region_name=region)
Expand Down Expand Up @@ -39,8 +39,7 @@ def get_log_group_details(session, group_arn, region):

def account_task(account_session, account_id, account_name, region):
results = {"alias": "N/A", "data": {}}
org_trail_count = 0
local_trail_count = 0
org_trail_count, local_trail_count = 0, 0

try:
results["alias"] = account_session.client('iam').list_account_aliases().get('AccountAliases', ["N/A"])[0]
Expand All @@ -52,6 +51,7 @@ def account_task(account_session, account_id, account_name, region):
for reg in enabled_regions:
reg_start = time.perf_counter()
ct = account_session.client('cloudtrail', region_name=reg)
# includeShadowTrails ensures we see trails from other home regions
trails = ct.describe_trails(includeShadowTrails=True).get('trailList', [])

for trail in trails:
Expand All @@ -66,7 +66,7 @@ def account_task(account_session, account_id, account_name, region):

t_name = trail['Name']
t_data = {
"trail_name": t_name,
"resource": t_name, # Separated Resource Name
"home_region": trail.get('HomeRegion', reg),
"is_logging": str(status.get('IsLogging', False)),
"is_org_trail": str(is_org),
Expand All @@ -84,7 +84,8 @@ def account_task(account_session, account_id, account_name, region):
if t_data['s3_bucket'] != 'N/A':
t_data.update(get_s3_metrics(account_session, t_data['s3_bucket'], reg))

results["data"][f"trail:{t_name}:{t_data['home_region']}"] = t_data
# New Key Format: Region:Resource
results["data"][f"{reg}:{t_name}"] = t_data

summary = "OK"
if org_trail_count > 0 and local_trail_count > 0:
Expand Down
9 changes: 7 additions & 2 deletions local-app/python-tools/cross-organization/check_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from datetime import datetime, timedelta

# --- VERSIONING ---
__version__ = "1.0.9"
__version__ = "1.1.0"

def get_s3_metrics(session, bucket_name, region):
cw = session.client('cloudwatch', region_name=region)
Expand Down Expand Up @@ -34,13 +34,18 @@ def account_task(account_session, account_id, account_name, region):
is_global = any(r.get('recordingGroup', {}).get('includeGlobalResourceTypes') for r in recorders)
if is_global: global_count += 1

reg_data = {"recorder_status": "ON" if recorders else "OFF", "global_recording": str(is_global)}
reg_data = {
"resource": "config", # New Resource Field
"recorder_status": "ON" if recorders else "OFF",
"global_recording": str(is_global)
}
if channels:
bucket = channels[0].get('s3BucketName', 'N/A')
reg_data.update({"s3_bucket": bucket, "delivery_freq": channels[0].get('configSnapshotDeliveryProperties', {}).get('deliveryFrequency', 'N/A')})
if bucket != "N/A": reg_data.update(get_s3_metrics(account_session, bucket, reg))

reg_data["check_elapsed_sec"] = round(time.perf_counter() - reg_start, 3)
# Region field is now strictly the region name
results["data"][reg] = reg_data

summary_val = f"OK/1" if global_count == 1 else f"MULTIPLE/{global_count}" if global_count > 1 else "NONE/0"
Expand Down
54 changes: 27 additions & 27 deletions local-app/python-tools/cross-organization/org_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
def tqdm(iterable, **kwargs): return iterable

# --- VERSIONING ---
__version__ = "1.6.5"
__version__ = "1.6.7"

class OrgTaskRunner:
def __init__(self, args):
Expand Down Expand Up @@ -89,7 +89,6 @@ def run(self):
sts_client = session.client('sts')
iam_client = session.client('iam')

# Resolve Header Info
caller = sts_client.get_caller_identity()
partition = caller['Arn'].split(':')[1]

Expand All @@ -100,9 +99,8 @@ def run(self):
try:
master_aliases = iam_client.list_account_aliases()['AccountAliases']
master_alias = master_aliases[0] if master_aliases else "None"
except: master_alias = "Unknown (Check Permissions)"
except: master_alias = "Unknown"

# Load tasks & build dynamic metadata
tasks, check_info = [], []
if self.args.enable_checks:
sys.path.append(os.getcwd())
Expand All @@ -117,20 +115,10 @@ def run(self):
for acc in page['Accounts'] if acc['Status'] == 'ACTIVE']
all_accounts.sort(key=lambda x: x['Name' if self.args.sort == 'name' else 'Id'].lower())

# UPDATED HEADER
print("-" * 100)
print(f"AWS ORG TASK RUNNER - v{__version__}")
print(f" Profile: {self.args.profile or 'default'}")
print(f" Region: {self.args.region}")
print(f" Caller Identity: {caller['Arn']}")
print(f" Organization ID: {self.org_id}")
print(f" Management ID: {master_id}")
print(f" Management Alias: {master_alias}")
print("-" * 100)
print(f" Target Role: {self.args.role_name}")
print(f" Max Workers: {self.args.max_workers}")
print(f" Enabled Checks: {', '.join(check_info) if check_info else 'None'}")
print(f" Accounts Found: {len(all_accounts)}")
print(f" Management ID: {master_id} ({master_alias})")
print("-" * 100)

with ThreadPoolExecutor(max_workers=self.args.max_workers) as executor:
Expand All @@ -144,27 +132,42 @@ def run(self):
if self.args.output:
ds = datetime.now().strftime("%Y%m%dT%H%M%S")

# 1. ACCOUNT BASELINE (RESTORED)
# ACCOUNT BASELINE
acc_base = f"audit_results.account.{ds}"
with open(f"{acc_base}.json", 'w') as f:
json.dump([r['metadata'] for r in self.full_results], f, indent=2)
with open(f"{acc_base}.csv", 'w', newline='') as f:
w = csv.DictWriter(f, fieldnames=["org_id", "account_id", "account_name", "alias", "ou_path", "ou_id"])
w.writeheader()
w.writerows([r['metadata'] for r in self.full_results])
self.created_files.extend([f"{acc_base}.json", f"{acc_base}.csv"])

# 2. CHECK SPECIFIC FILES
# CHECK SPECIFIC FILES
for mod_name, _ in tasks:
chk_base = f"audit_results.{mod_name}.{ds}"
with open(f"{chk_base}.csv", 'w', newline='') as f:
w = csv.writer(f)
w.writerow(["org_id", "account_id", "account_alias", "region", "field_name", "field_value"])
w.writerow(["org_id", "account_id", "account_alias", "region", "resource", "field_name", "field_value"])
for res in self.full_results:
mod_data = res["task_data"].get(mod_name, {})
for reg, fields in mod_data.items():
for key, fields in mod_data.items():
if key == "account_summary": continue

# Parse Region and Resource
if ":" in key:
region_part, resource_part = key.split(":", 1)
else:
region_part = key
resource_part = fields.get("resource", "config")

for k, v in fields.items():
w.writerow([self.org_id, res["metadata"]["account_id"], res["metadata"]["alias"], reg, k, v])
if k == "resource": continue
w.writerow([
self.org_id,
res["metadata"]["account_id"],
res["metadata"]["alias"],
region_part,
resource_part,
k,
v
])

with open(f"{chk_base}.json", 'w') as f:
json.dump([{
Expand All @@ -175,11 +178,8 @@ def run(self):
"ou_id": r["metadata"]["ou_id"],
"data": r["task_data"].get(mod_name, {})
} for r in self.full_results], f, indent=2)

self.created_files.extend([f"{chk_base}.csv", f"{chk_base}.json"])

print(f"\nTime: {round(time.perf_counter() - self.start_time, 2)}s\nFiles Created:")
for f in self.created_files: print(f" - {f}")
print(f"\nTime: {round(time.perf_counter() - self.start_time, 2)}s")

if __name__ == "__main__":
p = argparse.ArgumentParser()
Expand Down

0 comments on commit 0b560ec

Please sign in to comment.