Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-SkillsFramework mappings
MITRE ATT&CK
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
- AD Service Accounts: Windows services, scheduled tasks, IIS app pools
- Managed Service Accounts (gMSA): AD-managed automatic password rotation
- Cloud IAM Service Accounts: AWS IAM roles/users, Azure service principals, GCP service accounts
- Database Service Accounts: Application connection accounts, replication accounts
- 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
- Query AD for all service accounts (filter by description, OU, naming convention)
- Identify accounts with
ServicePrincipalNameset - List accounts in privileged groups (Domain Admins, Enterprise Admins)
- Check for gMSA vs traditional service accounts
- Identify accounts with
PasswordNeverExpiresflag
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
- NIST SP 800-53 AC-2: Account Management
- CIS Benchmark for Active Directory
- AWS IAM Best Practices: https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html
- Microsoft gMSA: https://learn.microsoft.com/en-us/windows-server/security/group-managed-service-accounts/group-managed-service-accounts-overview
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
- Export AD service accounts using PowerShell/LDAP queries
- Export cloud IAM service accounts (AWS credential report, Azure SP list, GCP SA list)
- Query databases for application-specific service accounts
- Consolidate into single inventory spreadsheet
- Cross-reference with CMDB for ownership data
Workflow 2: Assessment Phase
- Check each account against privilege policy
- Verify password/key rotation compliance (90-day max)
- Check last logon/activity date
- Validate owner assignment against HR data
- Flag accounts meeting orphaned/stale/over-privileged criteria
Workflow 3: Remediation Phase
- Contact owners of over-privileged accounts for justification
- Plan gMSA migration for eligible Windows service accounts
- Disable orphaned accounts (staged: disable first, delete after 30 days)
- Rotate stale credentials immediately
- 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 KBKeep exploring