| name | cloud-security-posture |
| description | Assess and improve cloud security posture across AWS, GCP, or Azure. Outputs CSPM findings, misconfiguration remediation, IAM least-privilege analysis, and continuous monitoring setup. |
| argument-hint | ["cloud provider","account count","compliance framework","current tooling"] |
| allowed-tools | Read, Write, Bash |
Cloud Security Posture Management (CSPM)
Cloud misconfigurations are the leading cause of cloud security breaches — not sophisticated exploits. Publicly accessible S3 buckets, overly permissive IAM roles, and unencrypted databases are common findings. CSPM is the continuous practice of detecting, prioritising, and remediating these misconfigurations.
Process
- Enable cloud-native security tools. AWS Security Hub, GCP Security Command Center, Azure Defender for Cloud — free baselines with no agents.
- Deploy CSPM scanner. Prowler (open source), Wiz, Orca, or Lacework for comprehensive multi-account coverage.
- Triage findings. Prioritise by severity and exploitability. Critically exposed resources (public internet facing) first.
- Remediate programmatically. Fix misconfigurations in IaC; detect drift with policy-as-code.
- Enforce with guardrails. AWS SCPs, GCP Organization Policies — prevent misconfigurations from being created.
- IAM least privilege. Analyse and reduce excessive permissions.
- Monitor continuously. CloudTrail / Cloud Audit Logs — alert on high-risk API calls.
AWS Security Assessment with Prowler
pip install prowler
prowler aws --output-formats json html csv \
--output-directory prowler-output/ \
--log-level ERROR
prowler aws \
--severity critical high \
--output-formats json \
--output-directory prowler-output/
prowler aws --compliance cis_2.0_aws
prowler aws --services s3 iam ec2 rds
python3 << 'EOF'
import json
from pathlib import Path
findings = []
for f in Path("prowler-output").glob("*.json"):
data = json.loads(f.read_text())
for r in data:
if r.get('status') == 'FAIL' and r.get('severity') in ['critical', 'high']:
findings.append({
"check": r['check_id'],
"title": r['check_title'],
"severity": r['severity'],
"resource": r.get('resource_id'),
"region": r.get('region'),
"remediation": r.get('remediation', {}).get('recommendation', {}).get('text'),
})
findings.sort(key=lambda x: {'critical': 0, 'high': 1}.get(x['severity'], 2))
for f in findings[:20]:
print(f"[{f['severity'].upper()}] {f['title']}")
(f)
(f)
EOF
IAM Least Privilege Analysis
import boto3
from datetime import datetime, timedelta
class IAMAnalyser:
def __init__(self):
self.iam = boto3.client('iam')
self.access_analyser = boto3.client('accessanalyzer')
def find_unused_permissions(self, days_threshold: int = 90) -> list:
"""Find permissions not used in the last N days."""
findings = []
paginator = self.iam.get_paginator('list_roles')
for page in paginator.paginate():
for role in page['Roles']:
role_arn = role['Arn']
try:
response = self.iam.generate_service_last_accessed_details(Arn=role_arn)
job_id = response['JobId']
import time
while True:
details = self.iam.get_service_last_accessed_details(JobId=job_id)
if details['JobStatus'] in ['COMPLETED', 'FAILED']:
time.sleep()
cutoff = datetime.utcnow() - timedelta(days=days_threshold)
unused_services = []
service details.get(, []):
last_used = service.get()
last_used last_used.replace(tzinfo=) < cutoff:
unused_services.append(service[])
unused_services:
findings.append({
: role[],
: role_arn,
: unused_services,
: ,
})
Exception e:
findings
() -> :
findings = []
paginator = .iam.get_paginator()
page paginator.paginate():
role page[]:
policy_name .iam.list_role_policies(
RoleName=role[]
)[]:
policy = .iam.get_role_policy(
RoleName=role[],
PolicyName=policy_name,
)[]
statement policy.get(, []):
statement.get() != :
actions = statement.get(, [])
resources = statement.get(, [])
(actions, ): actions = [actions]
(resources, ): resources = [resources]
actions resources:
findings.append({
: ,
: role[],
: policy_name,
: ,
})
actions:
findings.append({
: ,
: role[],
: policy_name,
: ,
})
findings
() -> :
findings = []
s3 = boto3.client()
bucket s3.list_buckets()[]:
name = bucket[]
:
acl = s3.get_bucket_acl(Bucket=name)
grant acl[]:
grant[].get() == :
findings.append({
: ,
: ,
: name,
: ,
:
})
public_access = s3.get_public_access_block(Bucket=name)[]
(public_access.values()):
findings.append({
: ,
: ,
: name,
: ,
:
})
Exception:
rds = boto3.client()
db rds.describe_db_instances()[]:
db.get():
findings.append({
: ,
: ,
: db[],
: ,
:
})
findings
Service Control Policies (AWS SCPs)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyRootAccess",
"Effect": "Deny",
"Principal": "*",
"Action": "*",
"Resource": "*",
"Condition": {
"StringLike": {
"aws:PrincipalArn": "arn:aws:iam::*:root"
}
}
},
{
"Sid": "RequireMFA",
"Effect": "Deny",
"NotAction": [
"iam:CreateVirtualMFADevice"
CloudTrail Alerting
HIGH_RISK_EVENTS = {
"ConsoleLogin": "Root account login",
"DeleteTrail": "CloudTrail disabled",
"StopLogging": "CloudTrail logging stopped",
"DeleteBucket": "S3 bucket deleted",
"PutBucketPolicy": "S3 bucket policy changed",
"CreateUser": "IAM user created",
"AttachUserPolicy": "Admin policy attached to user",
"CreateAccessKey": "Access key created",
"AuthorizeSecurityGroupIngress": "Security group opened",
}
resource "aws_cloudwatch_event_rule" "security_events" {
name = "high-risk-api-calls"
event_pattern = jsonencode({
source = ["aws.cloudtrail"]
detail-type = ["AWS API Call via CloudTrail"]
detail = {
eventName = keys(HIGH_RISK_EVENTS)
}
})
}
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Fix |
|---|
| Manual posture checks | Drift undetected between audits | Continuous automated scanning |
| Fixing findings in console only | IaC drift; reverts on next deploy | Fix in Terraform/CloudFormation; never in console |
| Alerting on all findings | Alert fatigue → ignored alerts | Prioritise by severity + exposure (internet-facing first) |
| No baseline | Can't measure improvement | Score posture on day 1; track over time |
| SCPs as afterthought | Misconfigurations created before prevention | SCPs deployed before onboarding accounts |
| Shared admin credentials | No accountability; can't revoke individual access | Individual IAM users with MFA; no shared root |
10 Rules
- Enable AWS Security Hub / GCP SCC / Azure Defender from day one — free baselines cost nothing.
- Public internet-facing misconfigurations (S3 ACLs, RDS publicly accessible) are Priority 1 — all else waits.
- Fix misconfigurations in IaC, not the console — console fixes revert on next deployment.
- SCPs prevent misconfigurations from being created — they are more valuable than detection.
- IAM least privilege is not a one-time exercise — review access quarterly and after every incident.
- Root account has no access keys — ever. MFA required. Used only for break-glass scenarios.
- CloudTrail is enabled in every region, every account, with log file integrity enabled.
- Track posture score over time — a CSPM finding fixed and regressed is worse than unfixed.
- Misconfigurations in dev/staging accounts are a preview of production — fix them.
- CSPM findings without owners don't get fixed — assign every finding to a team with an SLA.