Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Detecting compromised cloud credentials across AWS, Azure, and GCP by analyzing anomalous API activity, impossible travel patterns, unauthorized resource provisioning, and credential abuse indicators using GuardDuty, Defender for Identity, and SCC Event Threat Detection.
.claude/skills/detecting-compromised-cloud-credentials/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | — | — |
| case-06 | ✗→✓ | ▲ Improved | — | — |
| case-05 | ✗→✓ | ▲ Improved | — | — |
| case-08 | ✗→✓ | ▲ Improved | — | — |
| case-11 | ✗→✓ | ▲ Improved | — | — |
Do not use for preventing credential compromise (use MFA, credential rotation, and secrets management), for detecting application-level credential theft (use application security monitoring), or for endpoint credential harvesting detection (use EDR tools).
Monitor GuardDuty findings and CloudTrail anomalies that indicate credential abuse.
bash# List GuardDuty credential-related findings aws guardduty list-findings \ --detector-id $(aws guardduty list-detectors --query 'DetectorIds[0]' --output text) \ --finding-criteria '{ "Criterion": { "type": { "Eq": [ "UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS", "UnauthorizedAccess:IAMUser/MaliciousIPCaller", "UnauthorizedAccess:IAMUser/MaliciousIPCaller.Custom", "UnauthorizedAccess:IAMUser/TorIPCaller", "UnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B", "Recon:IAMUser/MaliciousIPCaller", "Recon:IAMUser/MaliciousIPCaller.Custom", "InitialAccess:IAMUser/AnomalousBehavior", "CredentialAccess:IAMUser/AnomalousBehavior", "Persistence:IAMUser/AnomalousBehavior" ] }, "service.archived": {"Eq": ["false"]} } }' --output json # Check for console logins from new locations aws logs start-query \ --log-group-name cloudtrail-logs \ --start-time $(date -d "7 days ago" +%s) \ --end-time $(date +%s) \ --query-string ' fields @timestamp, userIdentity.userName, sourceIPAddress, responseElements.ConsoleLogin | filter eventName = "ConsoleLogin" | filter responseElements.ConsoleLogin = "Success" | stats count() by userIdentity.userName, sourceIPAddress | sort count desc ' # Detect impossible travel (same user from geographically distant IPs within short time) aws logs start-query \ --log-group-name cloudtrail-logs \ --start-time $(date -d "24 hours ago" +%s) \ --end-time $(date +%s) \ --query-string ' fields @timestamp, userIdentity.arn, sourceIPAddress, eventName | filter userIdentity.type = "IAMUser" | stats earliest(@timestamp) as first_seen, latest(@timestamp) as last_seen, count_distinct(sourceIPAddress) as unique_ips by userIdentity.arn | filter unique_ips > 3 '
Monitor Entra ID sign-in logs and Defender for Identity alerts for compromised credentials.
bash# Check for risky sign-ins az rest --method GET \ --url "https://graph.microsoft.com/v1.0/auditLogs/signIns?\$filter=riskLevelDuringSignIn ne 'none' and createdDateTime ge 2026-02-16T00:00:00Z&\$top=50" \ --query "value[*].{User:userPrincipalName,Risk:riskLevelDuringSignIn,IP:ipAddress,Location:location.city,App:appDisplayName,Status:status.errorCode}" \ -o table # Check for sign-ins from anonymous or Tor IPs az rest --method GET \ --url "https://graph.microsoft.com/v1.0/auditLogs/signIns?\$filter=riskEventTypes_v2/any(r:r eq 'anonymizedIPAddress') and createdDateTime ge 2026-02-22T00:00:00Z" \ --query "value[*].{User:userPrincipalName,IP:ipAddress,Location:location.city}" \ -o table # List users flagged as compromised by Identity Protection az rest --method GET \ --url "https://graph.microsoft.com/v1.0/identityProtection/riskyUsers?\$filter=riskLevel eq 'high'" \ --query "value[*].{User:userPrincipalName,RiskLevel:riskLevel,RiskState:riskState,LastDetected:riskLastUpdatedDateTime}" \ -o table # Check for suspicious application consent grants az rest --method GET \ --url "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?\$filter=activityDisplayName eq 'Consent to application' and activityDateTime ge 2026-02-16T00:00:00Z" \ --query "value[*].{Activity:activityDisplayName,User:initiatedBy.user.userPrincipalName,App:targetResources[0].displayName}" \ -o table
Query GCP audit logs and SCC findings for credential compromise indicators.
bash# Check SCC Event Threat Detection findings gcloud scc findings list ORG_ID \ --filter="state=\"ACTIVE\" AND (category=\"ANOMALOUS_CALLER_LOCATION\" OR category=\"SUSPICIOUS_LOGIN\" OR category=\"CREDENTIAL_ACCESS\")" \ --format="table(finding.category, finding.severity, finding.resourceName, finding.eventTime)" # Query audit logs for service account key usage from unusual IPs gcloud logging read ' protoPayload.authenticationInfo.principalEmail:*@*.iam.gserviceaccount.com AND protoPayload.requestMetadata.callerIp!=("10." OR "172." OR "192.168.") AND timestamp>="2026-02-22T00:00:00Z" ' --limit=100 --format="table(timestamp, protoPayload.authenticationInfo.principalEmail, protoPayload.requestMetadata.callerIp, protoPayload.methodName)" # Detect API calls from Tor exit nodes gcloud logging read ' protoPayload.requestMetadata.callerIp:("185." OR "198." OR "45.") AND protoPayload.authenticationInfo.principalEmail:*@company.com AND timestamp>="2026-02-22T00:00:00Z" ' --limit=50 --format=json # Check for new service account keys created (persistence indicator) gcloud logging read ' protoPayload.methodName="google.iam.admin.v1.CreateServiceAccountKey" AND timestamp>="2026-02-16T00:00:00Z" ' --format="table(timestamp, protoPayload.authenticationInfo.principalEmail, protoPayload.request.name)"
Create SIEM rules that correlate credential abuse indicators across cloud providers.
python# siem_correlation.py - Cross-cloud credential abuse detection import json from datetime import datetime, timedelta def detect_impossible_travel(events): """Detect same identity used from distant locations in short timeframe.""" user_events = {} for event in events: user = event.get('principal', '') ip = event.get('source_ip', '') ts = event.get('timestamp', '') cloud = event.get('cloud_provider', '') key = f"{user}_{cloud}" if key not in user_events: user_events[key] = [] user_events[key].append({'ip': ip, 'timestamp': ts, 'cloud': cloud}) alerts = [] for user_key, accesses in user_events.items(): accesses.sort(key=lambda x: x['timestamp']) for i in range(1, len(accesses)): time_diff = (datetime.fromisoformat(accesses[i]['timestamp']) - datetime.fromisoformat(accesses[i-1]['timestamp'])) if time_diff < timedelta(hours=1) and accesses[i]['ip'] != accesses[i-1]['ip']: alerts.append({ 'type': 'IMPOSSIBLE_TRAVEL', 'user': user_key, 'ip_1': accesses[i-1]['ip'], 'ip_2': accesses[i]['ip'], 'time_gap_minutes': time_diff.total_seconds() / 60, 'severity': 'HIGH' }) return alerts def detect_credential_stuffing(events, threshold=10): """Detect multiple failed logins followed by success.""" user_attempts = {} for event in events: user = event.get('principal', '') success = event.get('success', False) key = user if key not in user_attempts: user_attempts[key] = {'failures': 0, 'success_after_failures': False} if not success: user_attempts[key]['failures'] += 1 elif user_attempts[key]['failures'] >= threshold: user_attempts[key]['success_after_failures'] = True return [{'user': u, 'failures': d['failures'], 'severity': 'CRITICAL'} for u, d in user_attempts.items() if d['success_after_failures']]
Execute containment actions when credential compromise is confirmed.
bash# AWS: Deactivate access key immediately aws iam update-access-key --user-name COMPROMISED_USER \ --access-key-id AKIA_COMPROMISED --status Inactive # AWS: Invalidate temporary role credentials by updating role trust policy aws iam update-assume-role-policy --role-name COMPROMISED_ROLE \ --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":"*","Action":"sts:AssumeRole"}]}' # AWS: Revoke all sessions for an IAM user aws iam put-user-policy --user-name COMPROMISED_USER \ --policy-name RevokeOldSessions \ --policy-document '{ "Version":"2012-10-17", "Statement":[{ "Effect":"Deny", "Action":"*", "Resource":"*", "Condition":{"DateLessThan":{"aws:TokenIssueTime":"2026-02-23T10:00:00Z"}} }] }' # Azure: Revoke all sign-in sessions az rest --method POST \ --url "https://graph.microsoft.com/v1.0/users/COMPROMISED_USER_ID/revokeSignInSessions" # Azure: Force password reset az ad user update --id COMPROMISED_USER_ID --force-change-password-next-sign-in true # GCP: Disable service account gcloud iam service-accounts disable COMPROMISED_SA_EMAIL # GCP: Delete service account keys gcloud iam service-accounts keys delete KEY_ID --iam-account=COMPROMISED_SA_EMAIL
| Term | Definition | |------|------------| | Impossible Travel | Detection of the same credential being used from geographically distant locations within a time period that makes physical travel impossible | | Credential Stuffing | Attack using stolen username/password combinations from data breaches to attempt login across multiple cloud services | | Instance Credential Exfiltration | GuardDuty finding indicating EC2 instance role credentials are being used from outside the expected AWS network | | Anomalous Behavior | Machine learning-based detection of API call patterns that deviate significantly from the established baseline for a principal | | Session Revocation | Invalidating all active authentication sessions for a compromised principal to force re-authentication with new credentials | | Persistence Indicator | Attacker actions designed to maintain access after initial compromise, such as creating new access keys or service account keys |
Context: A developer receives a phishing email that harvests their AWS console credentials. The attacker logs in from a foreign IP, creates a new access key, and begins enumerating the account.
Approach:
UnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B for login from unusual countryPitfalls: Simply changing the password does not invalidate existing access keys or active sessions. All access keys must be rotated and temporary credentials revoked by adding a deny-all policy for tokens issued before the compromise was detected. Attackers may create new IAM users or roles for persistence before the initial credential is revoked.
Cloud Credential Compromise Detection Report
===============================================
Detection Date: 2026-02-23
Scope: Multi-cloud (AWS, Azure, GCP)
Period: 2026-02-16 to 2026-02-23
ACTIVE COMPROMISE INDICATORS:
[CRED-001] AWS Console Login from Unusual Location
User: developer@company.com
Source IP: 185.x.x.x (Russia)
Normal Location: US-East
GuardDuty Finding: UnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B
Severity: HIGH
Status: Credential deactivated
[CRED-002] Azure Impossible Travel Detection
User: admin@company.onmicrosoft.com
Location 1: New York, US (09:00 UTC)
Location 2: Beijing, CN (09:15 UTC)
Risk Level: HIGH
Status: Sessions revoked, under investigation
DETECTION METRICS (Last 7 Days):
Impossible travel detections: 5
Anomalous API activity alerts: 12
Failed login attempts > threshold: 3
New credentials from unusual IPs: 2
Total compromises confirmed: 2
CONTAINMENT ACTIONS TAKEN:
AWS access keys deactivated: 3
Azure sessions revoked: 2
GCP service accounts disabled: 1
Passwords force-reset: 4
MFA re-enrolled: 4| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-04 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 21 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +27 percentage points is the difference between those two pass rates over the 21 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.