identity access management

Performing Service Account Audit

Audit service accounts across enterprise infrastructure to identify orphaned, over-privileged, and non-compliant accounts. This skill covers discovery of service accounts in Active Directory, cloud platforms, databases, and applications, assessing privilege levels, identifying missing owners, and enforcing lifecycle policies.

access-controlauditgovernanceiamidentityservice-accounts
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

Audit service accounts across enterprise infrastructure to identify orphaned, over-privileged, and non-compliant accounts. This skill covers discovery of service accounts in Active Directory, cloud platforms, databases, and applications, assessing privilege levels, identifying missing owners, and enforcing lifecycle policies.

When to Use

  • When conducting security assessments that involve performing service account audit
  • When following incident response procedures for related security events
  • When performing scheduled security testing or auditing activities
  • When validating security controls through hands-on testing

Prerequisites

  • Familiarity with identity access management concepts and tools
  • Access to a test or lab environment for safe execution
  • Python 3.8+ with required dependencies installed
  • Appropriate authorization for any testing activities

Objectives

  • Discover all service accounts across AD, cloud, databases, and applications
  • Identify orphaned accounts with no valid owner or associated application
  • Assess privilege levels and flag over-privileged service accounts
  • Check for non-rotating passwords and weak authentication
  • Map service account dependencies for safe remediation
  • Generate compliance reports for SOX, PCI DSS, and HIPAA audits

Key Concepts

Service Account Types

  1. AD Service Accounts: Windows services, scheduled tasks, IIS app pools
  2. Managed Service Accounts (gMSA): AD-managed automatic password rotation
  3. Cloud IAM Service Accounts: AWS IAM roles/users, Azure service principals, GCP service accounts
  4. Database Service Accounts: Application connection accounts, replication accounts
  5. Application Service Accounts: API keys, bot accounts, integration accounts

Audit Dimensions

  • Ownership: Who is responsible for this account?
  • Purpose: What application/service uses this account?
  • Privileges: What permissions does this account have?
  • Authentication: How does this account authenticate (password, key, certificate)?
  • Rotation: When was the credential last changed?
  • Activity: When was this account last used?

Workflow

Step 1: Discovery - Active Directory

  1. Query AD for all service accounts (filter by description, OU, naming convention)
  2. Identify accounts with ServicePrincipalName set
  3. List accounts in privileged groups (Domain Admins, Enterprise Admins)
  4. Check for gMSA vs traditional service accounts
  5. Identify accounts with PasswordNeverExpires flag

Step 2: Discovery - Cloud Platforms

  • AWS: List IAM users with access keys, check last used date, identify unused roles
  • Azure: Enumerate service principals, app registrations, managed identities
  • GCP: List service accounts, check key age, identify unused permissions

Step 3: Assessment

  • Flag accounts with admin/privileged group membership
  • Check password age against rotation policy (90 days max)
  • Identify accounts with no login activity in 90+ days
  • Verify account ownership against CMDB/asset inventory
  • Check for shared credentials (same password hash across accounts)

Step 4: Risk Classification

  • Critical: Domain/cloud admin privileges, no password rotation
  • High: Access to sensitive data, no identified owner
  • Medium: Standard service permissions, password older than 90 days
  • Low: Read-only access, managed credentials (gMSA, managed identity)

Step 5: Remediation

  • Disable orphaned accounts after validation with application teams
  • Convert traditional service accounts to gMSA where possible
  • Rotate credentials older than policy threshold
  • Reduce privileges to minimum required
  • Assign owners and document dependencies

Security Controls

Control NIST 800-53 Description
Account Management AC-2 Service account lifecycle
Account Review AC-2(3) Periodic review of accounts
Least Privilege AC-6 Minimum service account permissions
Authenticator Management IA-5 Service credential rotation
Audit Review AU-6 Review service account activity

Common Pitfalls

  • Disabling service accounts without verifying application dependencies first
  • Not discovering service accounts outside of Active Directory
  • Missing cloud service principals and managed identities
  • Not checking for interactive logon rights on service accounts
  • Failing to document dependencies before remediation

