mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-13 06:34:57 +03:00
c21af3347e
- Add scripts/agent.py and references/api-reference.md to all remaining skills - Update all 648 LICENSE files: copyright now reads 'Mahipal' - Add implementing-security-monitoring-with-datadog (new skill with full anatomy) - All 649 skills now have: SKILL.md, LICENSE, scripts/agent.py, references/api-reference.md
137 lines
5.4 KiB
Python
137 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Privileged Account Discovery agent — enumerates privileged accounts across
|
|
Active Directory using ldap3 and flags shadow admin paths."""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
try:
|
|
from ldap3 import Server, Connection, ALL, SUBTREE
|
|
except ImportError:
|
|
print("Install ldap3: pip install ldap3", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
PRIVILEGED_GROUPS = [
|
|
"Domain Admins", "Enterprise Admins", "Schema Admins",
|
|
"Administrators", "Account Operators", "Backup Operators",
|
|
"Server Operators", "Print Operators", "DnsAdmins",
|
|
]
|
|
|
|
|
|
def connect_ldap(server_url: str, username: str, password: str, use_ssl: bool = True) -> Connection:
|
|
"""Establish LDAP connection."""
|
|
srv = Server(server_url, get_info=ALL, use_ssl=use_ssl)
|
|
conn = Connection(srv, user=username, password=password, auto_bind=True)
|
|
return conn
|
|
|
|
|
|
def get_domain_dn(conn: Connection) -> str:
|
|
"""Extract domain distinguished name from RootDSE."""
|
|
return conn.server.info.other.get("defaultNamingContext", [""])[0]
|
|
|
|
|
|
def enumerate_privileged_groups(conn: Connection, base_dn: str) -> list[dict]:
|
|
"""Find all privileged group memberships recursively."""
|
|
results = []
|
|
for group_name in PRIVILEGED_GROUPS:
|
|
search_filter = f"(&(objectClass=group)(cn={group_name}))"
|
|
conn.search(base_dn, search_filter, search_scope=SUBTREE,
|
|
attributes=["cn", "member", "distinguishedName"])
|
|
for entry in conn.entries:
|
|
members = entry.member.values if hasattr(entry.member, "values") else []
|
|
results.append({
|
|
"group": str(entry.cn),
|
|
"dn": str(entry.distinguishedName),
|
|
"member_count": len(members),
|
|
"members": [str(m) for m in members],
|
|
})
|
|
return results
|
|
|
|
|
|
def find_nested_memberships(conn: Connection, base_dn: str, group_dn: str) -> list[str]:
|
|
"""Resolve nested group memberships using LDAP_MATCHING_RULE_IN_CHAIN."""
|
|
search_filter = f"(memberOf:1.2.840.113556.1.4.1941:={group_dn})"
|
|
conn.search(base_dn, search_filter, search_scope=SUBTREE,
|
|
attributes=["sAMAccountName", "objectClass"])
|
|
return [str(e.sAMAccountName) for e in conn.entries]
|
|
|
|
|
|
def find_service_accounts(conn: Connection, base_dn: str) -> list[dict]:
|
|
"""Discover service accounts via servicePrincipalName."""
|
|
search_filter = "(&(objectClass=user)(servicePrincipalName=*))"
|
|
conn.search(base_dn, search_filter, search_scope=SUBTREE,
|
|
attributes=["sAMAccountName", "servicePrincipalName",
|
|
"adminCount", "whenChanged", "userAccountControl"])
|
|
accounts = []
|
|
for entry in conn.entries:
|
|
uac = int(str(entry.userAccountControl)) if hasattr(entry, "userAccountControl") else 0
|
|
accounts.append({
|
|
"username": str(entry.sAMAccountName),
|
|
"spns": [str(s) for s in entry.servicePrincipalName.values],
|
|
"admin_count": str(entry.adminCount) if hasattr(entry, "adminCount") else "0",
|
|
"password_never_expires": bool(uac & 0x10000),
|
|
"last_changed": str(entry.whenChanged),
|
|
})
|
|
return accounts
|
|
|
|
|
|
def find_admin_count_users(conn: Connection, base_dn: str) -> list[str]:
|
|
"""Find users with adminCount=1 (shadow admins or orphaned flags)."""
|
|
search_filter = "(&(objectClass=user)(adminCount=1))"
|
|
conn.search(base_dn, search_filter, search_scope=SUBTREE,
|
|
attributes=["sAMAccountName"])
|
|
return [str(e.sAMAccountName) for e in conn.entries]
|
|
|
|
|
|
def generate_report(server_url: str, username: str, password: str, use_ssl: bool) -> dict:
|
|
"""Run full privileged account discovery and build JSON report."""
|
|
conn = connect_ldap(server_url, username, password, use_ssl)
|
|
base_dn = get_domain_dn(conn)
|
|
|
|
priv_groups = enumerate_privileged_groups(conn, base_dn)
|
|
svc_accounts = find_service_accounts(conn, base_dn)
|
|
admin_count_users = find_admin_count_users(conn, base_dn)
|
|
total_priv = sum(g["member_count"] for g in priv_groups)
|
|
|
|
conn.unbind()
|
|
return {
|
|
"report": "privileged_account_discovery",
|
|
"generated_at": datetime.utcnow().isoformat() + "Z",
|
|
"domain": base_dn,
|
|
"privileged_groups": priv_groups,
|
|
"total_privileged_members": total_priv,
|
|
"service_accounts": svc_accounts,
|
|
"admin_count_users": admin_count_users,
|
|
"summary": {
|
|
"privileged_groups_found": len(priv_groups),
|
|
"service_accounts": len(svc_accounts),
|
|
"admin_count_flagged_users": len(admin_count_users),
|
|
},
|
|
}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Privileged Account Discovery Agent")
|
|
parser.add_argument("--server", required=True, help="LDAP server URL (ldaps://dc.example.com)")
|
|
parser.add_argument("--username", required=True, help="Bind DN or UPN")
|
|
parser.add_argument("--password", required=True, help="Bind password")
|
|
parser.add_argument("--no-ssl", action="store_true", help="Disable SSL")
|
|
parser.add_argument("--output", help="Output JSON file path")
|
|
args = parser.parse_args()
|
|
|
|
report = generate_report(args.server, args.username, args.password, not args.no_ssl)
|
|
output = json.dumps(report, indent=2)
|
|
if args.output:
|
|
Path(args.output).write_text(output, encoding="utf-8")
|
|
print(f"Report written to {args.output}")
|
|
else:
|
|
print(output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|