mirror of
https://github.com/BagelHole/DevOps-Security-Agent-Skills.git
synced 2026-08-22 12:49:53 +02:00
V2
This commit is contained in:
@@ -9,68 +9,471 @@ metadata:
|
||||
|
||||
# Access Review
|
||||
|
||||
Implement periodic access review processes.
|
||||
Implement periodic access review processes for AWS IAM, GitHub, Okta, and other identity providers, including automated reporting, certification workflows, and unused permission detection.
|
||||
|
||||
## Review Process
|
||||
## When to Use
|
||||
|
||||
- Conducting quarterly or annual access reviews for compliance (SOC 2, HIPAA, PCI DSS, ISO 27001)
|
||||
- Identifying and removing stale accounts and unused credentials
|
||||
- Certifying that current access levels match job responsibilities
|
||||
- Detecting excessive privileges and dormant service accounts
|
||||
- Generating evidence for auditor requests on access governance
|
||||
|
||||
## Access Review Process
|
||||
|
||||
```yaml
|
||||
access_review_workflow:
|
||||
1_extract:
|
||||
- Pull access data from systems
|
||||
- Generate access report
|
||||
|
||||
2_review:
|
||||
- Manager certification
|
||||
- Risk-based prioritization
|
||||
- Decision documentation
|
||||
|
||||
3_action:
|
||||
- Revoke unnecessary access
|
||||
- Update exceptions
|
||||
- Document decisions
|
||||
|
||||
4_report:
|
||||
- Compliance metrics
|
||||
- Remediation tracking
|
||||
1_scope:
|
||||
actions:
|
||||
- Define systems in scope for the review cycle
|
||||
- Identify review owners (managers, system owners)
|
||||
- Set review timeline and deadlines
|
||||
- Generate access inventory from all identity sources
|
||||
frequency:
|
||||
privileged_access: Quarterly
|
||||
standard_access: Semi-annually
|
||||
service_accounts: Quarterly
|
||||
api_keys: Monthly
|
||||
|
||||
2_extract:
|
||||
actions:
|
||||
- Pull current access data from all systems
|
||||
- Correlate identities across platforms (SSO mapping)
|
||||
- Enrich with last login and activity data
|
||||
- Flag accounts for review (inactive, over-privileged, orphaned)
|
||||
|
||||
3_review:
|
||||
actions:
|
||||
- Assign review items to appropriate managers
|
||||
- Manager certifies each user's access (approve/revoke/modify)
|
||||
- Risk-based prioritization (privileged users reviewed first)
|
||||
- Escalate non-responses after deadline
|
||||
decisions:
|
||||
approve: "Access is appropriate for current role"
|
||||
modify: "Access needs adjustment (reduce/change scope)"
|
||||
revoke: "Access is no longer needed"
|
||||
|
||||
4_remediate:
|
||||
actions:
|
||||
- Revoke access flagged for removal
|
||||
- Modify access as directed by reviewers
|
||||
- Document exceptions with justification
|
||||
- Confirm changes with system owners
|
||||
sla:
|
||||
revocations: "Complete within 5 business days of decision"
|
||||
modifications: "Complete within 10 business days"
|
||||
exceptions: "Approved by security team, documented, time-limited"
|
||||
|
||||
5_report:
|
||||
actions:
|
||||
- Generate completion metrics (% reviewed, % on time)
|
||||
- Document all decisions and actions taken
|
||||
- Archive evidence for compliance audits
|
||||
- Identify process improvements for next cycle
|
||||
```
|
||||
|
||||
## AWS IAM Review
|
||||
## AWS IAM Access Review Scripts
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# aws-iam-review.sh - Comprehensive IAM access review report
|
||||
|
||||
OUTPUT_DIR="./access-review/$(date +%Y-%m)"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
echo "=== AWS IAM Access Review ==="
|
||||
|
||||
# Generate credential report
|
||||
aws iam generate-credential-report
|
||||
aws iam get-credential-report --output text --query Content | base64 -d
|
||||
aws iam generate-credential-report > /dev/null
|
||||
sleep 10
|
||||
aws iam get-credential-report --output text --query Content | \
|
||||
base64 -d > "$OUTPUT_DIR/credential-report.csv"
|
||||
|
||||
# Find inactive users
|
||||
aws iam list-users | jq -r '.Users[] | select(.PasswordLastUsed < "2024-01-01") | .UserName'
|
||||
echo "--- Users Without MFA ---"
|
||||
aws iam get-credential-report --output text --query Content | base64 -d | \
|
||||
awk -F, 'NR>1 && $4=="true" && $8=="false" {print $1}' | \
|
||||
tee "$OUTPUT_DIR/users-without-mfa.txt"
|
||||
|
||||
# List unused access keys
|
||||
aws iam get-access-key-last-used --access-key-id AKIAXXXXXXXX
|
||||
echo "--- Inactive Users (90+ days) ---"
|
||||
THRESHOLD=$(date -d '90 days ago' +%Y-%m-%dT%H:%M:%S 2>/dev/null || date -v-90d +%Y-%m-%dT%H:%M:%S)
|
||||
aws iam get-credential-report --output text --query Content | base64 -d | \
|
||||
awk -F, -v t="$THRESHOLD" 'NR>1 && $5!="N/A" && $5!="no_information" && $5<t {
|
||||
print $1","$5
|
||||
}' | tee "$OUTPUT_DIR/inactive-users.csv"
|
||||
|
||||
echo "--- Stale Access Keys (90+ days unused) ---"
|
||||
for user in $(aws iam list-users --query 'Users[*].UserName' --output text); do
|
||||
for key_id in $(aws iam list-access-keys --user-name "$user" \
|
||||
--query 'AccessKeyMetadata[?Status==`Active`].AccessKeyId' --output text); do
|
||||
last_used=$(aws iam get-access-key-last-used --access-key-id "$key_id" \
|
||||
--query 'AccessKeyLastUsed.LastUsedDate' --output text)
|
||||
if [ "$last_used" = "None" ] || [ "$last_used" \< "$THRESHOLD" ]; then
|
||||
echo "$user,$key_id,$last_used"
|
||||
fi
|
||||
done
|
||||
done | tee "$OUTPUT_DIR/stale-access-keys.csv"
|
||||
|
||||
echo "--- Users With Admin Policies ---"
|
||||
for user in $(aws iam list-users --query 'Users[*].UserName' --output text); do
|
||||
policies=$(aws iam list-attached-user-policies --user-name "$user" \
|
||||
--query 'AttachedPolicies[*].PolicyName' --output text)
|
||||
if echo "$policies" | grep -qi "admin\|fullaccess"; then
|
||||
groups=$(aws iam list-groups-for-user --user-name "$user" \
|
||||
--query 'Groups[*].GroupName' --output text)
|
||||
echo "$user|policies:$policies|groups:$groups"
|
||||
fi
|
||||
done | tee "$OUTPUT_DIR/admin-users.txt"
|
||||
|
||||
echo "--- IAM Roles With Cross-Account Trust ---"
|
||||
for role in $(aws iam list-roles --query 'Roles[*].RoleName' --output text); do
|
||||
trust=$(aws iam get-role --role-name "$role" \
|
||||
--query 'Role.AssumeRolePolicyDocument' --output json 2>/dev/null)
|
||||
if echo "$trust" | grep -q '"AWS"' && echo "$trust" | grep -qv "$(aws sts get-caller-identity --query Account --output text)"; then
|
||||
echo "$role: $trust" | jq -c '.Statement[].Principal'
|
||||
fi
|
||||
done | tee "$OUTPUT_DIR/cross-account-roles.txt"
|
||||
|
||||
echo "--- Service Accounts (Programmatic Only) ---"
|
||||
aws iam get-credential-report --output text --query Content | base64 -d | \
|
||||
awk -F, 'NR>1 && $4=="false" && $9!="N/A" {print $1","$11","$16}' | \
|
||||
tee "$OUTPUT_DIR/service-accounts.csv"
|
||||
|
||||
echo "Report generated in $OUTPUT_DIR"
|
||||
```
|
||||
|
||||
## Automation
|
||||
## GitHub Access Review
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# github-access-review.sh - GitHub organization access audit
|
||||
|
||||
ORG="your-org"
|
||||
OUTPUT_DIR="./access-review/github/$(date +%Y-%m)"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
echo "=== GitHub Organization Access Review ==="
|
||||
|
||||
echo "--- Organization Members ---"
|
||||
gh api orgs/$ORG/members --paginate \
|
||||
--jq '.[] | [.login, .site_admin] | @csv' \
|
||||
> "$OUTPUT_DIR/org-members.csv"
|
||||
|
||||
echo "--- Organization Owners ---"
|
||||
gh api "orgs/$ORG/members?role=admin" --paginate \
|
||||
--jq '.[] | .login' \
|
||||
> "$OUTPUT_DIR/org-owners.txt"
|
||||
|
||||
echo "--- Outside Collaborators ---"
|
||||
gh api orgs/$ORG/outside_collaborators --paginate \
|
||||
--jq '.[] | .login' \
|
||||
> "$OUTPUT_DIR/outside-collaborators.txt"
|
||||
|
||||
echo "--- Repository Access Per Repo ---"
|
||||
for repo in $(gh repo list $ORG --json name -q '.[].name' --limit 500); do
|
||||
echo "Repo: $repo"
|
||||
gh api "repos/$ORG/$repo/collaborators" --paginate \
|
||||
--jq '.[] | [.login, .role_name] | @csv' \
|
||||
> "$OUTPUT_DIR/repo-$repo-access.csv" 2>/dev/null
|
||||
done
|
||||
|
||||
echo "--- Team Memberships ---"
|
||||
for team in $(gh api orgs/$ORG/teams --paginate --jq '.[].slug'); do
|
||||
echo "Team: $team"
|
||||
gh api "orgs/$ORG/teams/$team/members" --paginate \
|
||||
--jq '.[] | .login' \
|
||||
> "$OUTPUT_DIR/team-$team-members.txt"
|
||||
done
|
||||
|
||||
echo "--- Pending Invitations ---"
|
||||
gh api orgs/$ORG/invitations --paginate \
|
||||
--jq '.[] | [.login, .email, .role, .created_at] | @csv' \
|
||||
> "$OUTPUT_DIR/pending-invitations.csv"
|
||||
|
||||
echo "--- Deploy Keys ---"
|
||||
for repo in $(gh repo list $ORG --json name -q '.[].name' --limit 500); do
|
||||
keys=$(gh api "repos/$ORG/$repo/keys" --jq '.[].title' 2>/dev/null)
|
||||
if [ -n "$keys" ]; then
|
||||
echo "$repo: $keys"
|
||||
fi
|
||||
done > "$OUTPUT_DIR/deploy-keys.txt"
|
||||
|
||||
echo "--- Branch Protection Rules ---"
|
||||
for repo in $(gh repo list $ORG --json name -q '.[].name' --limit 500); do
|
||||
protection=$(gh api "repos/$ORG/$repo/branches/main/protection" 2>/dev/null)
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "$repo: protected"
|
||||
echo "$protection" | jq '{required_reviews: .required_pull_request_reviews.required_approving_review_count, dismiss_stale: .required_pull_request_reviews.dismiss_stale_reviews}' \
|
||||
> "$OUTPUT_DIR/branch-protection-$repo.json"
|
||||
else
|
||||
echo "$repo: NOT protected" >> "$OUTPUT_DIR/unprotected-repos.txt"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Report generated in $OUTPUT_DIR"
|
||||
```
|
||||
|
||||
## Okta Access Review
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# okta-access-review.sh - Okta user and application access audit
|
||||
# Requires OKTA_DOMAIN and OKTA_API_TOKEN environment variables
|
||||
|
||||
OUTPUT_DIR="./access-review/okta/$(date +%Y-%m)"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
BASE_URL="https://${OKTA_DOMAIN}/api/v1"
|
||||
|
||||
echo "=== Okta Access Review ==="
|
||||
|
||||
echo "--- Active Users ---"
|
||||
curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
|
||||
"$BASE_URL/users?filter=status+eq+%22ACTIVE%22&limit=200" | \
|
||||
jq -r '.[] | [.profile.email, .profile.firstName, .profile.lastName, .lastLogin, .created] | @csv' \
|
||||
> "$OUTPUT_DIR/active-users.csv"
|
||||
|
||||
echo "--- Suspended/Deprovisioned Users ---"
|
||||
for status in SUSPENDED DEPROVISIONED; do
|
||||
curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
|
||||
"$BASE_URL/users?filter=status+eq+%22$status%22&limit=200" | \
|
||||
jq -r '.[] | [.profile.email, .status, .statusChanged] | @csv'
|
||||
done > "$OUTPUT_DIR/inactive-users.csv"
|
||||
|
||||
echo "--- Users Without MFA Enrolled ---"
|
||||
curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
|
||||
"$BASE_URL/users?limit=200" | \
|
||||
jq -r '.[] | .id' | while read -r uid; do
|
||||
factors=$(curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
|
||||
"$BASE_URL/users/$uid/factors" | jq 'length')
|
||||
if [ "$factors" -eq 0 ]; then
|
||||
curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
|
||||
"$BASE_URL/users/$uid" | jq -r '.profile.email'
|
||||
fi
|
||||
done > "$OUTPUT_DIR/users-without-mfa.txt"
|
||||
|
||||
echo "--- Application Assignments ---"
|
||||
curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
|
||||
"$BASE_URL/apps?limit=200" | \
|
||||
jq -r '.[] | [.id, .label, .status] | @csv' | while IFS=, read -r app_id app_name status; do
|
||||
echo "App: $app_name"
|
||||
curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
|
||||
"$BASE_URL/apps/$app_id/users?limit=200" | \
|
||||
jq -r '.[] | [.credentials.userName // .profile.email, .status] | @csv'
|
||||
done > "$OUTPUT_DIR/app-assignments.csv"
|
||||
|
||||
echo "--- Admin Role Assignments ---"
|
||||
curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
|
||||
"$BASE_URL/users?limit=200" | \
|
||||
jq -r '.[] | .id' | while read -r uid; do
|
||||
roles=$(curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
|
||||
"$BASE_URL/users/$uid/roles" | jq -r '.[].type' 2>/dev/null)
|
||||
if [ -n "$roles" ]; then
|
||||
email=$(curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
|
||||
"$BASE_URL/users/$uid" | jq -r '.profile.email')
|
||||
echo "$email: $roles"
|
||||
fi
|
||||
done > "$OUTPUT_DIR/admin-roles.txt"
|
||||
|
||||
echo "Report generated in $OUTPUT_DIR"
|
||||
```
|
||||
|
||||
## Unused Permission Detection
|
||||
|
||||
```python
|
||||
def generate_access_report():
|
||||
users = get_all_users()
|
||||
report = []
|
||||
|
||||
"""
|
||||
Detect unused IAM permissions using CloudTrail and IAM Access Analyzer.
|
||||
Generates recommendations for right-sizing access.
|
||||
"""
|
||||
import boto3
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
|
||||
def analyze_iam_usage(days_lookback=90):
|
||||
"""Analyze IAM user and role activity against granted permissions."""
|
||||
iam = boto3.client("iam")
|
||||
|
||||
report = {
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"lookback_days": days_lookback,
|
||||
"findings": [],
|
||||
}
|
||||
|
||||
users = iam.list_users()["Users"]
|
||||
for user in users:
|
||||
report.append({
|
||||
'user': user.email,
|
||||
'roles': user.roles,
|
||||
'last_login': user.last_login,
|
||||
'manager': user.manager,
|
||||
'review_status': 'pending'
|
||||
})
|
||||
|
||||
username = user["UserName"]
|
||||
|
||||
# Get service last accessed data
|
||||
job_id = iam.generate_service_last_accessed_details(
|
||||
Arn=user["Arn"]
|
||||
)["JobId"]
|
||||
|
||||
while True:
|
||||
result = iam.get_service_last_accessed_details(JobId=job_id)
|
||||
if result["JobStatus"] == "COMPLETED":
|
||||
break
|
||||
time.sleep(2)
|
||||
|
||||
threshold = datetime.now(timezone.utc) - timedelta(days=days_lookback)
|
||||
unused_services = []
|
||||
|
||||
for service in result["ServicesLastAccessed"]:
|
||||
last_accessed = service.get("LastAuthenticated")
|
||||
if last_accessed is None or last_accessed < threshold:
|
||||
unused_services.append({
|
||||
"service": service["ServiceNamespace"],
|
||||
"last_accessed": str(last_accessed) if last_accessed else "Never",
|
||||
})
|
||||
|
||||
if unused_services:
|
||||
report["findings"].append({
|
||||
"type": "unused_permissions",
|
||||
"user": username,
|
||||
"arn": user["Arn"],
|
||||
"unused_service_count": len(unused_services),
|
||||
"unused_services": unused_services[:10],
|
||||
"recommendation": "Review and remove unused service permissions",
|
||||
})
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def detect_overprivileged_roles():
|
||||
"""Use IAM Access Analyzer to find overprivileged roles."""
|
||||
analyzer = boto3.client("accessanalyzer")
|
||||
|
||||
findings = analyzer.list_findings(
|
||||
analyzerArn="arn:aws:access-analyzer:us-east-1:123456789012:analyzer/org-analyzer",
|
||||
filter={
|
||||
"status": {"eq": ["ACTIVE"]},
|
||||
"resourceType": {"eq": ["AWS::IAM::Role"]},
|
||||
},
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
"resource": f["resource"],
|
||||
"resource_type": f["resourceType"],
|
||||
"condition": f.get("condition", {}),
|
||||
"principal": f.get("principal", {}),
|
||||
"action": f.get("action", []),
|
||||
"created_at": str(f["createdAt"]),
|
||||
}
|
||||
for f in findings.get("findings", [])
|
||||
]
|
||||
```
|
||||
|
||||
## Certification Workflow Automation
|
||||
|
||||
```yaml
|
||||
# GitHub Actions - Automated access review reminder and tracking
|
||||
name: Quarterly Access Review
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 9 1 1,4,7,10 *' # First day of each quarter
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
generate-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Generate access reports
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AUDIT_AWS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AUDIT_AWS_SECRET }}
|
||||
OKTA_DOMAIN: ${{ secrets.OKTA_DOMAIN }}
|
||||
OKTA_API_TOKEN: ${{ secrets.OKTA_API_TOKEN }}
|
||||
run: |
|
||||
bash scripts/aws-iam-review.sh
|
||||
bash scripts/github-access-review.sh
|
||||
bash scripts/okta-access-review.sh
|
||||
|
||||
- name: Create review issue
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
QUARTER="Q$(( ($(date +%-m) - 1) / 3 + 1 )) $(date +%Y)"
|
||||
MFA_MISSING=$(wc -l < access-review/$(date +%Y-%m)/users-without-mfa.txt)
|
||||
INACTIVE=$(wc -l < access-review/$(date +%Y-%m)/inactive-users.csv)
|
||||
STALE_KEYS=$(wc -l < access-review/$(date +%Y-%m)/stale-access-keys.csv)
|
||||
|
||||
gh issue create \
|
||||
--title "Access Review - $QUARTER" \
|
||||
--label "compliance,access-review" \
|
||||
--body "## Quarterly Access Review - $QUARTER
|
||||
|
||||
### Summary
|
||||
- Users without MFA: **$MFA_MISSING**
|
||||
- Inactive users (90+ days): **$INACTIVE**
|
||||
- Stale access keys: **$STALE_KEYS**
|
||||
|
||||
### Required Actions
|
||||
- [ ] Review and disable inactive users
|
||||
- [ ] Enforce MFA for non-compliant users
|
||||
- [ ] Rotate or deactivate stale access keys
|
||||
- [ ] Review admin/privileged access assignments
|
||||
- [ ] Review outside collaborators on GitHub
|
||||
- [ ] Certify remaining access is appropriate
|
||||
- [ ] Document exceptions with justification
|
||||
|
||||
### Deadline
|
||||
Complete within 30 days."
|
||||
|
||||
- name: Upload reports as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: access-review-reports
|
||||
path: access-review/
|
||||
retention-days: 365
|
||||
```
|
||||
|
||||
## Access Review Checklist
|
||||
|
||||
```yaml
|
||||
access_review_checklist:
|
||||
preparation:
|
||||
- [ ] Define scope (systems, user populations, review period)
|
||||
- [ ] Assign review owners for each system
|
||||
- [ ] Extract current access data from all identity sources
|
||||
- [ ] Correlate identities across platforms via SSO mapping
|
||||
- [ ] Generate review packages for each manager
|
||||
|
||||
execution:
|
||||
- [ ] Managers notified with review assignments and deadline
|
||||
- [ ] Privileged access reviewed first (admin, root, service accounts)
|
||||
- [ ] Each user's access certified (approve, modify, or revoke)
|
||||
- [ ] Inactive accounts flagged for disable/removal
|
||||
- [ ] Stale credentials (keys, tokens) flagged for rotation
|
||||
- [ ] Outside collaborators and contractors verified
|
||||
- [ ] Service account ownership confirmed
|
||||
|
||||
remediation:
|
||||
- [ ] Revocations executed within SLA (5 business days)
|
||||
- [ ] Access modifications completed within SLA (10 business days)
|
||||
- [ ] Exceptions documented with business justification
|
||||
- [ ] Exception approvals recorded from security team
|
||||
- [ ] Changes verified in target systems
|
||||
|
||||
reporting:
|
||||
- [ ] Review completion rate documented (target: 100%)
|
||||
- [ ] Non-response escalations documented
|
||||
- [ ] Remediation actions summarized
|
||||
- [ ] Exception register updated
|
||||
- [ ] Evidence archived for audit (retained 3+ years)
|
||||
- [ ] Metrics compared to prior review cycle
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Quarterly reviews minimum
|
||||
- Risk-based frequency
|
||||
- Manager attestation
|
||||
- Automated revocation
|
||||
- Audit trail maintenance
|
||||
- Automate access data extraction to eliminate manual data gathering and reduce errors
|
||||
- Integrate access review with HR systems to automatically flag accounts for departed employees
|
||||
- Use risk-based review frequency: privileged access quarterly, standard access semi-annually
|
||||
- Provide managers with clear context: show last login date, permissions, and role to inform decisions
|
||||
- Set firm deadlines with escalation for non-response (no certification = automatic revocation)
|
||||
- Detect and eliminate orphaned accounts from contractors, former employees, and decommissioned services
|
||||
- Review service accounts and API keys alongside human accounts to prevent credential sprawl
|
||||
- Document all exceptions with business justification, approver, and expiration date
|
||||
- Track review metrics over time: completion rates, revocation rates, time to remediate
|
||||
- Archive all access review evidence for a minimum of 3 years for audit purposes
|
||||
|
||||
@@ -9,66 +9,495 @@ metadata:
|
||||
|
||||
# Asset Inventory
|
||||
|
||||
Maintain comprehensive IT asset tracking.
|
||||
Maintain comprehensive IT asset inventory using automated discovery, AWS Config rules, cloud asset discovery scripts, CMDB integration, and tagging enforcement for compliance and operational visibility.
|
||||
|
||||
## Asset Categories
|
||||
## When to Use
|
||||
|
||||
- Building or maintaining an IT asset inventory for compliance frameworks (ISO 27001, SOC 2, FedRAMP)
|
||||
- Implementing automated cloud resource discovery across accounts and regions
|
||||
- Enforcing tagging standards for cost allocation, ownership, and data classification
|
||||
- Integrating asset data with a CMDB for operational workflows
|
||||
- Preparing for audits that require a complete system component inventory
|
||||
|
||||
## Asset Categories and Schema
|
||||
|
||||
```yaml
|
||||
asset_types:
|
||||
hardware:
|
||||
- Servers
|
||||
- Network devices
|
||||
- Endpoints
|
||||
|
||||
software:
|
||||
- Applications
|
||||
- Operating systems
|
||||
- Licenses
|
||||
|
||||
cloud:
|
||||
- Compute instances
|
||||
- Storage
|
||||
- Databases
|
||||
|
||||
data:
|
||||
- Databases
|
||||
- File shares
|
||||
- Backups
|
||||
asset_categories:
|
||||
compute:
|
||||
cloud:
|
||||
- EC2 instances / Azure VMs / GCE instances
|
||||
- Lambda functions / Azure Functions / Cloud Functions
|
||||
- ECS/EKS clusters and tasks
|
||||
- Container images in registries
|
||||
on_premise:
|
||||
- Physical servers
|
||||
- Virtual machines (VMware, Hyper-V)
|
||||
|
||||
storage:
|
||||
- S3 buckets / Azure Storage / GCS buckets
|
||||
- EBS volumes / Managed Disks / Persistent Disks
|
||||
- RDS instances / Azure SQL / Cloud SQL
|
||||
- DynamoDB tables / Cosmos DB / Firestore
|
||||
- EFS / Azure Files / Filestore
|
||||
|
||||
network:
|
||||
- VPCs / VNets / VPC Networks
|
||||
- Load balancers (ALB, NLB, Azure LB, GCP LB)
|
||||
- DNS zones and records
|
||||
- VPN gateways and connections
|
||||
- CDN distributions
|
||||
|
||||
security:
|
||||
- IAM users, roles, and policies
|
||||
- KMS keys / Key Vault keys
|
||||
- Certificates (ACM, Key Vault, Certificate Manager)
|
||||
- Security groups / NSGs / Firewall rules
|
||||
- WAF configurations
|
||||
|
||||
applications:
|
||||
- SaaS subscriptions
|
||||
- Licensed software
|
||||
- Custom applications
|
||||
- APIs and integrations
|
||||
|
||||
endpoints:
|
||||
- Laptops and desktops
|
||||
- Mobile devices
|
||||
- Printers and peripherals
|
||||
|
||||
asset_record_schema:
|
||||
required_fields:
|
||||
asset_id: "Unique identifier (auto-generated)"
|
||||
name: "Human-readable name"
|
||||
type: "Category from above taxonomy"
|
||||
provider: "AWS / Azure / GCP / On-Premise / SaaS"
|
||||
account_or_subscription: "Cloud account ID"
|
||||
region: "Deployment region/location"
|
||||
owner: "Team or individual responsible"
|
||||
data_classification: "Public / Internal / Confidential / Restricted"
|
||||
environment: "Production / Staging / Development / Sandbox"
|
||||
status: "Active / Decommissioning / Retired"
|
||||
created_date: "When the asset was provisioned"
|
||||
last_seen: "Last automated discovery timestamp"
|
||||
|
||||
optional_fields:
|
||||
cost_center: "For cost allocation"
|
||||
compliance_scope: "SOC2 / HIPAA / PCI / None"
|
||||
backup_policy: "Backup schedule reference"
|
||||
dr_tier: "Critical / Essential / Standard / Non-essential"
|
||||
expiration_date: "For time-limited resources"
|
||||
tags: "Key-value pairs from cloud provider"
|
||||
dependencies: "Upstream and downstream services"
|
||||
```
|
||||
|
||||
## AWS Inventory
|
||||
## AWS Resource Discovery Script
|
||||
|
||||
```bash
|
||||
# List all resources
|
||||
aws resourcegroupstaggingapi get-resources
|
||||
#!/usr/bin/env bash
|
||||
# aws-asset-discovery.sh - Discover and inventory all AWS resources
|
||||
|
||||
# EC2 instances
|
||||
aws ec2 describe-instances --query 'Reservations[*].Instances[*].[InstanceId,InstanceType,State.Name]'
|
||||
OUTPUT_DIR="./asset-inventory/aws/$(date +%Y-%m-%d)"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
|
||||
|
||||
# AWS Config
|
||||
aws configservice describe-configuration-recorders
|
||||
echo "=== AWS Asset Discovery for Account $ACCOUNT_ID ==="
|
||||
|
||||
# EC2 Instances
|
||||
echo "--- EC2 Instances ---"
|
||||
aws ec2 describe-instances \
|
||||
--query 'Reservations[*].Instances[*].{
|
||||
InstanceId:InstanceId,
|
||||
Type:InstanceType,
|
||||
State:State.Name,
|
||||
AZ:Placement.AvailabilityZone,
|
||||
VpcId:VpcId,
|
||||
PrivateIP:PrivateIpAddress,
|
||||
PublicIP:PublicIpAddress,
|
||||
LaunchTime:LaunchTime,
|
||||
Name:Tags[?Key==`Name`].Value|[0],
|
||||
Owner:Tags[?Key==`Owner`].Value|[0],
|
||||
Environment:Tags[?Key==`Environment`].Value|[0]
|
||||
}' --output json | jq 'flatten' > "$OUTPUT_DIR/ec2-instances.json"
|
||||
|
||||
# RDS Databases
|
||||
echo "--- RDS Instances ---"
|
||||
aws rds describe-db-instances \
|
||||
--query 'DBInstances[*].{
|
||||
DBInstanceId:DBInstanceIdentifier,
|
||||
Engine:Engine,
|
||||
EngineVersion:EngineVersion,
|
||||
Class:DBInstanceClass,
|
||||
Status:DBInstanceStatus,
|
||||
MultiAZ:MultiAZ,
|
||||
Encrypted:StorageEncrypted,
|
||||
Endpoint:Endpoint.Address,
|
||||
BackupRetention:BackupRetentionPeriod
|
||||
}' --output json > "$OUTPUT_DIR/rds-instances.json"
|
||||
|
||||
# S3 Buckets
|
||||
echo "--- S3 Buckets ---"
|
||||
aws s3api list-buckets --query 'Buckets[*].{Name:Name,Created:CreationDate}' --output json | \
|
||||
jq -c '.[]' | while read -r bucket; do
|
||||
name=$(echo "$bucket" | jq -r '.Name')
|
||||
region=$(aws s3api get-bucket-location --bucket "$name" --query 'LocationConstraint' --output text 2>/dev/null)
|
||||
encryption=$(aws s3api get-bucket-encryption --bucket "$name" 2>/dev/null | jq -r '.ServerSideEncryptionConfiguration.Rules[0].ApplyServerSideEncryptionByDefault.SSEAlgorithm' 2>/dev/null)
|
||||
versioning=$(aws s3api get-bucket-versioning --bucket "$name" --query 'Status' --output text 2>/dev/null)
|
||||
echo "{\"Name\":\"$name\",\"Region\":\"${region:-us-east-1}\",\"Encryption\":\"${encryption:-none}\",\"Versioning\":\"${versioning:-Disabled}\"}"
|
||||
done | jq -s '.' > "$OUTPUT_DIR/s3-buckets.json"
|
||||
|
||||
# Lambda Functions
|
||||
echo "--- Lambda Functions ---"
|
||||
aws lambda list-functions \
|
||||
--query 'Functions[*].{
|
||||
Name:FunctionName,
|
||||
Runtime:Runtime,
|
||||
MemorySize:MemorySize,
|
||||
Timeout:Timeout,
|
||||
LastModified:LastModified,
|
||||
CodeSize:CodeSize
|
||||
}' --output json > "$OUTPUT_DIR/lambda-functions.json"
|
||||
|
||||
# VPCs and Security Groups
|
||||
echo "--- VPCs ---"
|
||||
aws ec2 describe-vpcs \
|
||||
--query 'Vpcs[*].{
|
||||
VpcId:VpcId,
|
||||
CidrBlock:CidrBlock,
|
||||
State:State,
|
||||
IsDefault:IsDefault,
|
||||
Name:Tags[?Key==`Name`].Value|[0]
|
||||
}' --output json > "$OUTPUT_DIR/vpcs.json"
|
||||
|
||||
echo "--- Security Groups ---"
|
||||
aws ec2 describe-security-groups \
|
||||
--query 'SecurityGroups[*].{
|
||||
GroupId:GroupId,
|
||||
GroupName:GroupName,
|
||||
VpcId:VpcId,
|
||||
Description:Description,
|
||||
IngressRuleCount:length(IpPermissions),
|
||||
EgressRuleCount:length(IpPermissionsEgress)
|
||||
}' --output json > "$OUTPUT_DIR/security-groups.json"
|
||||
|
||||
# IAM Users and Roles
|
||||
echo "--- IAM Users ---"
|
||||
aws iam list-users \
|
||||
--query 'Users[*].{UserName:UserName,Created:CreateDate,PasswordLastUsed:PasswordLastUsed}' \
|
||||
--output json > "$OUTPUT_DIR/iam-users.json"
|
||||
|
||||
echo "--- IAM Roles ---"
|
||||
aws iam list-roles \
|
||||
--query 'Roles[*].{RoleName:RoleName,Created:CreateDate,LastUsed:RoleLastUsed.LastUsedDate}' \
|
||||
--output json > "$OUTPUT_DIR/iam-roles.json"
|
||||
|
||||
# EKS Clusters
|
||||
echo "--- EKS Clusters ---"
|
||||
aws eks list-clusters --query 'clusters' --output json | jq -r '.[]' | while read -r cluster; do
|
||||
aws eks describe-cluster --name "$cluster" \
|
||||
--query 'cluster.{Name:name,Version:version,Status:status,Endpoint:endpoint,Created:createdAt}'
|
||||
done | jq -s '.' > "$OUTPUT_DIR/eks-clusters.json" 2>/dev/null
|
||||
|
||||
# KMS Keys
|
||||
echo "--- KMS Keys ---"
|
||||
aws kms list-keys --query 'Keys[*].KeyId' --output text | tr '\t' '\n' | while read -r key_id; do
|
||||
aws kms describe-key --key-id "$key_id" \
|
||||
--query 'KeyMetadata.{KeyId:KeyId,Description:Description,State:KeyState,Created:CreationDate,Manager:KeyManager}' 2>/dev/null
|
||||
done | jq -s '.' > "$OUTPUT_DIR/kms-keys.json"
|
||||
|
||||
# Generate summary
|
||||
echo "=== Inventory Summary ==="
|
||||
echo "EC2 Instances: $(jq 'length' "$OUTPUT_DIR/ec2-instances.json")"
|
||||
echo "RDS Instances: $(jq 'length' "$OUTPUT_DIR/rds-instances.json")"
|
||||
echo "S3 Buckets: $(jq 'length' "$OUTPUT_DIR/s3-buckets.json")"
|
||||
echo "Lambda Functions: $(jq 'length' "$OUTPUT_DIR/lambda-functions.json")"
|
||||
echo "VPCs: $(jq 'length' "$OUTPUT_DIR/vpcs.json")"
|
||||
echo "Security Groups: $(jq 'length' "$OUTPUT_DIR/security-groups.json")"
|
||||
echo "IAM Users: $(jq 'length' "$OUTPUT_DIR/iam-users.json")"
|
||||
echo "IAM Roles: $(jq 'length' "$OUTPUT_DIR/iam-roles.json")"
|
||||
|
||||
echo "Inventory saved to $OUTPUT_DIR"
|
||||
```
|
||||
|
||||
## Asset Database Schema
|
||||
## AWS Config Rules for Inventory Compliance
|
||||
|
||||
```bash
|
||||
# Enable AWS Config recorder
|
||||
aws configservice put-configuration-recorder \
|
||||
--configuration-recorder name=default,roleARN=arn:aws:iam::123456789012:role/aws-config-role \
|
||||
--recording-group allSupported=true,includeGlobalResourceTypes=true
|
||||
|
||||
# Start recording
|
||||
aws configservice start-configuration-recorder --configuration-recorder-name default
|
||||
|
||||
# Enable required-tags Config rule
|
||||
aws configservice put-config-rule --config-rule '{
|
||||
"ConfigRuleName": "required-tags",
|
||||
"Source": {
|
||||
"Owner": "AWS",
|
||||
"SourceIdentifier": "REQUIRED_TAGS"
|
||||
},
|
||||
"InputParameters": "{\"tag1Key\":\"Owner\",\"tag2Key\":\"Environment\",\"tag3Key\":\"CostCenter\",\"tag4Key\":\"DataClassification\"}",
|
||||
"Scope": {
|
||||
"ComplianceResourceTypes": [
|
||||
"AWS::EC2::Instance",
|
||||
"AWS::RDS::DBInstance",
|
||||
"AWS::S3::Bucket",
|
||||
"AWS::Lambda::Function"
|
||||
]
|
||||
}
|
||||
}'
|
||||
|
||||
# Config rule for encryption compliance
|
||||
aws configservice put-config-rule --config-rule '{
|
||||
"ConfigRuleName": "encrypted-volumes",
|
||||
"Source": {
|
||||
"Owner": "AWS",
|
||||
"SourceIdentifier": "ENCRYPTED_VOLUMES"
|
||||
}
|
||||
}'
|
||||
|
||||
aws configservice put-config-rule --config-rule '{
|
||||
"ConfigRuleName": "rds-storage-encrypted",
|
||||
"Source": {
|
||||
"Owner": "AWS",
|
||||
"SourceIdentifier": "RDS_STORAGE_ENCRYPTED"
|
||||
}
|
||||
}'
|
||||
|
||||
aws configservice put-config-rule --config-rule '{
|
||||
"ConfigRuleName": "s3-bucket-server-side-encryption-enabled",
|
||||
"Source": {
|
||||
"Owner": "AWS",
|
||||
"SourceIdentifier": "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED"
|
||||
}
|
||||
}'
|
||||
|
||||
# Query AWS Config for all resources of a type
|
||||
aws configservice list-discovered-resources --resource-type AWS::EC2::Instance
|
||||
aws configservice list-discovered-resources --resource-type AWS::RDS::DBInstance
|
||||
|
||||
# Advanced query with AWS Config SQL
|
||||
aws configservice select-resource-config \
|
||||
--expression "SELECT resourceId, resourceType, tags, configuration.instanceType
|
||||
WHERE resourceType = 'AWS::EC2::Instance'
|
||||
AND tags.tag('Environment') = 'production'"
|
||||
|
||||
# Get compliance summary
|
||||
aws configservice get-compliance-summary-by-config-rule
|
||||
aws configservice get-compliance-summary-by-resource-type
|
||||
```
|
||||
|
||||
## Tagging Enforcement
|
||||
|
||||
```yaml
|
||||
asset:
|
||||
id: unique identifier
|
||||
name: display name
|
||||
type: hardware/software/cloud
|
||||
owner: responsible team
|
||||
classification: public/internal/confidential
|
||||
location: physical/cloud location
|
||||
status: active/retired/decommissioned
|
||||
created: timestamp
|
||||
updated: timestamp
|
||||
tags: []
|
||||
# AWS Tag Policy (applied via AWS Organizations)
|
||||
tag_policy:
|
||||
tags:
|
||||
Owner:
|
||||
tag_key:
|
||||
"@@assign": "Owner"
|
||||
enforced_for:
|
||||
"@@assign":
|
||||
- "ec2:instance"
|
||||
- "rds:db"
|
||||
- "s3:bucket"
|
||||
- "lambda:function"
|
||||
|
||||
Environment:
|
||||
tag_key:
|
||||
"@@assign": "Environment"
|
||||
tag_value:
|
||||
"@@assign":
|
||||
- "production"
|
||||
- "staging"
|
||||
- "development"
|
||||
- "sandbox"
|
||||
|
||||
DataClassification:
|
||||
tag_key:
|
||||
"@@assign": "DataClassification"
|
||||
tag_value:
|
||||
"@@assign":
|
||||
- "public"
|
||||
- "internal"
|
||||
- "confidential"
|
||||
- "restricted"
|
||||
|
||||
CostCenter:
|
||||
tag_key:
|
||||
"@@assign": "CostCenter"
|
||||
```
|
||||
|
||||
```hcl
|
||||
# Terraform - Enforce tags on all resources with default_tags
|
||||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
|
||||
default_tags {
|
||||
tags = {
|
||||
ManagedBy = "terraform"
|
||||
Environment = var.environment
|
||||
Owner = var.team_name
|
||||
CostCenter = var.cost_center
|
||||
DataClassification = var.data_classification
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Multi-Cloud Discovery
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# multi-cloud-discovery.sh - Discover assets across AWS, Azure, and GCP
|
||||
|
||||
OUTPUT_DIR="./asset-inventory/multi-cloud/$(date +%Y-%m-%d)"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
echo "=== Multi-Cloud Asset Discovery ==="
|
||||
|
||||
# AWS - using Resource Groups Tagging API
|
||||
echo "--- AWS Resources ---"
|
||||
aws resourcegroupstaggingapi get-resources \
|
||||
--query 'ResourceTagMappingList[*].{ARN:ResourceARN,Tags:Tags}' \
|
||||
--output json > "$OUTPUT_DIR/aws-all-resources.json"
|
||||
echo "AWS resources: $(jq 'length' "$OUTPUT_DIR/aws-all-resources.json")"
|
||||
|
||||
# Azure - using Resource Graph
|
||||
echo "--- Azure Resources ---"
|
||||
az graph query -q "Resources | project name, type, location, resourceGroup, subscriptionId, tags" \
|
||||
--output json > "$OUTPUT_DIR/azure-all-resources.json" 2>/dev/null
|
||||
|
||||
# GCP - using Cloud Asset Inventory
|
||||
echo "--- GCP Resources ---"
|
||||
gcloud asset search-all-resources \
|
||||
--scope="organizations/ORG_ID" \
|
||||
--format=json > "$OUTPUT_DIR/gcp-all-resources.json" 2>/dev/null
|
||||
|
||||
# Find untagged resources
|
||||
echo "=== Untagged Resources ==="
|
||||
jq '[.[] | select(.Tags == null or .Tags == [])] | length' "$OUTPUT_DIR/aws-all-resources.json"
|
||||
|
||||
echo "Discovery complete. Results in $OUTPUT_DIR"
|
||||
```
|
||||
|
||||
## CMDB Integration
|
||||
|
||||
```python
|
||||
"""
|
||||
CMDB sync script - Normalize cloud assets and push to CMDB API.
|
||||
"""
|
||||
import json
|
||||
import requests
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
class CMDBSync:
|
||||
def __init__(self, cmdb_url, api_token):
|
||||
self.cmdb_url = cmdb_url
|
||||
self.headers = {
|
||||
"Authorization": f"Bearer {api_token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
def normalize_aws_instance(self, instance):
|
||||
"""Convert AWS EC2 instance to common asset schema."""
|
||||
tags = {t["Key"]: t["Value"] for t in (instance.get("Tags") or [])}
|
||||
return {
|
||||
"asset_id": f"aws:{instance['InstanceId']}",
|
||||
"name": tags.get("Name", instance["InstanceId"]),
|
||||
"type": "compute",
|
||||
"provider": "aws",
|
||||
"region": instance.get("AZ", "unknown")[:-1],
|
||||
"configuration": {
|
||||
"instance_type": instance.get("Type"),
|
||||
"state": instance.get("State"),
|
||||
"vpc_id": instance.get("VpcId"),
|
||||
},
|
||||
"owner": tags.get("Owner", "unassigned"),
|
||||
"environment": tags.get("Environment", "unknown"),
|
||||
"data_classification": tags.get("DataClassification", "unknown"),
|
||||
"status": "active" if instance.get("State") == "running" else "stopped",
|
||||
"last_seen": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
def sync_assets(self, assets):
|
||||
"""Push normalized assets to CMDB."""
|
||||
results = {"created": 0, "updated": 0, "errors": 0}
|
||||
for asset in assets:
|
||||
try:
|
||||
resp = requests.get(
|
||||
f"{self.cmdb_url}/assets/{asset['asset_id']}",
|
||||
headers=self.headers,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
requests.put(
|
||||
f"{self.cmdb_url}/assets/{asset['asset_id']}",
|
||||
headers=self.headers,
|
||||
json=asset,
|
||||
)
|
||||
results["updated"] += 1
|
||||
else:
|
||||
requests.post(
|
||||
f"{self.cmdb_url}/assets",
|
||||
headers=self.headers,
|
||||
json=asset,
|
||||
)
|
||||
results["created"] += 1
|
||||
except Exception:
|
||||
results["errors"] += 1
|
||||
return results
|
||||
```
|
||||
|
||||
## Asset Inventory Checklist
|
||||
|
||||
```yaml
|
||||
asset_inventory_checklist:
|
||||
discovery:
|
||||
- [ ] Automated discovery scripts running for all cloud accounts
|
||||
- [ ] Discovery covers all resource types (compute, storage, network, IAM)
|
||||
- [ ] Multi-region discovery enabled
|
||||
- [ ] On-premise assets cataloged
|
||||
- [ ] SaaS subscriptions inventoried
|
||||
- [ ] Discovery runs daily (minimum weekly)
|
||||
|
||||
classification:
|
||||
- [ ] Required tags defined (Owner, Environment, DataClassification, CostCenter)
|
||||
- [ ] Tag enforcement via AWS Organizations tag policies
|
||||
- [ ] Tag enforcement via Terraform default_tags
|
||||
- [ ] Tag enforcement via CI/CD policy checks (Checkov, OPA)
|
||||
- [ ] Untagged resource reports generated and tracked
|
||||
|
||||
configuration_management:
|
||||
- [ ] AWS Config enabled in all regions
|
||||
- [ ] Config rules enforce encryption, tagging, and security baselines
|
||||
- [ ] Configuration compliance summary reviewed weekly
|
||||
- [ ] Drift detection enabled for IaC-managed resources
|
||||
|
||||
cmdb:
|
||||
- [ ] CMDB sync automated from cloud discovery
|
||||
- [ ] Common schema defined across all providers
|
||||
- [ ] Reconciliation process identifies orphaned records
|
||||
- [ ] New resources auto-assigned default owner
|
||||
- [ ] Asset lifecycle tracked (created, active, decommissioning, retired)
|
||||
|
||||
governance:
|
||||
- [ ] Asset owners assigned and current
|
||||
- [ ] Quarterly inventory reconciliation conducted
|
||||
- [ ] Compliance scope tagging accurate (SOC2, HIPAA, PCI)
|
||||
- [ ] Asset inventory available for auditor review
|
||||
- [ ] Decommissioned assets tracked for data retention compliance
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Automated discovery
|
||||
- Regular reconciliation
|
||||
- Owner assignment
|
||||
- Classification tagging
|
||||
- Lifecycle tracking
|
||||
- Automate discovery rather than relying on manual inventory: cloud environments change too fast for spreadsheets
|
||||
- Use AWS Config, Azure Resource Graph, and GCP Cloud Asset Inventory as authoritative data sources
|
||||
- Enforce tagging at provisioning time through IaC defaults and policy-as-code guardrails
|
||||
- Assign every asset an owner: unowned resources become security and cost liabilities
|
||||
- Reconcile inventory regularly and investigate orphaned assets (CMDB record with no real resource and vice versa)
|
||||
- Track data classification as a mandatory tag to support compliance scoping decisions
|
||||
- Maintain asset lifecycle states to distinguish active resources from those being decommissioned
|
||||
- Integrate asset inventory with incident response to quickly identify affected systems during investigations
|
||||
- Export inventory data for compliance audits in accessible formats (CSV, JSON)
|
||||
- Review untagged and unclassified resource reports weekly to maintain inventory quality
|
||||
|
||||
@@ -9,73 +9,497 @@ metadata:
|
||||
|
||||
# Change Management
|
||||
|
||||
Implement structured change management processes.
|
||||
Implement structured change management processes covering change classification, CAB workflows, emergency change procedures, and automation for compliance with SOC 2, ITIL, and regulatory frameworks.
|
||||
|
||||
## Change Process
|
||||
## When to Use
|
||||
|
||||
```yaml
|
||||
change_workflow:
|
||||
1_request:
|
||||
- Change description
|
||||
- Risk assessment
|
||||
- Rollback plan
|
||||
- Testing evidence
|
||||
|
||||
2_review:
|
||||
- Technical review
|
||||
- Security review
|
||||
- CAB approval (if high risk)
|
||||
|
||||
3_schedule:
|
||||
- Change window
|
||||
- Communication
|
||||
- Resource allocation
|
||||
|
||||
4_implement:
|
||||
- Execute change
|
||||
- Verify success
|
||||
- Update documentation
|
||||
|
||||
5_review:
|
||||
- Post-implementation review
|
||||
- Lessons learned
|
||||
```
|
||||
- Establishing change management processes for production environments
|
||||
- Implementing change advisory board (CAB) workflows
|
||||
- Defining change classification and approval requirements
|
||||
- Configuring automated change tracking in CI/CD pipelines
|
||||
- Handling emergency changes with proper controls and documentation
|
||||
|
||||
## Change Classification
|
||||
|
||||
| Type | Risk | Approval | Example |
|
||||
|------|------|----------|---------|
|
||||
| Standard | Low | Pre-approved | Patching |
|
||||
| Normal | Medium | Manager | Config change |
|
||||
| Emergency | Variable | Expedited | Security fix |
|
||||
```yaml
|
||||
change_types:
|
||||
standard:
|
||||
risk: Low
|
||||
approval: Pre-approved (no per-change approval needed)
|
||||
lead_time: None (within maintenance window)
|
||||
examples:
|
||||
- Routine patching within tested patch sets
|
||||
- Certificate rotation with established procedure
|
||||
- Scaling operations (adding/removing instances within limits)
|
||||
- Pre-approved configuration changes
|
||||
- Log rotation and archival
|
||||
requirements:
|
||||
- Change must match an approved Standard Change template
|
||||
- Automated testing must pass
|
||||
- Documented rollback procedure exists
|
||||
- Within defined maintenance window
|
||||
|
||||
## Pull Request Template
|
||||
normal_low:
|
||||
risk: Low
|
||||
approval: Peer review (1 approver)
|
||||
lead_time: 2 business days
|
||||
examples:
|
||||
- Non-critical configuration changes
|
||||
- Feature flag toggles
|
||||
- Documentation updates to production systems
|
||||
- Adding monitoring dashboards or alerts
|
||||
|
||||
normal_medium:
|
||||
risk: Medium
|
||||
approval: Team lead + peer review (2 approvers)
|
||||
lead_time: 5 business days
|
||||
examples:
|
||||
- Application deployments with new features
|
||||
- Database schema changes (non-breaking)
|
||||
- Network rule modifications
|
||||
- Integration endpoint changes
|
||||
- Dependency version upgrades
|
||||
|
||||
normal_high:
|
||||
risk: High
|
||||
approval: CAB review required
|
||||
lead_time: 10 business days
|
||||
examples:
|
||||
- Infrastructure migrations
|
||||
- Breaking database schema changes
|
||||
- Major version upgrades (OS, runtime, database engine)
|
||||
- Changes to authentication or authorization systems
|
||||
- Multi-service coordinated deployments
|
||||
- Changes affecting data processing or compliance controls
|
||||
|
||||
emergency:
|
||||
risk: Variable
|
||||
approval: Emergency CAB (minimum 2 approvers from on-call)
|
||||
lead_time: None (immediate implementation)
|
||||
examples:
|
||||
- Security vulnerability remediation (active exploitation)
|
||||
- Production outage resolution
|
||||
- Data integrity emergency fixes
|
||||
- Regulatory compliance deadline fixes
|
||||
requirements:
|
||||
- Retroactive full documentation within 48 hours
|
||||
- Post-implementation review required
|
||||
- CAB retroactive review at next meeting
|
||||
```
|
||||
|
||||
## Change Request Template
|
||||
|
||||
```yaml
|
||||
change_request:
|
||||
metadata:
|
||||
id: "CR-YYYY-NNNN"
|
||||
title: ""
|
||||
requestor: ""
|
||||
date_submitted: ""
|
||||
target_date: ""
|
||||
change_type: "" # standard | normal_low | normal_medium | normal_high | emergency
|
||||
|
||||
description:
|
||||
summary: "Brief description of the change"
|
||||
detailed_description: "Full technical details of what will change"
|
||||
business_justification: "Why this change is needed"
|
||||
affected_systems: []
|
||||
affected_services: []
|
||||
affected_users: "Description of user impact"
|
||||
|
||||
risk_assessment:
|
||||
risk_level: "" # low | medium | high
|
||||
impact_if_failed: "What happens if the change fails"
|
||||
likelihood_of_failure: "" # low | medium | high
|
||||
risk_mitigation: "Steps to reduce risk"
|
||||
dependencies: "Other systems or changes this depends on"
|
||||
|
||||
implementation:
|
||||
change_window:
|
||||
start: ""
|
||||
end: ""
|
||||
maintenance_window: true
|
||||
implementation_steps:
|
||||
- step: "Step 1 description"
|
||||
responsible: "Person/team"
|
||||
estimated_duration: "X minutes"
|
||||
- step: "Step 2 description"
|
||||
responsible: "Person/team"
|
||||
estimated_duration: "X minutes"
|
||||
|
||||
testing:
|
||||
pre_change_testing:
|
||||
- "Unit tests pass"
|
||||
- "Integration tests pass"
|
||||
- "Staging deployment verified"
|
||||
post_change_verification:
|
||||
- "Health check endpoints responding"
|
||||
- "Key transactions processing successfully"
|
||||
- "No error rate increase in monitoring"
|
||||
- "Performance metrics within baseline"
|
||||
|
||||
rollback:
|
||||
rollback_plan: "Detailed steps to revert the change"
|
||||
rollback_trigger: "Conditions that trigger rollback"
|
||||
rollback_estimated_time: "X minutes"
|
||||
rollback_steps:
|
||||
- "Step 1: Revert deployment to previous version"
|
||||
- "Step 2: Verify rollback successful"
|
||||
- "Step 3: Notify stakeholders"
|
||||
data_rollback: "Describe any data migration rollback needed"
|
||||
|
||||
communication:
|
||||
stakeholders_notified: []
|
||||
notification_sent_date: ""
|
||||
status_page_update: true
|
||||
customer_notification_required: false
|
||||
|
||||
approvals:
|
||||
technical_reviewer: ""
|
||||
technical_approval_date: ""
|
||||
security_reviewer: ""
|
||||
security_approval_date: ""
|
||||
cab_approval_date: ""
|
||||
cab_notes: ""
|
||||
|
||||
closure:
|
||||
implementation_date: ""
|
||||
implementation_result: "" # success | partial | failed | rolled_back
|
||||
post_implementation_review: ""
|
||||
lessons_learned: ""
|
||||
follow_up_actions: []
|
||||
```
|
||||
|
||||
## CAB Workflow
|
||||
|
||||
```yaml
|
||||
cab_workflow:
|
||||
meeting_schedule:
|
||||
regular_cab: "Weekly, Thursday 2:00 PM"
|
||||
emergency_cab: "On-demand, minimum 2 members required"
|
||||
|
||||
cab_members:
|
||||
permanent:
|
||||
- Engineering Manager (Chair)
|
||||
- Security Team Representative
|
||||
- Infrastructure/SRE Lead
|
||||
- Release Manager
|
||||
advisory:
|
||||
- Business stakeholder (invited per change)
|
||||
- Database administrator (for DB changes)
|
||||
- Network engineer (for network changes)
|
||||
|
||||
agenda:
|
||||
1: "Review emergency changes from prior week"
|
||||
2: "Review high-risk change requests for upcoming window"
|
||||
3: "Review failed changes and lessons learned"
|
||||
4: "Discuss upcoming change freeze periods"
|
||||
5: "Review change metrics and trends"
|
||||
|
||||
decision_criteria:
|
||||
approve_when:
|
||||
- Risk assessment is complete and accurate
|
||||
- Testing evidence is provided
|
||||
- Rollback plan is documented and feasible
|
||||
- Change window is appropriate
|
||||
- Required approvals obtained
|
||||
- No conflicts with other scheduled changes
|
||||
request_changes_when:
|
||||
- Rollback plan is missing or incomplete
|
||||
- Testing is insufficient for the risk level
|
||||
- Impact assessment needs clarification
|
||||
- Change conflicts with another scheduled change
|
||||
deny_when:
|
||||
- Risk is unacceptable without mitigation
|
||||
- Change window conflicts with freeze period
|
||||
- Dependencies are not resolved
|
||||
- Compliance concerns are unaddressed
|
||||
```
|
||||
|
||||
## Emergency Change Procedure
|
||||
|
||||
```yaml
|
||||
emergency_change_process:
|
||||
definition: "A change required to restore service or prevent imminent security compromise"
|
||||
|
||||
step_1_declare:
|
||||
actions:
|
||||
- On-call engineer identifies need for emergency change
|
||||
- Incident commander approves emergency classification
|
||||
- Minimum 2 approvers from emergency CAB roster contacted
|
||||
- Document initial justification in incident channel
|
||||
|
||||
step_2_approve:
|
||||
approval_method:
|
||||
- Slack/Teams approval with screenshots preserved
|
||||
- Verbal approval over bridge call (documented in notes)
|
||||
- Emergency approvers can be any 2 of the following roles:
|
||||
- Engineering Manager
|
||||
- SRE/Infrastructure Lead
|
||||
- Security Team Lead
|
||||
- VP of Engineering
|
||||
timeout: "If no response in 15 minutes, escalate to next tier"
|
||||
|
||||
step_3_implement:
|
||||
actions:
|
||||
- Implement the minimum change needed to resolve the issue
|
||||
- Record all actions taken with timestamps
|
||||
- Monitor for successful resolution
|
||||
- Document any deviations from planned change
|
||||
|
||||
step_4_verify:
|
||||
actions:
|
||||
- Confirm service restoration
|
||||
- Verify no unintended side effects
|
||||
- Run post-change verification checks
|
||||
- Update status page and stakeholders
|
||||
|
||||
step_5_document:
|
||||
deadline: "Within 48 hours of implementation"
|
||||
required_documentation:
|
||||
- Complete change request form (retroactive)
|
||||
- Timeline of events and actions
|
||||
- Justification for emergency classification
|
||||
- Approval records (messages, emails)
|
||||
- Post-implementation verification results
|
||||
- Root cause analysis (what made it an emergency)
|
||||
- Preventive actions to avoid future emergency
|
||||
|
||||
step_6_review:
|
||||
actions:
|
||||
- CAB review at next regular meeting
|
||||
- Assess if emergency classification was appropriate
|
||||
- Identify process improvements
|
||||
- Track emergency change trends
|
||||
```
|
||||
|
||||
## Pull Request Template for Changes
|
||||
|
||||
```markdown
|
||||
## Change Description
|
||||
## Change Request
|
||||
|
||||
## Risk Level
|
||||
- [ ] Low - Standard change
|
||||
- [ ] Medium - Normal change
|
||||
- [ ] High - CAB required
|
||||
### Type
|
||||
- [ ] Standard (pre-approved, low risk)
|
||||
- [ ] Normal - Low Risk
|
||||
- [ ] Normal - Medium Risk
|
||||
- [ ] Normal - High Risk (CAB required)
|
||||
- [ ] Emergency (retroactive documentation required)
|
||||
|
||||
## Testing
|
||||
### Description
|
||||
<!-- What is being changed and why? -->
|
||||
|
||||
### Risk Assessment
|
||||
**Impact if failed:** <!-- What breaks? -->
|
||||
**Likelihood of failure:** Low / Medium / High
|
||||
**Affected services:** <!-- List services -->
|
||||
**User impact:** <!-- Will users notice? -->
|
||||
|
||||
### Testing Evidence
|
||||
- [ ] Unit tests pass
|
||||
- [ ] Integration tests pass
|
||||
- [ ] Staging deployment verified
|
||||
- [ ] Performance test completed (if applicable)
|
||||
- [ ] Security scan clean (if applicable)
|
||||
|
||||
## Rollback Plan
|
||||
### Rollback Plan
|
||||
<!-- How to revert if something goes wrong -->
|
||||
**Estimated rollback time:** <!-- X minutes -->
|
||||
**Data rollback needed:** Yes / No
|
||||
|
||||
## Stakeholders Notified
|
||||
- [ ] Operations
|
||||
- [ ] Security
|
||||
- [ ] Business owners
|
||||
### Deployment Plan
|
||||
**Target window:** <!-- Date and time -->
|
||||
**Estimated duration:** <!-- X minutes -->
|
||||
|
||||
### Post-Deployment Verification
|
||||
- [ ] Health checks passing
|
||||
- [ ] Error rates within baseline
|
||||
- [ ] Key transactions working
|
||||
- [ ] Monitoring dashboards reviewed
|
||||
|
||||
### Communication
|
||||
- [ ] Team notified
|
||||
- [ ] Stakeholders notified (if user-facing)
|
||||
- [ ] Status page updated (if applicable)
|
||||
|
||||
### Approvals Required
|
||||
- [ ] Peer review
|
||||
- [ ] Team lead (medium+ risk)
|
||||
- [ ] Security review (security-impacting changes)
|
||||
- [ ] CAB approval (high risk)
|
||||
```
|
||||
|
||||
## CI/CD Change Tracking Automation
|
||||
|
||||
```yaml
|
||||
# GitHub Actions - Automated change tracking
|
||||
name: Change Management
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, labeled]
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
classify-change:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Classify change risk
|
||||
id: classify
|
||||
run: |
|
||||
FILES_CHANGED=$(gh pr diff ${{ github.event.pull_request.number }} --name-only)
|
||||
|
||||
# High risk indicators
|
||||
if echo "$FILES_CHANGED" | grep -qE 'terraform/|infrastructure/|migrations/|auth/|security/'; then
|
||||
echo "risk=high" >> $GITHUB_OUTPUT
|
||||
echo "::warning::High-risk change detected - CAB review may be required"
|
||||
elif echo "$FILES_CHANGED" | grep -qE 'config/|database/|api/'; then
|
||||
echo "risk=medium" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "risk=low" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Add risk label
|
||||
run: |
|
||||
gh pr edit ${{ github.event.pull_request.number }} \
|
||||
--add-label "risk:${{ steps.classify.outputs.risk }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enforce approvals by risk
|
||||
if: steps.classify.outputs.risk == 'high'
|
||||
run: |
|
||||
APPROVALS=$(gh pr view ${{ github.event.pull_request.number }} \
|
||||
--json reviews --jq '[.reviews[] | select(.state=="APPROVED")] | length')
|
||||
if [ "$APPROVALS" -lt 2 ]; then
|
||||
echo "::error::High-risk changes require at least 2 approvals"
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
record-deployment:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Record deployment
|
||||
run: |
|
||||
CHANGE_ID="CR-$(date +%Y)-$(printf '%04d' ${{ github.run_number }})"
|
||||
echo "Change ID: $CHANGE_ID"
|
||||
echo "Deployed at: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo "Commit: ${{ github.sha }}"
|
||||
echo "Author: ${{ github.actor }}"
|
||||
|
||||
cat > /tmp/deployment-record.json <<EOF
|
||||
{
|
||||
"change_id": "$CHANGE_ID",
|
||||
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"commit": "${{ github.sha }}",
|
||||
"author": "${{ github.actor }}",
|
||||
"environment": "production",
|
||||
"status": "deployed"
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
## Change Freeze Policy
|
||||
|
||||
```yaml
|
||||
change_freeze:
|
||||
definition: "Period during which non-emergency changes are prohibited"
|
||||
|
||||
scheduled_freezes:
|
||||
year_end:
|
||||
start: "December 15"
|
||||
end: "January 3"
|
||||
scope: "All production changes"
|
||||
major_events:
|
||||
- "Black Friday through Cyber Monday (e-commerce)"
|
||||
- "Tax filing deadline periods (financial services)"
|
||||
- "Open enrollment periods (healthcare)"
|
||||
|
||||
exceptions_during_freeze:
|
||||
allowed:
|
||||
- Security patches for actively exploited vulnerabilities
|
||||
- Changes required by regulatory deadline
|
||||
- Fixes for P1/SEV1 production incidents
|
||||
approval: "VP of Engineering + Security Lead"
|
||||
|
||||
communication:
|
||||
announcement: "2 weeks before freeze"
|
||||
reminder: "1 week and 1 day before freeze"
|
||||
daily_status: "During freeze period"
|
||||
lift_notification: "When freeze ends"
|
||||
```
|
||||
|
||||
## Change Management Metrics
|
||||
|
||||
```yaml
|
||||
metrics:
|
||||
change_success_rate:
|
||||
description: "Percentage of changes implemented without rollback or incident"
|
||||
target: ">95%"
|
||||
formula: "(successful changes / total changes) * 100"
|
||||
|
||||
emergency_change_rate:
|
||||
description: "Percentage of changes classified as emergency"
|
||||
target: "<5%"
|
||||
formula: "(emergency changes / total changes) * 100"
|
||||
|
||||
rollback_rate:
|
||||
description: "Percentage of changes that required rollback"
|
||||
target: "<3%"
|
||||
|
||||
mean_time_to_implement:
|
||||
description: "Average time from approval to implementation"
|
||||
target: "Varies by type"
|
||||
|
||||
cab_approval_time:
|
||||
description: "Average time from submission to CAB decision"
|
||||
target: "<5 business days for normal changes"
|
||||
```
|
||||
|
||||
## Change Management Checklist
|
||||
|
||||
```yaml
|
||||
change_management_checklist:
|
||||
process_setup:
|
||||
- [ ] Change types defined with classification criteria
|
||||
- [ ] Approval matrix documented (who approves what)
|
||||
- [ ] CAB established with regular meeting schedule
|
||||
- [ ] Emergency change procedure documented
|
||||
- [ ] Change request template created
|
||||
- [ ] Change freeze policy defined
|
||||
|
||||
tooling:
|
||||
- [ ] PR template includes change management fields
|
||||
- [ ] Automated risk classification in CI/CD
|
||||
- [ ] Branch protection enforces required approvals
|
||||
- [ ] Deployment records captured automatically
|
||||
- [ ] Change audit trail preserved (PR history, approvals)
|
||||
|
||||
compliance:
|
||||
- [ ] All production changes have documented approval
|
||||
- [ ] Rollback plans exist for every change
|
||||
- [ ] Post-implementation reviews conducted for failures
|
||||
- [ ] Emergency changes documented retroactively within 48 hours
|
||||
- [ ] Change metrics reported monthly
|
||||
- [ ] Audit trail retained for compliance period (1-3 years)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Clear change categories
|
||||
- Required approvals by risk
|
||||
- Rollback procedures documented
|
||||
- Post-change verification
|
||||
- Change freeze windows
|
||||
- Classify changes by risk level to apply proportionate controls without slowing low-risk work
|
||||
- Automate risk classification based on files changed, services affected, and deployment scope
|
||||
- Use PR approvals as the native change approval mechanism for code-driven changes
|
||||
- Require rollback plans for every change and test rollback procedures periodically
|
||||
- Track emergency changes as a key metric: a high rate indicates systemic process issues
|
||||
- Implement change freezes during critical business periods to protect stability
|
||||
- Conduct post-implementation reviews for all failed changes to drive improvement
|
||||
- Separate duty of implementation from duty of approval (no self-approving changes)
|
||||
- Capture deployment records automatically in CI/CD rather than relying on manual entry
|
||||
- Keep the CAB focused on high-risk decisions; do not bottleneck low-risk changes through CAB
|
||||
|
||||
@@ -9,61 +9,592 @@ metadata:
|
||||
|
||||
# Policy as Code
|
||||
|
||||
Automate policy enforcement through code.
|
||||
Automate policy enforcement through code using OPA/Rego, Kyverno, Checkov, and CI/CD integration to prevent compliance violations before they reach production.
|
||||
|
||||
## Open Policy Agent (OPA)
|
||||
## When to Use
|
||||
|
||||
- Enforcing security and compliance policies on infrastructure-as-code changes
|
||||
- Preventing misconfigured Kubernetes workloads from deploying
|
||||
- Automating guardrails in CI/CD pipelines for Terraform, CloudFormation, or Helm
|
||||
- Implementing organizational standards that must be consistently applied
|
||||
- Replacing manual approval gates with automated policy checks
|
||||
|
||||
## Open Policy Agent (OPA) Rego Policies
|
||||
|
||||
```rego
|
||||
# deny_public_buckets.rego
|
||||
package terraform.s3
|
||||
# deny_public_s3.rego - Deny S3 buckets with public access
|
||||
package terraform.aws.s3
|
||||
|
||||
deny[msg] {
|
||||
resource := input.resource.aws_s3_bucket[name]
|
||||
resource.acl == "public-read"
|
||||
msg := sprintf("S3 bucket '%s' has public ACL", [name])
|
||||
import rego.v1
|
||||
|
||||
deny contains msg if {
|
||||
resource := input.resource_changes[_]
|
||||
resource.type == "aws_s3_bucket"
|
||||
resource.change.after.acl == "public-read"
|
||||
msg := sprintf(
|
||||
"S3 bucket '%s' has public-read ACL. All buckets must be private. [Policy: no-public-s3]",
|
||||
[resource.address]
|
||||
)
|
||||
}
|
||||
|
||||
deny contains msg if {
|
||||
resource := input.resource_changes[_]
|
||||
resource.type == "aws_s3_bucket"
|
||||
resource.change.after.acl == "public-read-write"
|
||||
msg := sprintf(
|
||||
"S3 bucket '%s' has public-read-write ACL. This is strictly prohibited. [Policy: no-public-s3]",
|
||||
[resource.address]
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Kyverno (Kubernetes)
|
||||
```rego
|
||||
# require_encryption.rego - Require encryption on data stores
|
||||
package terraform.aws.encryption
|
||||
|
||||
import rego.v1
|
||||
|
||||
deny contains msg if {
|
||||
resource := input.resource_changes[_]
|
||||
resource.type == "aws_db_instance"
|
||||
not resource.change.after.storage_encrypted
|
||||
msg := sprintf(
|
||||
"RDS instance '%s' does not have storage encryption enabled. [Policy: require-rds-encryption]",
|
||||
[resource.address]
|
||||
)
|
||||
}
|
||||
|
||||
deny contains msg if {
|
||||
resource := input.resource_changes[_]
|
||||
resource.type == "aws_ebs_volume"
|
||||
not resource.change.after.encrypted
|
||||
msg := sprintf(
|
||||
"EBS volume '%s' is not encrypted. [Policy: require-ebs-encryption]",
|
||||
[resource.address]
|
||||
)
|
||||
}
|
||||
|
||||
deny contains msg if {
|
||||
resource := input.resource_changes[_]
|
||||
resource.type == "aws_s3_bucket"
|
||||
not has_encryption(resource)
|
||||
msg := sprintf(
|
||||
"S3 bucket '%s' does not have default encryption configured. [Policy: require-s3-encryption]",
|
||||
[resource.address]
|
||||
)
|
||||
}
|
||||
|
||||
has_encryption(resource) if {
|
||||
resource.change.after.server_side_encryption_configuration[_]
|
||||
}
|
||||
```
|
||||
|
||||
```rego
|
||||
# require_tags.rego - Enforce mandatory tagging
|
||||
package terraform.aws.tags
|
||||
|
||||
import rego.v1
|
||||
|
||||
required_tags := {"Environment", "Owner", "CostCenter", "DataClassification"}
|
||||
|
||||
deny contains msg if {
|
||||
resource := input.resource_changes[_]
|
||||
tags := object.get(resource.change.after, "tags", {})
|
||||
missing := required_tags - {key | tags[key]}
|
||||
count(missing) > 0
|
||||
msg := sprintf(
|
||||
"Resource '%s' is missing required tags: %v. [Policy: required-tags]",
|
||||
[resource.address, missing]
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```rego
|
||||
# restrict_regions.rego - Limit resource deployment to approved regions
|
||||
package terraform.aws.regions
|
||||
|
||||
import rego.v1
|
||||
|
||||
approved_regions := {"us-east-1", "us-west-2", "eu-west-1"}
|
||||
|
||||
deny contains msg if {
|
||||
resource := input.resource_changes[_]
|
||||
provider_config := input.configuration.provider_config.aws
|
||||
region := provider_config.expressions.region.constant_value
|
||||
not region in approved_regions
|
||||
msg := sprintf(
|
||||
"Resource '%s' is in region '%s'. Approved regions: %v. [Policy: approved-regions]",
|
||||
[resource.address, region, approved_regions]
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
# Evaluate OPA policies against Terraform plan
|
||||
terraform plan -out=tfplan
|
||||
terraform show -json tfplan > tfplan.json
|
||||
|
||||
# Run OPA evaluation
|
||||
opa eval \
|
||||
--data policies/ \
|
||||
--input tfplan.json \
|
||||
"data.terraform.aws.s3.deny" \
|
||||
--format pretty
|
||||
|
||||
# Use conftest for easier CI integration
|
||||
conftest test tfplan.json --policy policies/ --output table
|
||||
```
|
||||
|
||||
## Kyverno Kubernetes Policies
|
||||
|
||||
```yaml
|
||||
# require-labels.yaml - Enforce required labels on all pods
|
||||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: require-labels
|
||||
annotations:
|
||||
policies.kyverno.io/title: Require Labels
|
||||
policies.kyverno.io/category: Best Practices
|
||||
policies.kyverno.io/severity: medium
|
||||
spec:
|
||||
validationFailureAction: enforce
|
||||
validationFailureAction: Enforce
|
||||
background: true
|
||||
rules:
|
||||
- name: check-labels
|
||||
- name: check-required-labels
|
||||
match:
|
||||
resources:
|
||||
kinds:
|
||||
- Pod
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Pod
|
||||
validate:
|
||||
message: "Label 'app' is required"
|
||||
message: >-
|
||||
Labels 'app.kubernetes.io/name', 'app.kubernetes.io/version',
|
||||
and 'team' are required on all Pods.
|
||||
pattern:
|
||||
metadata:
|
||||
labels:
|
||||
app: "?*"
|
||||
app.kubernetes.io/name: "?*"
|
||||
app.kubernetes.io/version: "?*"
|
||||
team: "?*"
|
||||
---
|
||||
# disallow-privileged.yaml - Block privileged containers
|
||||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: disallow-privileged-containers
|
||||
annotations:
|
||||
policies.kyverno.io/title: Disallow Privileged Containers
|
||||
policies.kyverno.io/category: Pod Security
|
||||
policies.kyverno.io/severity: high
|
||||
spec:
|
||||
validationFailureAction: Enforce
|
||||
background: true
|
||||
rules:
|
||||
- name: deny-privileged
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Pod
|
||||
validate:
|
||||
message: "Privileged containers are not allowed."
|
||||
pattern:
|
||||
spec:
|
||||
containers:
|
||||
- securityContext:
|
||||
privileged: "false"
|
||||
=(initContainers):
|
||||
- securityContext:
|
||||
privileged: "false"
|
||||
---
|
||||
# require-resource-limits.yaml - Enforce resource limits
|
||||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: require-resource-limits
|
||||
annotations:
|
||||
policies.kyverno.io/title: Require Resource Limits
|
||||
policies.kyverno.io/severity: medium
|
||||
spec:
|
||||
validationFailureAction: Enforce
|
||||
background: true
|
||||
rules:
|
||||
- name: check-resource-limits
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Pod
|
||||
validate:
|
||||
message: "All containers must have CPU and memory limits defined."
|
||||
pattern:
|
||||
spec:
|
||||
containers:
|
||||
- resources:
|
||||
limits:
|
||||
memory: "?*"
|
||||
cpu: "?*"
|
||||
---
|
||||
# disallow-latest-tag.yaml - Block usage of 'latest' image tag
|
||||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: disallow-latest-tag
|
||||
annotations:
|
||||
policies.kyverno.io/title: Disallow Latest Tag
|
||||
policies.kyverno.io/severity: medium
|
||||
spec:
|
||||
validationFailureAction: Enforce
|
||||
background: true
|
||||
rules:
|
||||
- name: validate-image-tag
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Pod
|
||||
validate:
|
||||
message: "Images must use a specific tag, not 'latest'."
|
||||
pattern:
|
||||
spec:
|
||||
containers:
|
||||
- image: "!*:latest & *:*"
|
||||
---
|
||||
# restrict-image-registries.yaml - Allow only approved registries
|
||||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: restrict-image-registries
|
||||
annotations:
|
||||
policies.kyverno.io/title: Restrict Image Registries
|
||||
policies.kyverno.io/severity: high
|
||||
spec:
|
||||
validationFailureAction: Enforce
|
||||
background: true
|
||||
rules:
|
||||
- name: validate-registries
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Pod
|
||||
validate:
|
||||
message: >-
|
||||
Images must come from approved registries:
|
||||
123456789012.dkr.ecr.us-east-1.amazonaws.com or ghcr.io/your-org.
|
||||
pattern:
|
||||
spec:
|
||||
containers:
|
||||
- image: "123456789012.dkr.ecr.*.amazonaws.com/* | ghcr.io/your-org/*"
|
||||
---
|
||||
# require-networkpolicy.yaml - Ensure namespaces have NetworkPolicies
|
||||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: require-networkpolicy
|
||||
annotations:
|
||||
policies.kyverno.io/title: Require Network Policy
|
||||
policies.kyverno.io/severity: high
|
||||
spec:
|
||||
validationFailureAction: Audit
|
||||
background: true
|
||||
rules:
|
||||
- name: check-networkpolicy
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Deployment
|
||||
preconditions:
|
||||
all:
|
||||
- key: "{{request.object.metadata.namespace}}"
|
||||
operator: NotIn
|
||||
value: ["kube-system", "kube-public"]
|
||||
validate:
|
||||
message: "A NetworkPolicy must exist in namespace '{{request.object.metadata.namespace}}' before deploying workloads."
|
||||
deny:
|
||||
conditions:
|
||||
all:
|
||||
- key: "{{request.object.metadata.namespace}}"
|
||||
operator: AnyNotIn
|
||||
value: "{{request.object.metadata.namespace}}"
|
||||
```
|
||||
|
||||
## Checkov
|
||||
## Checkov Custom Checks
|
||||
|
||||
```python
|
||||
# custom_checks/require_s3_versioning.py
|
||||
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
|
||||
from checkov.common.models.enums import CheckResult, CheckCategories
|
||||
|
||||
|
||||
class S3Versioning(BaseResourceCheck):
|
||||
def __init__(self):
|
||||
name = "Ensure S3 bucket has versioning enabled"
|
||||
id = "CUSTOM_S3_001"
|
||||
supported_resources = ["aws_s3_bucket_versioning"]
|
||||
categories = [CheckCategories.BACKUP_AND_RECOVERY]
|
||||
super().__init__(name=name, id=id,
|
||||
categories=categories,
|
||||
supported_resources=supported_resources)
|
||||
|
||||
def scan_resource_conf(self, conf):
|
||||
versioning = conf.get("versioning_configuration", [{}])
|
||||
if isinstance(versioning, list):
|
||||
versioning = versioning[0] if versioning else {}
|
||||
status = versioning.get("status", ["Disabled"])
|
||||
if isinstance(status, list):
|
||||
status = status[0]
|
||||
return CheckResult.PASSED if status == "Enabled" else CheckResult.FAILED
|
||||
|
||||
|
||||
check = S3Versioning()
|
||||
```
|
||||
|
||||
```python
|
||||
# custom_checks/require_rds_backup.py
|
||||
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
|
||||
from checkov.common.models.enums import CheckResult, CheckCategories
|
||||
|
||||
|
||||
class RDSBackupRetention(BaseResourceCheck):
|
||||
def __init__(self):
|
||||
name = "Ensure RDS has backup retention of at least 7 days"
|
||||
id = "CUSTOM_RDS_001"
|
||||
supported_resources = ["aws_db_instance"]
|
||||
categories = [CheckCategories.BACKUP_AND_RECOVERY]
|
||||
super().__init__(name=name, id=id,
|
||||
categories=categories,
|
||||
supported_resources=supported_resources)
|
||||
|
||||
def scan_resource_conf(self, conf):
|
||||
retention = conf.get("backup_retention_period", [0])
|
||||
if isinstance(retention, list):
|
||||
retention = retention[0]
|
||||
return CheckResult.PASSED if int(retention) >= 7 else CheckResult.FAILED
|
||||
|
||||
|
||||
check = RDSBackupRetention()
|
||||
```
|
||||
|
||||
```bash
|
||||
# Scan Terraform
|
||||
checkov -d . --framework terraform
|
||||
# Run Checkov with custom checks
|
||||
checkov -d ./terraform \
|
||||
--framework terraform \
|
||||
--external-checks-dir ./custom_checks \
|
||||
--output cli \
|
||||
--compact
|
||||
|
||||
# Custom check
|
||||
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
|
||||
# Run specific check IDs
|
||||
checkov -d ./terraform \
|
||||
--check CUSTOM_S3_001,CUSTOM_RDS_001,CKV_AWS_18,CKV_AWS_19
|
||||
|
||||
class S3Encryption(BaseResourceCheck):
|
||||
def scan_resource_conf(self, conf):
|
||||
return CheckResult.PASSED if 'encryption' in conf else CheckResult.FAILED
|
||||
# Generate SARIF output for GitHub Advanced Security integration
|
||||
checkov -d ./terraform \
|
||||
--framework terraform \
|
||||
--output sarif \
|
||||
--output-file checkov-results.sarif
|
||||
|
||||
# Skip specific checks with documented justification
|
||||
checkov -d ./terraform \
|
||||
--skip-check CKV_AWS_145 \
|
||||
--skip-check CKV_AWS_79
|
||||
```
|
||||
|
||||
## CI/CD Pipeline Integration
|
||||
|
||||
```yaml
|
||||
# GitHub Actions - Policy enforcement in PR workflow
|
||||
name: Policy Checks
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'terraform/**'
|
||||
- 'kubernetes/**'
|
||||
|
||||
jobs:
|
||||
terraform-policy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@v3
|
||||
|
||||
- name: Terraform Init and Plan
|
||||
working-directory: terraform/
|
||||
run: |
|
||||
terraform init -backend=false
|
||||
terraform plan -out=tfplan
|
||||
terraform show -json tfplan > tfplan.json
|
||||
|
||||
- name: OPA Policy Check
|
||||
uses: open-policy-agent/setup-opa@v2
|
||||
with:
|
||||
version: latest
|
||||
- run: |
|
||||
RESULTS=$(opa eval \
|
||||
--data policies/ \
|
||||
--input terraform/tfplan.json \
|
||||
--format json \
|
||||
"data.terraform" | jq '.result[0].expressions[0].value')
|
||||
DENY_COUNT=$(echo "$RESULTS" | jq '[.. | .deny? // empty | .[] ] | length')
|
||||
if [ "$DENY_COUNT" -gt 0 ]; then
|
||||
echo "::error::Policy violations found:"
|
||||
echo "$RESULTS" | jq '.. | .deny? // empty | .[]'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkov Scan
|
||||
uses: bridgecrewio/checkov-action@v12
|
||||
with:
|
||||
directory: terraform/
|
||||
framework: terraform
|
||||
output_format: cli,sarif
|
||||
output_file_path: console,checkov-results.sarif
|
||||
soft_fail: false
|
||||
external_checks_dirs: custom_checks/
|
||||
|
||||
- name: Upload SARIF
|
||||
if: always()
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: checkov-results.sarif
|
||||
|
||||
kubernetes-policy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Kyverno CLI
|
||||
run: |
|
||||
curl -LO https://github.com/kyverno/kyverno/releases/latest/download/kyverno-cli_linux_amd64.tar.gz
|
||||
tar -xzf kyverno-cli_linux_amd64.tar.gz
|
||||
sudo mv kyverno /usr/local/bin/
|
||||
|
||||
- name: Test Kyverno Policies
|
||||
run: |
|
||||
kyverno apply policies/kyverno/ \
|
||||
--resource kubernetes/manifests/ \
|
||||
--detailed-results \
|
||||
--output-format table
|
||||
|
||||
- name: Conftest Kubernetes Manifests
|
||||
uses: open-policy-agent/conftest-action@v2
|
||||
with:
|
||||
files: kubernetes/manifests/
|
||||
policy: policies/kubernetes/
|
||||
```
|
||||
|
||||
## Policy Exception Management
|
||||
|
||||
```yaml
|
||||
exception_workflow:
|
||||
request:
|
||||
fields:
|
||||
- policy_id: "Which policy needs an exception"
|
||||
- resource: "Specific resource requiring exception"
|
||||
- justification: "Business reason for the exception"
|
||||
- compensating_controls: "Alternative mitigations in place"
|
||||
- duration: "Temporary (with expiry) or permanent"
|
||||
- requestor: "Person requesting"
|
||||
- approver: "Security team member who approved"
|
||||
|
||||
approval_process:
|
||||
1: "Requestor submits exception with justification"
|
||||
2: "Security team reviews and assesses risk"
|
||||
3: "Compensating controls verified"
|
||||
4: "Exception approved or denied with rationale"
|
||||
5: "Exception documented in registry"
|
||||
6: "Automated enforcement updated to allow exception"
|
||||
|
||||
enforcement:
|
||||
opa: |
|
||||
# Exception list loaded as data
|
||||
# policies/exceptions.json
|
||||
# {"exceptions": [{"resource": "aws_s3_bucket.public_website", "policy": "no-public-s3", "expires": "2025-06-01"}]}
|
||||
kyverno: |
|
||||
# Use Kyverno PolicyException resource
|
||||
apiVersion: kyverno.io/v2beta1
|
||||
kind: PolicyException
|
||||
metadata:
|
||||
name: allow-public-website
|
||||
namespace: web
|
||||
spec:
|
||||
exceptions:
|
||||
- policyName: disallow-privileged-containers
|
||||
ruleNames:
|
||||
- deny-privileged
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Pod
|
||||
names:
|
||||
- legacy-app-*
|
||||
|
||||
review_schedule:
|
||||
- Review all active exceptions quarterly
|
||||
- Expire temporary exceptions automatically
|
||||
- Re-justify permanent exceptions annually
|
||||
- Track exception count trends as a security metric
|
||||
```
|
||||
|
||||
## Policy Testing
|
||||
|
||||
```bash
|
||||
# Test OPA policies with mock input
|
||||
mkdir -p policies/tests
|
||||
|
||||
# Create test input
|
||||
cat > policies/tests/public_bucket_test.json <<'EOF'
|
||||
{
|
||||
"resource_changes": [{
|
||||
"address": "aws_s3_bucket.test",
|
||||
"type": "aws_s3_bucket",
|
||||
"change": {
|
||||
"after": {"acl": "public-read"}
|
||||
}
|
||||
}]
|
||||
}
|
||||
EOF
|
||||
|
||||
# Run test
|
||||
opa eval --data policies/ --input policies/tests/public_bucket_test.json \
|
||||
"data.terraform.aws.s3.deny" --format pretty
|
||||
# Should output the deny message
|
||||
|
||||
# OPA unit tests
|
||||
cat > policies/tests/s3_test.rego <<'EOF'
|
||||
package terraform.aws.s3_test
|
||||
|
||||
import rego.v1
|
||||
import data.terraform.aws.s3
|
||||
|
||||
test_deny_public_bucket if {
|
||||
result := s3.deny with input as {"resource_changes": [{"address": "test", "type": "aws_s3_bucket", "change": {"after": {"acl": "public-read"}}}]}
|
||||
count(result) > 0
|
||||
}
|
||||
|
||||
test_allow_private_bucket if {
|
||||
result := s3.deny with input as {"resource_changes": [{"address": "test", "type": "aws_s3_bucket", "change": {"after": {"acl": "private"}}}]}
|
||||
count(result) == 0
|
||||
}
|
||||
EOF
|
||||
|
||||
opa test policies/ -v
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Version control policies
|
||||
- Test policies in CI
|
||||
- Gradual rollout (warn → enforce)
|
||||
- Exception management
|
||||
- Version control all policies alongside the infrastructure code they govern
|
||||
- Start in audit/warn mode and transition to enforce after verifying no false positives
|
||||
- Write unit tests for every policy to catch regressions and verify intended behavior
|
||||
- Implement a formal exception process: never disable policies to bypass legitimate checks
|
||||
- Use policy results as PR status checks to block non-compliant merges
|
||||
- Layer policies: Checkov for static analysis, OPA for Terraform plan evaluation, Kyverno for runtime
|
||||
- Tag policies with compliance framework references (e.g., SOC 2 CC6.1, PCI Req 2.2)
|
||||
- Monitor policy violation trends over time to identify systemic issues
|
||||
- Provide clear, actionable error messages that explain how to fix violations
|
||||
- Roll out new policies gradually: inform teams, give a remediation window, then enforce
|
||||
|
||||
@@ -9,65 +9,504 @@ metadata:
|
||||
|
||||
# Vendor Management
|
||||
|
||||
Manage third-party vendor security risks.
|
||||
Implement a vendor risk management program covering vendor assessment questionnaires, risk scoring, contract tracking, SLA monitoring, and ongoing oversight for compliance with SOC 2, ISO 27001, and regulatory frameworks.
|
||||
|
||||
## Vendor Assessment
|
||||
## When to Use
|
||||
|
||||
- Onboarding new vendors that will access company data or systems
|
||||
- Conducting annual vendor risk assessments and reassessments
|
||||
- Negotiating security requirements in vendor contracts
|
||||
- Monitoring vendor SLA compliance and security posture
|
||||
- Preparing vendor management evidence for SOC 2 or ISO 27001 audits
|
||||
|
||||
## Vendor Risk Tiering
|
||||
|
||||
```yaml
|
||||
assessment_process:
|
||||
1_identify:
|
||||
- Catalog all vendors
|
||||
- Classify by risk tier
|
||||
|
||||
2_assess:
|
||||
- Security questionnaire
|
||||
- SOC 2 review
|
||||
- Penetration test results
|
||||
|
||||
3_contract:
|
||||
- Security requirements
|
||||
- Data processing agreement
|
||||
- SLAs
|
||||
|
||||
4_monitor:
|
||||
- Continuous monitoring
|
||||
- Annual reassessment
|
||||
- Incident notification
|
||||
vendor_risk_tiers:
|
||||
critical:
|
||||
criteria:
|
||||
- Processes or stores sensitive/regulated data (PII, PHI, PCI)
|
||||
- Single point of failure (no alternative vendor)
|
||||
- Has privileged access to production systems
|
||||
- Handles authentication or security-critical functions
|
||||
assessment_requirements:
|
||||
- Full security questionnaire (SIG or custom)
|
||||
- SOC 2 Type II report review (or equivalent)
|
||||
- Penetration test results review
|
||||
- On-site or virtual security assessment (optional)
|
||||
- Business continuity and DR plan review
|
||||
review_frequency: Annual
|
||||
contract_requirements:
|
||||
- Data processing agreement (DPA)
|
||||
- Business associate agreement (BAA) if PHI
|
||||
- Security SLA with breach notification timeline
|
||||
- Right to audit clause
|
||||
- Cyber insurance requirements
|
||||
examples:
|
||||
- Cloud infrastructure providers (AWS, Azure, GCP)
|
||||
- Identity providers (Okta, Azure AD)
|
||||
- Payment processors (Stripe, Adyen)
|
||||
- Primary database or CRM SaaS
|
||||
|
||||
high:
|
||||
criteria:
|
||||
- Accesses significant company data (internal or confidential)
|
||||
- Integrates with production systems via API
|
||||
- Processes customer-facing transactions
|
||||
- Substitution would cause significant business disruption
|
||||
assessment_requirements:
|
||||
- Security questionnaire
|
||||
- SOC 2 report review (Type I or Type II)
|
||||
- Compliance certifications verified
|
||||
review_frequency: Annual
|
||||
contract_requirements:
|
||||
- Data processing agreement
|
||||
- Security requirements appendix
|
||||
- Incident notification clause (72 hours)
|
||||
examples:
|
||||
- Email/marketing platforms (SendGrid, HubSpot)
|
||||
- Monitoring and logging SaaS (Datadog, Splunk)
|
||||
- CI/CD platforms (GitHub, GitLab)
|
||||
- Customer support platforms
|
||||
|
||||
medium:
|
||||
criteria:
|
||||
- Limited data access (internal data only)
|
||||
- Non-production system integration
|
||||
- Some business impact if unavailable
|
||||
assessment_requirements:
|
||||
- Abbreviated security questionnaire
|
||||
- Compliance certification verification
|
||||
review_frequency: Every 2 years
|
||||
contract_requirements:
|
||||
- Standard vendor terms with security clause
|
||||
- NDA
|
||||
examples:
|
||||
- Project management tools
|
||||
- HR platforms
|
||||
- Travel and expense systems
|
||||
|
||||
low:
|
||||
criteria:
|
||||
- No access to company data
|
||||
- No system integration
|
||||
- Easily replaceable
|
||||
assessment_requirements:
|
||||
- Basic due diligence (public info review)
|
||||
- Confirm no data sharing
|
||||
review_frequency: Every 3 years or on renewal
|
||||
contract_requirements:
|
||||
- Standard terms
|
||||
examples:
|
||||
- Office supply vendors
|
||||
- Facilities services
|
||||
- General consulting (no data access)
|
||||
```
|
||||
|
||||
## Risk Tiers
|
||||
|
||||
| Tier | Criteria | Assessment |
|
||||
|------|----------|------------|
|
||||
| Critical | Access to sensitive data | Full assessment, annual |
|
||||
| High | Significant data access | Questionnaire + SOC 2 |
|
||||
| Medium | Limited data access | Security questionnaire |
|
||||
| Low | No data access | Basic due diligence |
|
||||
|
||||
## Security Questionnaire
|
||||
## Vendor Assessment Questionnaire
|
||||
|
||||
```yaml
|
||||
categories:
|
||||
security_questionnaire:
|
||||
section_1_governance:
|
||||
questions:
|
||||
- "Do you have a documented information security policy?"
|
||||
- "Is there a designated CISO or security lead?"
|
||||
- "Do you conduct annual security risk assessments?"
|
||||
- "Do you have a security awareness training program?"
|
||||
- "What compliance certifications do you hold? (SOC 2, ISO 27001, etc.)"
|
||||
- "When was your last external security audit?"
|
||||
- "Do you carry cyber liability insurance? What coverage limits?"
|
||||
evidence_requested:
|
||||
- Information security policy (or summary)
|
||||
- SOC 2 Type II report (or bridge letter)
|
||||
- ISO 27001 certificate
|
||||
- Cyber insurance certificate
|
||||
|
||||
section_2_access_control:
|
||||
questions:
|
||||
- "How do you manage user access to systems containing our data?"
|
||||
- "Is multi-factor authentication enforced for all personnel?"
|
||||
- "How frequently do you conduct access reviews?"
|
||||
- "What is your process for revoking access upon employee termination?"
|
||||
- "Do you support SSO/SAML integration for customer access?"
|
||||
- "How do you manage privileged access?"
|
||||
evidence_requested:
|
||||
- Access management policy
|
||||
- MFA configuration documentation
|
||||
- Access review records (sample)
|
||||
|
||||
section_3_data_protection:
|
||||
questions:
|
||||
- "How is our data encrypted at rest?"
|
||||
- "How is our data encrypted in transit?"
|
||||
- "In which geographic regions is our data stored?"
|
||||
- "Do you use sub-processors? If so, provide a list."
|
||||
- "What is your data retention policy?"
|
||||
- "How is our data isolated from other customers? (multi-tenancy model)"
|
||||
- "Can you provide data export in standard formats upon request?"
|
||||
- "What is your data destruction process at contract end?"
|
||||
evidence_requested:
|
||||
- Encryption standards documentation
|
||||
- Sub-processor list
|
||||
- Data flow diagram showing customer data handling
|
||||
|
||||
section_4_vulnerability_management:
|
||||
questions:
|
||||
- "How frequently do you perform vulnerability scans?"
|
||||
- "How frequently do you conduct penetration tests?"
|
||||
- "What is your patch management SLA for critical vulnerabilities?"
|
||||
- "Do you have a responsible disclosure or bug bounty program?"
|
||||
- "How do you manage vulnerabilities in third-party dependencies?"
|
||||
evidence_requested:
|
||||
- Penetration test executive summary (last 12 months)
|
||||
- Vulnerability management policy
|
||||
- Patch management SLA documentation
|
||||
|
||||
section_5_incident_response:
|
||||
questions:
|
||||
- "Do you have a documented incident response plan?"
|
||||
- "What is your breach notification timeline?"
|
||||
- "Have you experienced a data breach in the last 3 years?"
|
||||
- "How would you notify us in the event of a security incident?"
|
||||
- "Do you conduct incident response tabletop exercises?"
|
||||
evidence_requested:
|
||||
- Incident response plan summary
|
||||
- Breach notification procedure
|
||||
|
||||
section_6_business_continuity:
|
||||
questions:
|
||||
- "Do you have a business continuity plan?"
|
||||
- "Do you have a disaster recovery plan?"
|
||||
- "What are your RTO and RPO targets?"
|
||||
- "How frequently do you test your DR plan?"
|
||||
- "What is your uptime SLA?"
|
||||
- "Do you have geographic redundancy?"
|
||||
evidence_requested:
|
||||
- BCP/DR plan summary
|
||||
- Uptime SLA documentation
|
||||
- Most recent DR test results
|
||||
|
||||
section_7_compliance:
|
||||
questions:
|
||||
- "Do you process data subject to GDPR, HIPAA, or PCI DSS?"
|
||||
- "How do you support our compliance obligations?"
|
||||
- "Do you have a Data Processing Agreement (DPA) template?"
|
||||
- "How do you handle data subject access requests (DSARs)?"
|
||||
- "Are you FedRAMP authorized? If so, at what impact level?"
|
||||
evidence_requested:
|
||||
- DPA template
|
||||
- Compliance certification documentation
|
||||
```
|
||||
|
||||
## Risk Scoring Model
|
||||
|
||||
```yaml
|
||||
risk_scoring:
|
||||
dimensions:
|
||||
data_sensitivity:
|
||||
weight: 30
|
||||
scores:
|
||||
1: "No access to company or customer data"
|
||||
2: "Access to public or non-sensitive internal data"
|
||||
3: "Access to internal confidential data"
|
||||
4: "Access to PII or customer financial data"
|
||||
5: "Access to regulated data (PHI, PCI, classified)"
|
||||
|
||||
system_access:
|
||||
weight: 25
|
||||
scores:
|
||||
1: "No system access"
|
||||
2: "Read-only access to non-production"
|
||||
3: "Read/write access to non-production or read-only production"
|
||||
4: "Read/write access to production systems"
|
||||
5: "Privileged/admin access to production or security systems"
|
||||
|
||||
business_criticality:
|
||||
weight: 20
|
||||
scores:
|
||||
1: "No operational dependency"
|
||||
2: "Minor convenience; easily replaced"
|
||||
3: "Moderate dependency; replacement in weeks"
|
||||
4: "Significant dependency; replacement in months"
|
||||
5: "Critical dependency; no viable alternative"
|
||||
|
||||
security_posture:
|
||||
weight: 15
|
||||
scores:
|
||||
5: "No certifications, no formal security program"
|
||||
4: "Some security controls but no external validation"
|
||||
3: "SOC 2 Type I or equivalent"
|
||||
2: "SOC 2 Type II within last 12 months"
|
||||
1: "Multiple certifications (SOC 2 + ISO 27001), strong program"
|
||||
|
||||
regulatory_exposure:
|
||||
weight: 10
|
||||
scores:
|
||||
1: "No regulatory requirements"
|
||||
2: "General data protection (GDPR basic)"
|
||||
3: "Industry-specific (HIPAA, PCI)"
|
||||
4: "Government (FedRAMP, ITAR)"
|
||||
5: "Multiple stringent regulations"
|
||||
|
||||
calculation:
|
||||
formula: "Sum of (dimension_score * dimension_weight) / 100"
|
||||
risk_levels:
|
||||
low: "Score 1.0 - 2.0"
|
||||
medium: "Score 2.1 - 3.0"
|
||||
high: "Score 3.1 - 4.0"
|
||||
critical: "Score 4.1 - 5.0"
|
||||
|
||||
example:
|
||||
vendor: "Payment Processor X"
|
||||
data_sensitivity: 5 # PCI data
|
||||
system_access: 4 # Production API integration
|
||||
business_criticality: 5 # No alternative
|
||||
security_posture: 2 # SOC 2 Type II
|
||||
regulatory_exposure: 3 # PCI DSS
|
||||
score: "(5*30 + 4*25 + 5*20 + 2*15 + 3*10) / 100 = 4.1 -> Critical"
|
||||
```
|
||||
|
||||
## Vendor Registry and Contract Tracking
|
||||
|
||||
```yaml
|
||||
vendor_registry_schema:
|
||||
vendor_info:
|
||||
vendor_id: "VND-NNNN"
|
||||
vendor_name: ""
|
||||
vendor_website: ""
|
||||
primary_contact_email: ""
|
||||
security_contact_email: ""
|
||||
vendor_category: "" # SaaS, IaaS, Consulting, etc.
|
||||
|
||||
risk_assessment:
|
||||
risk_tier: "" # critical, high, medium, low
|
||||
risk_score: 0.0
|
||||
last_assessment_date: ""
|
||||
next_assessment_date: ""
|
||||
assessment_status: "" # current, due, overdue
|
||||
open_findings: 0
|
||||
certifications:
|
||||
- type: "SOC 2 Type II"
|
||||
valid_until: ""
|
||||
report_on_file: true
|
||||
- type: "ISO 27001"
|
||||
valid_until: ""
|
||||
certificate_on_file: true
|
||||
|
||||
contract:
|
||||
contract_id: ""
|
||||
start_date: ""
|
||||
end_date: ""
|
||||
auto_renewal: true
|
||||
cancellation_notice_days: 90
|
||||
annual_value: 0
|
||||
terms:
|
||||
data_processing_agreement: true
|
||||
nda: true
|
||||
baa: false
|
||||
right_to_audit: true
|
||||
breach_notification_sla: "72 hours"
|
||||
data_return_clause: true
|
||||
data_destruction_clause: true
|
||||
cyber_insurance_required: true
|
||||
|
||||
data_access:
|
||||
data_types: []
|
||||
data_classification: ""
|
||||
data_location: []
|
||||
sub_processors: []
|
||||
|
||||
sla_tracking:
|
||||
uptime_sla: "99.9%"
|
||||
actual_uptime_last_month: ""
|
||||
support_response_sla: ""
|
||||
sla_breaches_ytd: 0
|
||||
|
||||
status: "" # active, under_review, offboarding, inactive
|
||||
owner: "" # Internal team/person responsible
|
||||
```
|
||||
|
||||
## SLA Monitoring
|
||||
|
||||
```python
|
||||
"""
|
||||
Vendor SLA monitoring - Track uptime and response time commitments.
|
||||
"""
|
||||
import requests
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
class VendorSLAMonitor:
|
||||
def __init__(self, vendors_config):
|
||||
self.vendors = vendors_config
|
||||
|
||||
def check_uptime(self, vendor):
|
||||
"""Check vendor service availability."""
|
||||
results = []
|
||||
for endpoint in vendor.get("health_endpoints", []):
|
||||
try:
|
||||
resp = requests.get(
|
||||
endpoint["url"],
|
||||
timeout=endpoint.get("timeout", 10),
|
||||
headers=endpoint.get("headers", {}),
|
||||
)
|
||||
results.append({
|
||||
"endpoint": endpoint["url"],
|
||||
"status": resp.status_code,
|
||||
"response_time_ms": resp.elapsed.total_seconds() * 1000,
|
||||
"healthy": resp.status_code == endpoint.get("expected_status", 200),
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
})
|
||||
except requests.RequestException as e:
|
||||
results.append({
|
||||
"endpoint": endpoint["url"],
|
||||
"status": "error",
|
||||
"error": str(e),
|
||||
"healthy": False,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
})
|
||||
return results
|
||||
|
||||
def check_status_page(self, vendor):
|
||||
"""Check vendor status page for active incidents."""
|
||||
status_url = vendor.get("status_page_url")
|
||||
if not status_url:
|
||||
return None
|
||||
try:
|
||||
api_url = f"{status_url}/api/v2/summary.json"
|
||||
resp = requests.get(api_url, timeout=10)
|
||||
data = resp.json()
|
||||
return {
|
||||
"vendor": vendor["name"],
|
||||
"status": data.get("status", {}).get("indicator", "unknown"),
|
||||
"active_incidents": len(data.get("incidents", [])),
|
||||
"components": [
|
||||
{"name": c["name"], "status": c["status"]}
|
||||
for c in data.get("components", [])
|
||||
],
|
||||
}
|
||||
except Exception:
|
||||
return {"vendor": vendor["name"], "status": "unknown"}
|
||||
|
||||
def generate_sla_report(self, vendor_name, monthly_checks):
|
||||
"""Calculate monthly SLA compliance."""
|
||||
total = len(monthly_checks)
|
||||
healthy = sum(1 for c in monthly_checks if c.get("healthy"))
|
||||
uptime_pct = (healthy / total * 100) if total > 0 else 0
|
||||
avg_response = (
|
||||
sum(c.get("response_time_ms", 0) for c in monthly_checks if c.get("healthy"))
|
||||
/ max(healthy, 1)
|
||||
)
|
||||
return {
|
||||
"vendor": vendor_name,
|
||||
"period": datetime.now(timezone.utc).strftime("%Y-%m"),
|
||||
"total_checks": total,
|
||||
"healthy_checks": healthy,
|
||||
"uptime_percentage": round(uptime_pct, 3),
|
||||
"avg_response_time_ms": round(avg_response, 1),
|
||||
"sla_met": uptime_pct >= 99.9,
|
||||
}
|
||||
```
|
||||
|
||||
## Vendor Lifecycle Management
|
||||
|
||||
```yaml
|
||||
vendor_lifecycle:
|
||||
onboarding:
|
||||
step_1_request:
|
||||
- Business owner submits vendor request with use case
|
||||
- Procurement assigns vendor ID
|
||||
- Initial risk tier assessment based on data access and criticality
|
||||
|
||||
step_2_assess:
|
||||
- Send security questionnaire (appropriate to tier)
|
||||
- Review compliance certifications
|
||||
- Evaluate questionnaire responses
|
||||
- Score vendor risk
|
||||
|
||||
step_3_contract:
|
||||
- Negotiate security requirements based on risk tier
|
||||
- Execute DPA/BAA as required
|
||||
- Document data flows and access scope
|
||||
- Set SLA expectations
|
||||
|
||||
step_4_provision:
|
||||
- Configure integration with least privilege access
|
||||
- Enable audit logging for vendor access
|
||||
- Add to vendor registry
|
||||
- Schedule first reassessment
|
||||
|
||||
ongoing_management:
|
||||
monitoring:
|
||||
- Track SLA compliance monthly
|
||||
- Monitor vendor status pages for incidents
|
||||
- Review vendor security advisories
|
||||
- Track data sub-processor changes
|
||||
reassessment:
|
||||
- Conduct reassessment per tier schedule
|
||||
- Review updated SOC 2 / ISO 27001 reports
|
||||
- Verify certifications are current
|
||||
- Update risk score
|
||||
|
||||
offboarding:
|
||||
step_1_plan:
|
||||
- Data migration or transition to replacement vendor
|
||||
- Identify all integrations and access points
|
||||
- Communication plan for stakeholders
|
||||
|
||||
step_2_execute:
|
||||
- Revoke all API keys, credentials, and access
|
||||
- Request data return or destruction certificate
|
||||
- Remove vendor integrations from systems
|
||||
- Disable SSO/SAML connections
|
||||
|
||||
step_3_verify:
|
||||
- Confirm data destruction (written certification)
|
||||
- Verify all access revoked
|
||||
- Update vendor registry status to inactive
|
||||
- Archive vendor records for retention period
|
||||
```
|
||||
|
||||
## Vendor Management Checklist
|
||||
|
||||
```yaml
|
||||
vendor_management_checklist:
|
||||
program_setup:
|
||||
- [ ] Vendor risk tiering criteria defined
|
||||
- [ ] Security questionnaire template created
|
||||
- [ ] Risk scoring model documented
|
||||
- [ ] Vendor registry established
|
||||
- [ ] Onboarding and offboarding procedures documented
|
||||
- [ ] Contract security requirements defined per tier
|
||||
|
||||
ongoing_operations:
|
||||
- [ ] All active vendors cataloged in registry
|
||||
- [ ] Risk tier assigned to each vendor
|
||||
- [ ] Security assessments current (per tier schedule)
|
||||
- [ ] Compliance certifications on file and not expired
|
||||
- [ ] DPAs/BAAs signed for all vendors handling personal data
|
||||
- [ ] SLA monitoring active for critical and high-tier vendors
|
||||
- [ ] Sub-processor lists reviewed and tracked
|
||||
- [ ] Vendor security incidents tracked and assessed
|
||||
|
||||
governance:
|
||||
- Security policies
|
||||
- Risk management
|
||||
- Compliance certifications
|
||||
|
||||
technical:
|
||||
- Access controls
|
||||
- Encryption
|
||||
- Vulnerability management
|
||||
|
||||
operational:
|
||||
- Incident response
|
||||
- Business continuity
|
||||
- Change management
|
||||
- [ ] Vendor management policy approved and published
|
||||
- [ ] Roles and responsibilities assigned (owner per vendor)
|
||||
- [ ] Assessment findings tracked to remediation
|
||||
- [ ] Vendor risk reported to management quarterly
|
||||
- [ ] Offboarding includes data destruction verification
|
||||
- [ ] Evidence retained for compliance audit (3+ years)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Tier-based assessments
|
||||
- Regular reassessment
|
||||
- Contract security terms
|
||||
- Incident notification requirements
|
||||
- Exit strategy planning
|
||||
- Tier vendors by risk before investing assessment effort: not every vendor needs a full security review
|
||||
- Use standardized questionnaires (SIG, CAIQ, or consistent custom template) for comparable assessments
|
||||
- Review SOC 2 Type II reports thoroughly, including complementary user entity controls
|
||||
- Include right-to-audit clauses in contracts for critical vendors even if you do not exercise them frequently
|
||||
- Monitor vendor status pages and set up alerts for outages affecting your services
|
||||
- Track sub-processor changes: your vendor's vendor is part of your supply chain risk
|
||||
- Maintain a vendor registry as a single source of truth for all vendor relationships
|
||||
- Conduct offboarding rigorously: revoke all access and obtain data destruction certificates
|
||||
- Score vendor risk quantitatively to enable consistent prioritization and trend analysis
|
||||
- Report vendor risk metrics to management quarterly as part of the overall risk management program
|
||||
|
||||
Reference in New Issue
Block a user