Verification

  • Service accounts inventoried across all platforms
  • Each account has assigned owner
  • Privileged service accounts documented with justification
  • Password rotation compliance checked
  • Orphaned accounts flagged for remediation
  • gMSA migration candidates identified
  • Compliance report generated
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 3

api-reference.md2.2 KB

API Reference: Service Account Audit

Active Directory PowerShell Cmdlets

Cmdlet Description
Get-ADUser -Filter {ServicePrincipalName -ne '$null'} Find accounts with SPNs
Get-ADServiceAccount -Filter * List managed service accounts
Get-ADGroupMember -Identity "Domain Admins" List privileged group members
Search-ADAccount -PasswordNeverExpires Find non-expiring passwords
Search-ADAccount -AccountInactive -TimeSpan 90.00:00:00 Find inactive accounts

AWS IAM CLI Commands

Command Description
aws iam list-users List all IAM users
aws iam list-access-keys --user-name <name> List access keys for user
aws iam get-access-key-last-used --access-key-id <id> Check key last used date
aws iam list-user-policies --user-name <name> List inline policies
aws iam list-attached-user-policies --user-name <name> List managed policies
aws iam generate-credential-report Generate credential report

Azure CLI Commands

Command Description
az ad sp list --all List all service principals
az ad app list --all List all app registrations
az ad app credential list --id <app-id> List credential expiration

Risk Classification

Level Score Range Criteria
Critical >= 40 Domain admin + stale password + no owner
High 25-39 Privileged group membership or orphaned
Medium 10-24 Password age exceeded or PasswordNeverExpires
Low 0-9 Standard permissions, managed credentials

Python Libraries

Library Version Purpose
subprocess stdlib Execute PowerShell and AWS CLI commands
json stdlib Parse CLI output
ldap3 >=2.9 Direct LDAP queries to Active Directory
boto3 >=1.26 AWS IAM programmatic access

References

standards.md0.9 KB

Standards - Service Account Audit

NIST Standards

  • NIST SP 800-53 Rev 5: AC-2, AC-2(3), AC-6, IA-5, AU-6
  • NIST SP 800-171: 3.1.1, 3.1.2, 3.5.1, 3.5.2

Industry Frameworks

  • CIS Controls v8: Control 5.3 - Disable Dormant Accounts, Control 5.4 - Restrict Administrator Privileges
  • MITRE ATT&CK: T1078 (Valid Accounts), T1136 (Create Account)
  • PCI DSS 4.0: 7.2.5 - Review user access, 8.6 - Application/system account management
  • SOX Section 404: Service account access controls for financial systems

Tools

  • Microsoft AD: Get-ADServiceAccount, Get-ADUser with SPN filter
  • AWS IAM: Access Analyzer, Credential Report, IAM Access Advisor
  • Azure Entra ID: Service principal reports, App registration audit
  • CyberArk DNA: Automated privileged account discovery
  • Stealthbits (Netwrix): Service account discovery and monitoring
workflows.md1.0 KB

Service Account Audit Workflows

Workflow 1: Discovery Phase

  1. Export AD service accounts using PowerShell/LDAP queries
  2. Export cloud IAM service accounts (AWS credential report, Azure SP list, GCP SA list)
  3. Query databases for application-specific service accounts
  4. Consolidate into single inventory spreadsheet
  5. Cross-reference with CMDB for ownership data

Workflow 2: Assessment Phase

  1. Check each account against privilege policy
  2. Verify password/key rotation compliance (90-day max)
  3. Check last logon/activity date
  4. Validate owner assignment against HR data
  5. Flag accounts meeting orphaned/stale/over-privileged criteria

Workflow 3: Remediation Phase

  1. Contact owners of over-privileged accounts for justification
  2. Plan gMSA migration for eligible Windows service accounts
  3. Disable orphaned accounts (staged: disable first, delete after 30 days)
  4. Rotate stale credentials immediately
  5. Update documentation, close findings, report to compliance

Scripts 2

agent.py6.7 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Agent for auditing service accounts across AD, cloud, and databases.

