Sortable HTML tables, local-time toggle, and RFC 3339 dates everywhere
- All timestamps (last logon, password last set, whenCreated, report generation time) now emit as RFC 3339 UTC in both the Markdown report and raw JSON dump, for both the Python and PowerShell scripts. - md_to_html.py: click any table header to sort ascending/descending (vanilla JS, numeric-aware); a "Show local time" toggle swaps every timestamp between UTC and the viewer's local offset, still RFC 3339.
This commit is contained in:
+23
-8
@@ -43,6 +43,21 @@ def filetime_to_datetime(value):
|
||||
return None
|
||||
|
||||
|
||||
def rfc3339(dt):
|
||||
"""Format a datetime as RFC 3339 (UTC, second precision, 'Z' suffix).
|
||||
|
||||
Falls back to str(dt) for values ldap3 didn't parse into a datetime
|
||||
(e.g. if a server's schema doesn't trigger its generalizedTime formatter).
|
||||
"""
|
||||
if dt is None:
|
||||
return ""
|
||||
if not isinstance(dt, datetime.datetime):
|
||||
return str(dt)
|
||||
if dt.tzinfo is not None:
|
||||
dt = dt.astimezone(datetime.timezone.utc)
|
||||
return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def parse_args():
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument("--host", required=True, help="DC hostname or IP")
|
||||
@@ -114,7 +129,7 @@ def audit_ous(conn, base_dn):
|
||||
"dn": str(e.distinguishedName),
|
||||
"name": str(e.ou) if e.ou else "",
|
||||
"description": str(e.description) if e.description else "",
|
||||
"created": str(e.whenCreated) if e.whenCreated else "",
|
||||
"created": rfc3339(e.whenCreated.value) if e.whenCreated else "",
|
||||
"depth": str(e.distinguishedName).count("OU="),
|
||||
}
|
||||
)
|
||||
@@ -163,11 +178,11 @@ def audit_users(conn, base_dn, stale_days):
|
||||
"trusted_for_delegation": bool(uac & UAC_TRUSTED_FOR_DELEGATION),
|
||||
"kerberos_preauth_disabled": bool(uac & UAC_DONT_REQ_PREAUTH),
|
||||
"admin_count": bool(e.adminCount and int(str(e.adminCount)) > 0),
|
||||
"last_logon": last_logon.isoformat() if last_logon else None,
|
||||
"last_logon": rfc3339(last_logon) if last_logon else None,
|
||||
"never_logged_on": never_logged_on,
|
||||
"stale": stale,
|
||||
"pwd_last_set": pwd_last_set.isoformat() if pwd_last_set else None,
|
||||
"created": str(e.whenCreated) if e.whenCreated else "",
|
||||
"pwd_last_set": rfc3339(pwd_last_set) if pwd_last_set else None,
|
||||
"created": rfc3339(e.whenCreated.value) if e.whenCreated else "",
|
||||
"group_count": len(e.memberOf.values) if e.memberOf else 0,
|
||||
}
|
||||
)
|
||||
@@ -203,9 +218,9 @@ def audit_computers(conn, base_dn, stale_days):
|
||||
"os": str(e.operatingSystem) if e.operatingSystem else "",
|
||||
"os_version": str(e.operatingSystemVersion) if e.operatingSystemVersion else "",
|
||||
"disabled": disabled,
|
||||
"last_logon": last_logon.isoformat() if last_logon else None,
|
||||
"last_logon": rfc3339(last_logon) if last_logon else None,
|
||||
"stale": stale,
|
||||
"created": str(e.whenCreated) if e.whenCreated else "",
|
||||
"created": rfc3339(e.whenCreated.value) if e.whenCreated else "",
|
||||
}
|
||||
)
|
||||
return computers
|
||||
@@ -239,7 +254,7 @@ def audit_groups(conn, base_dn):
|
||||
"member_count": len(members),
|
||||
"empty": len(members) == 0,
|
||||
"description": str(e.description) if e.description else "",
|
||||
"created": str(e.whenCreated) if e.whenCreated else "",
|
||||
"created": rfc3339(e.whenCreated.value) if e.whenCreated else "",
|
||||
}
|
||||
)
|
||||
return groups
|
||||
@@ -428,7 +443,7 @@ def build_report(data, args):
|
||||
a(f"")
|
||||
a(f"- Base DN: `{args.base_dn}`")
|
||||
a(f"- Domain Controller: `{args.host}`")
|
||||
a(f"- Generated: {datetime.datetime.utcnow().isoformat()}Z")
|
||||
a(f"- Generated: {rfc3339(datetime.datetime.utcnow())}")
|
||||
a(f"- Stale-account threshold: {args.stale_days} days of inactivity")
|
||||
a(f"")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user