cloud-assessment

Multi-cloud READ-ONLY security assessment methodology for AWS, Azure, and GCP using CIS benchmark-aligned checks

SKILL.md

GitHub

Cloud Security Assessment Methodology

Multi-cloud READ-ONLY security assessment using the cloud_audit tool. All checks use describe/list/get CLI calls via native TypeScript — no Python dependency, no SDK imports. Uses aws/az/gcloud CLIs. Aligned with CIS benchmarks for AWS, Azure, and GCP.

Safety First

ALWAYS run verify_readonly before any other audit program. This confirms the current credentials have no dangerous write permissions. If the check returns FAIL, stop and request read-only credentials.

cloud_audit verify_readonly --provider all

Assessment Phases

Phase 1 — Credential Safety Verification

Check Command Purpose
Verify read-only cloud_audit verify_readonly --provider all Confirm no write permissions — MUST pass before proceeding

Phase 2 — Identity & Access Management

IAM is the most critical attack surface in cloud environments.

Provider Command Key Checks
AWS cloud_audit aws_iam_audit --json-output MFA status, wildcard policies, unused keys, cross-account trust, root access keys
Azure cloud_audit azure_iam_audit --json-output Dangerous role assignments (Owner/Contributor), subscription-level owners, wildcard custom roles
GCP cloud_audit gcp_iam_audit --json-output Primitive roles (Owner/Editor at project), SA key age >90d, domain-wide delegation

Intelligence integration: After IAM audit, report findings via add_intel with type infrastructure:

add_intel type=infrastructure data="IAM audit: 3 users without MFA, 2 wildcard policies found"

Phase 3 — Storage Security

Public storage buckets are the #1 cloud data breach vector.

Provider Command Key Checks
AWS cloud_audit aws_storage_audit --json-output S3 Block Public Access, ACLs, default encryption, versioning, access logging
Azure cloud_audit azure_storage_audit --json-output Blob public access, HTTPS-only, minimum TLS version, SAS policies
GCP cloud_audit gcp_storage_audit --json-output allUsers/allAuthenticatedUsers bindings, uniform bucket-level access, versioning

Phase 4 — Network Security

Open security groups and missing flow logs are common misconfigurations.

Provider Command Key Checks
AWS cloud_audit aws_network_audit --json-output SGs open to 0.0.0.0/0 on dangerous ports, IMDSv1, VPC flow logs
Azure cloud_audit azure_network_audit --json-output NSG Any/Any rules, public IPs on VMs, NSG flow logs
GCP cloud_audit gcp_network_audit --json-output Firewall rules open to 0.0.0.0/0, external IPs, legacy networks

Phase 5 — Encryption at Rest

Unencrypted storage is a compliance violation in most frameworks.

Provider Command Key Checks
AWS cloud_audit aws_encryption_audit --json-output EBS/RDS encryption, KMS key rotation, CMK vs AWS-managed
Azure cloud_audit azure_encryption_audit --json-output Disk encryption, storage CMK, Key Vault rotation
GCP cloud_audit gcp_encryption_audit --json-output Disk/SQL/GCS CMEK, KMS key rotation

Phase 6 — Logging & Monitoring

Missing audit logs mean attacks go undetected.

Provider Command Key Checks
AWS cloud_audit aws_logging_audit --json-output CloudTrail multi-region, GuardDuty, Config recorder
Azure cloud_audit azure_logging_audit --json-output Activity Log retention, Diagnostic settings, Defender status
GCP cloud_audit gcp_logging_audit --json-output Audit log config (DATA_READ/DATA_WRITE), log sinks, filters

Phase 7 — DNS & TLS

External-facing services need DNS security and valid TLS.

Check Command Key Checks
DNS cloud_audit dns_audit --domain TARGET Dangling CNAMEs (subdomain takeover), DNSSEC, CAA records
TLS cloud_audit tls_audit --target HOST:PORT Protocol version, certificate expiry, cipher strength, HSTS

Vulnerability Reporting

For each FAIL finding, report via report_vulnerability:

report_vulnerability
  title: "AWS IAM user without MFA: admin-user"
  severity: high
  evidence:
    requestSent: "cloud_audit aws_iam_audit --json-output"
    responseCode: 0
    responseSummary: "checkId AWS-IAM-001 FAIL — user admin-user has console access without MFA"
    reasoning: "CIS AWS 1.10 requires MFA for all IAM users with console access"