Discovers service accounts via LDAP queries, AWS IAM, and Azure AD,
checks password age, privilege levels, and orphan status, then
generates a risk-classified compliance report.
"""

import json
import sys
import subprocess
from datetime import datetime
from collections import defaultdict


class ServiceAccountAuditor:
    """Audits service accounts across enterprise infrastructure."""

    RISK_WEIGHTS = {"Domain Admins": 30, "Enterprise Admins": 30,
                    "Schema Admins": 25, "Administrators": 20,
                    "Account Operators": 15, "Backup Operators": 10}

    def __init__(self, domain=None, max_password_age_days=90):
        self.domain = domain
        self.max_password_age_days = max_password_age_days
        self.accounts = []

    def discover_ad_service_accounts(self):
        """Discover service accounts in Active Directory via PowerShell."""
        ps_cmd = (
            "Get-ADUser -Filter {ServicePrincipalName -ne '$null'} "
            "-Properties ServicePrincipalName,PasswordLastSet,LastLogonDate,"
            "Enabled,MemberOf,Description,PasswordNeverExpires "
            "| Select-Object Name,SamAccountName,Enabled,PasswordLastSet,"
            "LastLogonDate,PasswordNeverExpires,"
            "@{N='SPNs';E={$_.ServicePrincipalName -join ';'}},"
            "@{N='Groups';E={($_.MemberOf | ForEach-Object "
            "{($_ -split ',')[0] -replace 'CN=',''}) -join ';'}},"
            "Description | ConvertTo-Json -Depth 3"
        )
        try:
            result = subprocess.run(
                ["powershell", "-NoProfile", "-Command", ps_cmd],
                capture_output=True, text=True, timeout=120
            )
            if result.returncode == 0 and result.stdout.strip():
                data = json.loads(result.stdout)
                if isinstance(data, dict):
                    data = [data]
                for acct in data:
                    acct["source"] = "ActiveDirectory"
                self.accounts.extend(data)
                return data
        except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError) as exc:
            return {"error": str(exc)}
        return []

    def discover_aws_iam_users(self):
        """Discover AWS IAM service users via CLI."""
        try:
            result = subprocess.run(
                ["aws", "iam", "list-users", "--output", "json"],
                capture_output=True, text=True, timeout=60
            )
            if result.returncode == 0:
                users = json.loads(result.stdout).get("Users", [])
                svc_users = []
                for u in users:
                    name = u.get("UserName", "")
                    if any(p in name.lower() for p in ["svc", "service", "bot", "automation"]):
                        keys_result = subprocess.run(
                            ["aws", "iam", "list-access-keys",
                             "--user-name", name, "--output", "json"],
                            capture_output=True, text=True, timeout=30
                        )
                        keys = []
                        if keys_result.returncode == 0:
                            keys = json.loads(keys_result.stdout).get("AccessKeyMetadata", [])
                        svc_users.append({
                            "Name": name, "source": "AWS_IAM",
                            "CreateDate": u.get("CreateDate", ""),
                            "PasswordLastUsed": u.get("PasswordLastUsed", ""),
                            "AccessKeys": len(keys),
                            "OldestKeyDate": min(
                                (k.get("CreateDate", "") for k in keys), default=""
                            ),
                        })
                self.accounts.extend(svc_users)
                return svc_users
        except (FileNotFoundError, json.JSONDecodeError):
            pass
        return []

    def assess_risk(self, account):
        """Classify account risk based on privilege, age, and activity."""
        score = 0
        issues = []

        groups = account.get("Groups", "").split(";") if account.get("Groups") else []
        for grp in groups:
            if grp in self.RISK_WEIGHTS:
                score += self.RISK_WEIGHTS[grp]
                issues.append(f"Member of {grp}")

        if account.get("PasswordNeverExpires"):
            score += 15
            issues.append("PasswordNeverExpires set")

        pwd_set = account.get("PasswordLastSet")
        if pwd_set:
            try:
                pwd_date = datetime.fromisoformat(pwd_set.replace("/Date(", "").rstrip(")/"))
            except (ValueError, AttributeError):
                pwd_date = None
            if pwd_date and (datetime.utcnow() - pwd_date).days > self.max_password_age_days:
                age_days = (datetime.utcnow() - pwd_date).days
                score += 10
                issues.append(f"Password age {age_days} days (>{self.max_password_age_days})")

        last_logon = account.get("LastLogonDate")
        if not last_logon:
            score += 10
            issues.append("No recorded logon (possible orphan)")

        if score >= 40:
            level = "Critical"
        elif score >= 25:
            level = "High"
        elif score >= 10:
            level = "Medium"
        else:
            level = "Low"

        return {"risk_level": level, "risk_score": score, "issues": issues}

    def generate_report(self):
        """Generate a compliance report for all discovered accounts."""
        report = {
            "audit_date": datetime.utcnow().isoformat(),
            "domain": self.domain,
            "total_accounts": len(self.accounts),
            "by_source": defaultdict(int),
            "by_risk": defaultdict(int),
            "accounts": [],
        }

        for acct in self.accounts:
            assessment = self.assess_risk(acct)
            report["by_source"][acct.get("source", "unknown")] += 1
            report["by_risk"][assessment["risk_level"]] += 1
            report["accounts"].append({
                "name": acct.get("Name") or acct.get("SamAccountName", ""),
                "source": acct.get("source", ""),
                **assessment,
            })

        report["by_source"] = dict(report["by_source"])
        report["by_risk"] = dict(report["by_risk"])
        report["accounts"].sort(key=lambda a: a["risk_score"], reverse=True)
        print(json.dumps(report, indent=2, default=str))
        return report


def main():
    domain = sys.argv[1] if len(sys.argv) > 1 else None
    auditor = ServiceAccountAuditor(domain=domain)
    auditor.discover_ad_service_accounts()
    auditor.discover_aws_iam_users()
    auditor.generate_report()


if __name__ == "__main__":
    main()
process.py11.5 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""
Service Account Audit Engine

Discovers, classifies, and audits service accounts across enterprise
infrastructure. Identifies orphaned, over-privileged, and non-compliant
service accounts with remediation recommendations.
"""