Coverage Notes

Use record_coverage_note with scope: "wide" for account-level findings:

record_coverage_note
  scope: wide
  note: "AWS IAM audit complete — 5 findings across 12 users. No root access keys detected."

Program Reference

Program Domain Providers
verify_readonly Safety AWS, Azure, GCP
aws_iam_audit IAM AWS
azure_iam_audit IAM Azure
gcp_iam_audit IAM GCP
aws_storage_audit Storage AWS
azure_storage_audit Storage Azure
gcp_storage_audit Storage GCP
aws_network_audit Network AWS
azure_network_audit Network Azure
gcp_network_audit Network GCP
aws_encryption_audit Encryption AWS
azure_encryption_audit Encryption Azure
gcp_encryption_audit Encryption GCP
aws_logging_audit Logging AWS
azure_logging_audit Logging Azure
gcp_logging_audit Logging GCP
dns_audit DNS Cross-cloud
tls_audit TLS Cross-cloud


5. CONFIRMING THE FINDING

Step Question What it proves
1 Is the account or subscription actually in scope, per the signed rules of engagement? authority to test at all
2 Did you establish the credential's identity from the provider, not from documentation? the starting principal
3 Did you read a real resource within scope (an object, a secret, a row)? a finding, not a permission listing
4 Did the control identity fail the same call? escalation, not the baseline grant
5 Was the activity logged where the client can see it (CloudTrail, Activity Log, Audit Log)? the report is detectable and scoped
6 Did you revert every write, and verify the reversion? engagement integrity
7 Is the exposure still live at report time? it has not already been fixed

A scoped, logged, reverted, and read-verified finding is the bar. A cloud assessment that produces permission listings and no reads has produced inventory, not findings.


6. EXECUTION PRIMITIVES

Assessment findings are proven by a read within scope, a control that fails, and a reversion you can show. Every block below is provider-neutral in shape and ends at an artefact.

6.1 Pre-flight: scope, authority, and the audit trail

# confirm the identity you are supposed to be using, per the engagement
aws sts get-caller-identity 2>/dev/null || az account show --query '{sub:id,tenant:tenantId}' -o json 2>/dev/null || gcloud auth list --format='value(account)' 2>/dev/null
# confirm the accounts in scope, and record the ones you must NOT touch
cat /tmp/scope-accounts.txt 2>/dev/null
# confirm that your activity is being logged BEFORE you start, so the client can scope the report
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=GetCallerIdentity --max-results 1 2>/dev/null | head -5
az monitor activity-log list --max-events 1 2>/dev/null | head -5
gcloud logging read 'protoPayload.methodName="google.cloud.audit.AuditLog"' --limit=1 2>/dev/null | head -5

Confirm logging before the first probe. An assessment whose actions are not in the client's logs cannot be scoped afterwards, and that is a reportable process failure in itself.

6.2 Inventory, with the control

# the provider-neutral inventory: identity, then what that identity can see, then one read
aws organizations list-accounts 2>/dev/null | head -20
az account list --query '[].{name:name,id:id,state:state}' -o table 2>/dev/null | head -20
gcloud projects list --format='table(projectId,name)' 2>/dev/null | head -20
# the control: the calls that must fail, recorded as the baseline for every later claim
aws iam list-users 2>&1 | head -2; az keyvault list 2>&1 | head -2; gcloud secrets list 2>&1 | head -2

Record the inventory and the controls together. The value of an assessment is the delta between what the identity should see and what it does see.

6.3 Exposure checks that produce a finding

# public storage, which is the highest-frequency cloud exposure in every provider
aws s3api get-bucket-policy-status --bucket BUCKET --query 'PolicyStatus.IsPublic' 2>/dev/null
aws s3api get-public-access-block --bucket BUCKET 2>/dev/null | head -20
az storage container show-permission --account-name ACCT --name CONTAINER --query publicAccess -o tsv 2>/dev/null
gcloud storage buckets describe gs://BUCKET --format='value(iamConfiguration.publicAccessPrevention)' 2>/dev/null
# and the identity that should never be public
aws iam get-policy-version --policy-arn ARN --version-id v1 --query 'PolicyVersion.Document.Statement[?Effect==`Allow`].Principal' 2>/dev/null
# the network surface, which is the other half
aws ec2 describe-security-groups --query 'SecurityGroups[?IpPermissions[?IpRanges[?CidrIp==`0.0.0.0/0`]]].[GroupId,GroupName]' --output table 2>/dev/null | head -20