import json
import datetime
from typing import Dict, List, Optional
from dataclasses import dataclass, field
from collections import defaultdict


@dataclass
class ServiceAccount:
    """Represents a service account across any platform."""
    account_id: str
    username: str
    platform: str  # ad, aws, azure, gcp, database, application
    account_type: str  # service, gmsa, iam_user, iam_role, service_principal, managed_identity
    owner: str = ""
    application: str = ""
    privilege_level: str = "standard"  # standard, elevated, admin, domain_admin
    password_last_set: Optional[str] = None
    last_logon: Optional[str] = None
    password_never_expires: bool = False
    interactive_logon_allowed: bool = False
    member_of_privileged_group: bool = False
    has_spn: bool = False
    status: str = "active"


@dataclass
class AuditFinding:
    severity: str
    category: str
    title: str
    details: str
    affected_accounts: List[str] = field(default_factory=list)
    remediation: str = ""


class ServiceAccountAuditor:
    """Audits service accounts for security compliance."""

    def __init__(self, password_max_age_days: int = 90, stale_threshold_days: int = 90):
        self.accounts: List[ServiceAccount] = []
        self.findings: List[AuditFinding] = []
        self.password_max_age = password_max_age_days
        self.stale_threshold = stale_threshold_days

    def load_accounts(self, accounts: List[Dict]):
        for a in accounts:
            self.accounts.append(ServiceAccount(**a))

    def audit_all(self) -> List[AuditFinding]:
        self.findings = []
        self._check_orphaned_accounts()
        self._check_password_age()
        self._check_stale_accounts()
        self._check_privilege_levels()
        self._check_interactive_logon()
        self._check_password_never_expires()
        self._check_kerberoastable()
        self._check_gmsa_candidates()
        return self.findings

    def _check_orphaned_accounts(self):
        orphaned = [a for a in self.accounts if not a.owner and a.status == "active"]
        if orphaned:
            self.findings.append(AuditFinding(
                severity="high",
                category="Orphaned Accounts",
                title=f"{len(orphaned)} service accounts have no assigned owner",
                details="Accounts without owners cannot be reviewed or maintained.",
                affected_accounts=[f"{a.username}@{a.platform}" for a in orphaned],
                remediation="Assign owner using CMDB/application inventory. Disable if no owner found."
            ))

    def _check_password_age(self):
        now = datetime.datetime.now()
        stale_pwd = []
        for a in self.accounts:
            if a.status != "active" or a.account_type in ("gmsa", "managed_identity"):
                continue
            if a.password_last_set:
                try:
                    pwd_date = datetime.datetime.fromisoformat(a.password_last_set)
                    age = (now - pwd_date).days
                    if age > self.password_max_age:
                        stale_pwd.append(f"{a.username}@{a.platform}: {age} days old")
                except ValueError:
                    pass
            else:
                stale_pwd.append(f"{a.username}@{a.platform}: unknown age")

        if stale_pwd:
            self.findings.append(AuditFinding(
                severity="critical",
                category="Password Age",
                title=f"{len(stale_pwd)} service accounts exceed {self.password_max_age}-day password policy",
                details="Stale passwords increase risk of credential compromise.",
                affected_accounts=stale_pwd,
                remediation="Rotate credentials immediately. Implement automated rotation."
            ))

    def _check_stale_accounts(self):
        now = datetime.datetime.now()
        stale = []
        for a in self.accounts:
            if a.status != "active":
                continue
            if a.last_logon:
                try:
                    last = datetime.datetime.fromisoformat(a.last_logon)
                    if (now - last).days > self.stale_threshold:
                        stale.append(a)
                except ValueError:
                    pass
            elif not a.last_logon:
                stale.append(a)

        if stale:
            self.findings.append(AuditFinding(
                severity="medium",
                category="Stale Accounts",
                title=f"{len(stale)} service accounts inactive for {self.stale_threshold}+ days",
                details="Unused accounts are attack surface without business value.",
                affected_accounts=[f"{a.username}@{a.platform}" for a in stale],
                remediation="Validate with application owners. Disable if no longer needed."
            ))

    def _check_privilege_levels(self):
        over_priv = [a for a in self.accounts
                     if a.member_of_privileged_group and a.status == "active"]
        if over_priv:
            self.findings.append(AuditFinding(
                severity="critical",
                category="Over-Privileged",
                title=f"{len(over_priv)} service accounts in privileged groups",
                details="Service accounts with admin privileges are high-value targets.",
                affected_accounts=[f"{a.username}@{a.platform} ({a.privilege_level})" for a in over_priv],
                remediation="Reduce to minimum required permissions. Vault credentials in PAM."
            ))

    def _check_interactive_logon(self):
        interactive = [a for a in self.accounts
                       if a.interactive_logon_allowed and a.status == "active"]
        if interactive:
            self.findings.append(AuditFinding(
                severity="high",
                category="Interactive Logon",
                title=f"{len(interactive)} service accounts allow interactive logon",
                details="Service accounts should not permit interactive/remote logon.",
                affected_accounts=[f"{a.username}@{a.platform}" for a in interactive],
                remediation="Deny interactive logon via GPO/policy. Service accounts should only run as services."
            ))

    def _check_password_never_expires(self):
        never_exp = [a for a in self.accounts
                     if a.password_never_expires and a.account_type != "gmsa" and a.status == "active"]
        if never_exp:
            self.findings.append(AuditFinding(
                severity="high",
                category="Password Policy",
                title=f"{len(never_exp)} service accounts with PasswordNeverExpires",
                details="Non-expiring passwords circumvent rotation policies.",
                affected_accounts=[f"{a.username}@{a.platform}" for a in never_exp],
                remediation="Migrate to gMSA or implement automated PAM rotation."
            ))

    def _check_kerberoastable(self):
        kerberoastable = [a for a in self.accounts
                          if a.has_spn and a.account_type == "service" and a.status == "active"]
        if kerberoastable:
            self.findings.append(AuditFinding(
                severity="high",
                category="Kerberoasting",
                title=f"{len(kerberoastable)} accounts vulnerable to Kerberoasting",
                details="Accounts with SPNs can have their password hashes extracted offline.",
                affected_accounts=[f"{a.username}@{a.platform}" for a in kerberoastable],
                remediation="Use long (25+ char) passwords. Migrate to gMSA. Monitor for Kerberoast attacks."
            ))

    def _check_gmsa_candidates(self):
        candidates = [a for a in self.accounts
                      if a.platform == "ad" and a.account_type == "service" and a.status == "active"]
        if candidates:
            self.findings.append(AuditFinding(
                severity="low",
                category="gMSA Migration",
                title=f"{len(candidates)} AD service accounts eligible for gMSA migration",
                details="Group Managed Service Accounts provide automatic password rotation.",
                affected_accounts=[f"{a.username}" for a in candidates],
                remediation="Evaluate each account for gMSA compatibility. Plan migration."
            ))

    def generate_report(self) -> str:
        if not self.findings:
            self.audit_all()

        lines = [
            "=" * 70,
            "SERVICE ACCOUNT AUDIT REPORT",
            "=" * 70,
            f"Report Date: {datetime.datetime.now().isoformat()}",
            f"Total Accounts Audited: {len(self.accounts)}",
            f"Findings: {len(self.findings)}",
            "-" * 70, ""
        ]

        by_platform = defaultdict(int)
        for a in self.accounts:
            by_platform[a.platform] += 1
        lines.append("PLATFORM DISTRIBUTION:")
        for p, c in sorted(by_platform.items()):
            lines.append(f"  {p}: {c}")
        lines.append("")

        sev_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
        for f in sorted(self.findings, key=lambda x: sev_order.get(x.severity, 5)):
            lines.append(f"[{f.severity.upper()}] {f.title}")
            lines.append(f"  Category: {f.category}")
            lines.append(f"  {f.details}")
            if f.remediation:
                lines.append(f"  Fix: {f.remediation}")
            if f.affected_accounts:
                for a in f.affected_accounts[:5]:
                    lines.append(f"    - {a}")
                if len(f.affected_accounts) > 5:
                    lines.append(f"    ... and {len(f.affected_accounts) - 5} more")
            lines.append("")

        lines.append("=" * 70)
        critical = sum(1 for f in self.findings if f.severity == "critical")
        lines.append(f"OVERALL: {'FAIL' if critical else 'PASS WITH FINDINGS'}")
        lines.append("=" * 70)
        return "\n".join(lines)