Each exposure check pairs a configuration read with a data read. A bucket policy marked public is the configuration; an object fetched over the public internet is the finding.

6.4 IAM over-permission, measured by use

# do not report the policy - report the USE of the permission
# 1) identify the over-broad binding
aws iam list-attached-user-policies --user-name USER 2>/dev/null | head -10
# 2) exercise it, minimally and in scope
aws s3 ls --profile TARGET 2>/dev/null | head -5
# 3) and pair it with the identity that should have been the only holder
aws iam get-policy-version --policy-arn ARN --version-id v1 --query 'PolicyVersion.Document.Statement[].Resource' 2>/dev/null

The use is the finding. "This role holds s3:*" is a configuration statement; "this role read prod-backups/, which the client classified as restricted" is a finding with an owner.

6.5 Credential hygiene checks

# keys that have never been rotated, and keys with no last-used date
aws iam list-users --query 'Users[].UserName' --output text 2>/dev/null | tr '\t' '\n' | while read -r U; do
  aws iam list-access-keys --user-name "$U" --query "AccessKeyMetadata[?Status=='Active'].[UserName,AccessKeyId,CreateDate]" --output text 2>/dev/null
done | head -20
# keys older than the policy requires - the age is the finding, not the existence
python3 -c "
from datetime import datetime,timezone
print('flag keys with CreateDate older than the stated rotation policy, and state the age in days')"
# and the console users without MFA, which is the other hygiene finding
aws iam list-users --query 'Users[].UserName' --output text 2>/dev/null | tr '\t' '\n' | while read -r U; do
  M=$(aws iam list-mfa-devices --user-name "$U" --query 'length(MFADevices)' --output text 2>/dev/null)
  [ "$M" = "0" ] && echo "no MFA: $U"
done | head -20

An unrotated key is a finding with an age attached. Report the number of days, the policy it violates, and the permissions the key holds - not merely that a key exists.

6.6 Cross-account and trust relationships

# who can enter this account, and who this account can enter
aws iam list-roles --query 'Roles[].RoleName' --output text 2>/dev/null | tr '\t' '\n' | while read -r R; do
  T=$(aws iam get-role --role-name "$R" --query 'Role.AssumeRolePolicyDocument.Statement[].Principal' 2>/dev/null)
  echo "$T" | grep -q 'arn:aws:iam::[0-9]' && echo "cross-account trust: $R -> $T"
done | head -20
# and the resource policies, which are the quieter path
aws s3api get-bucket-policy --bucket BUCKET 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
for s in json.loads(d['Policy'])['Statement']:
    p=s.get('Principal'); 
    if p and p!='*': print('principal:',p,'actions:',s.get('Action'))" 2>/dev/null | head -10

A cross-account trust with no external ID and no condition is the finding. Name the trusting role, the trusted principal, and the absence of the condition.

6.7 The evidence hygiene a cloud report requires

# 1) redact before you store - never keep a live secret in the report artefact
python3 -c "
import re
print('for every secret, record: the identifier, the read time, the permission used, and the fact it was read.')
print('never the value. the client can re-read it; the report should not carry it.')"
# 2) attach the provider's own evidence - the request ID ties your action to their log
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=GetSecretValue \
  --max-results 3 --query 'Events[].CloudTrailEvent' --output text 2>/dev/null | head -c 400; echo
# 3) and record the reversion for every write you performed
echo "for each write: the creating call, the deleting call, and the verification call"

Provider request IDs are the strongest evidence available. They tie the tester's action to the client's log, and they are the reason a cloud assessment can be scoped precisely.

6.8 A harness over the assessment phases

python3 - <<'PY'
import subprocess, shlex
def run(name, cmd, timeout=45):
    try:
        r = subprocess.run(shlex.split(cmd), capture_output=True, text=True, timeout=timeout)
        ok = r.returncode == 0
        out = (r.stdout + r.stderr).strip().replace("\n", " ")[:70]
    except Exception as e:
        ok, out = False, type(e).__name__
    print("  %-34s %s %s" % (name, "OK  " if ok else "FAIL", out))
    return ok

print("PHASE 1 - identity and logging")
run("identity", "aws sts get-caller-identity")
run("logging-visible", "aws cloudtrail lookup-events --max-results 1")
print()
print("PHASE 2 - controls (these should FAIL at the assessment identity)")
run("control-iam", "aws iam list-users")
run("control-kv", "aws secretsmanager list-secrets")
print()
print("PHASE 3 - scoped reads (these are the findings, one per service)")
run("buckets", "aws s3 ls")
run("instances", "aws ec2 describe-instances")
print()
print("PHASE 4 - reversion verification (run LAST, and only for writes you performed)")
print("  report the deleting call and a verification call for every write, or state that none were made")
PY

Four phases in order: identity + logging, controls, reads, reversion. An assessment report that skips the controls or the reversion phase is not evidence-based.


7. EVIDENCE STANDARD

Item Why
The account or subscription ID and its in-scope status authority and scope
The identity used for the assessment, from the provider the credential under test
The control call result the baseline every finding is measured against
The provider request ID for each significant action ties the finding to the client's own log
Each finding as a resource read, not a permission listing impact
The dataclass or owner of each affected resource who acts on it
The reversion proof for every write engagement integrity
Confirmation that logging captured the activity the report is scoped and hunts are possible
Redaction of every secret value, referenced by identifier only no unnecessary disclosure
The stated boundaries - resources deliberately not touched the negative space is part of the report

Report the identity, the control, and the read: "using the assessment identity arn:aws:iam::123456789012:user/pentest-readonly, sts get-caller-identity confirmed the account, and the control calls iam list-users and secretsmanager list-secrets both returned AccessDenied, confirming the read-only baseline. s3 ls returned 14 buckets including prod-customer-exports; the bucket's policy grants Principal: "*" with s3:GetObject, and an unauthenticated curl of a known object returned a 200 with a CSV containing 2,000 customer records. CloudTrail recorded the read events under request ID ... and the request was logged in the client's trail", never "the account has public buckets".

False positives - do not report these

Observation Why it is not a finding
A permission listed but not exercised the inventory, not a finding
A Denied response the control working, and worth reporting as a positive control
A publicly-readable object that is intended to be public (a website asset) verify the classification with the client
A key older than the policy but with no permissions report the age with the actual reach
A cross-account trust that requires an external ID and a condition that is the correct configuration
A 0.0.0.0/0 rule on a port serving a public service verify the intended exposure
A finding in an account outside the signed scope an engagement violation
A resource in a sandbox or test account the client excluded in scope only if named
A secret value reproduced in full in the report a disclosure
A finding the client has already remediated during the engagement verify liveness before publishing
Permission listings without a single read not an evidence-based assessment

Controls are findings too. A control that correctly denied you is worth stating explicitly, because it tells the client their baseline works.


8. REMEDIATION REFERENCE

  1. Give each identity the minimum permission it needs, at the narrowest scope, and re-run the unused-permission analysis quarterly - the review is the control, not the initial grant.
  2. Require short-lived credentials via roles or workload identity, and eliminate long-lived access keys - it removes the class of finding that cannot be revoked quickly.
  3. Enforce MFA on every console and privileged programmatic path, with conditional access - it closes the simplest credential path in all three providers.
  4. Enforce public-access blocks at the account or organization level, and alert on any policy that grants a public principal - the account-level block overrides the individual bucket.
  5. Log every control-plane action to a trail in a separate, restricted account with a locked retention policy - the assessment's own evidence comes from this, and an attacker with account access must not be able to delete it.
  6. Alert on the escalation primitives (CreateAccessKey, AttachUserPolicy, SetIamPolicy, AssumeRole, role assignment create) - each maps directly to a finding class in this document.
  7. Use an external ID and a condition on every cross-account trust, and review unused trusts - the confused-deputy problem is entirely the missing condition.
  8. Rotate secrets centrally with a managed store, and alert on unexpected read patterns - the read event is the detection.
  9. Define a resource classification, and test the public exposure of each class on a schedule - the assessment is only as good as the classification it tests against.
  10. Restrict the deploy-time identities (CI roles, build service accounts) to the minimum, because they are code execution by design - they are consistently the most over-permissioned identities in a cloud estate.
  11. Establish a logging-and-scoping practice for external assessments, and verify it before the first probe - it is the difference between a report that can be scoped and one that cannot.

9. RELATED SIBLINGS - LOAD TOGETHER