def main():
    auditor = ServiceAccountAuditor()
    auditor.load_accounts([
        {"account_id": "1", "username": "svc_backup", "platform": "ad", "account_type": "service",
         "owner": "ops-team", "application": "Veeam Backup", "privilege_level": "admin",
         "password_last_set": "2025-06-15", "last_logon": "2026-02-22",
         "password_never_expires": True, "member_of_privileged_group": True, "has_spn": True},
        {"account_id": "2", "username": "svc_monitoring", "platform": "ad", "account_type": "service",
         "owner": "", "application": "", "privilege_level": "standard",
         "password_last_set": "2024-08-01", "last_logon": "2025-03-01",
         "password_never_expires": True, "has_spn": True},
        {"account_id": "3", "username": "app-api-key", "platform": "aws", "account_type": "iam_user",
         "owner": "dev-team", "application": "Web API", "privilege_level": "elevated",
         "password_last_set": "2026-01-15", "last_logon": "2026-02-23"},
        {"account_id": "4", "username": "svc_sql_agent", "platform": "ad", "account_type": "service",
         "owner": "dba-team", "application": "SQL Server", "privilege_level": "admin",
         "password_last_set": "2026-02-20", "last_logon": "2026-02-23",
         "interactive_logon_allowed": True, "member_of_privileged_group": True},
    ])
    print(auditor.generate_report())


if __name__ == "__main__":
    main()

Assets 1

template.mdtext/markdown · 0.9 KB
Keep exploring