-
Classify each gap into one of three types. This determines the remediation path.
| Type | Diagnosis | Fix requires |
|---|
| Design gap | Policy missing, control not designed | Draft policy, get it approved, communicate to staff |
| Operating gap | Policy exists, control inconsistently applied | Configure the technical control / enforce via automation |
| Evidence gap | Control works, no artifact proves it over the audit period | Set up automatic collection into the evidence locker |
-
Close Design gaps by drafting a policy. Every policy follows the same minimum structure:
# <Policy Name>
## Purpose
Two sentences: what this policy governs and why.
## Scope
Which systems, roles, or data classifications this applies to.
## Policy
The required behavior — phrased as "must" statements.
## Procedure
Numbered steps for implementing the policy.
## Roles (RACI)
| Activity | Responsible | Accountable | Consulted | Informed |
## Exceptions
Who approves, time-bound, compensating control requirement.
## Review
Owner + cadence (annual is minimum for most SOC2 policies).
## References
- Related controls: <framework>:<control-id>
- Related policies: <links>
Ship the policy via PR → CTO / CISO approval → store in compliance/<framework>/<control-id>/policy.md → announce in #all-hands or onboarding docs.
-
Close Operating gaps by configuring the control. Map control IDs to concrete infrastructure actions:
SOC2 CC6.1 (Logical access) — MFA enforcement:
aws iam update-account-password-policy \
--minimum-password-length 14 \
--require-symbols --require-numbers \
--require-uppercase-characters --require-lowercase-characters \
--max-password-age 90 --password-reuse-prevention 12
cat > scp-require-mfa.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "DenyAllExceptMfaLogin",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {"BoolIfExists": {"aws:MultiFactorAuthPresent": "false"}}
}]
}
EOF
aws organizations create-policy --type SERVICE_CONTROL_POLICY \
--name RequireMFA --content file://scp-require-mfa.json
SOC2 CC6.2 (Access provisioning) — SSO + JIT access:
aws sso-admin create-permission-set --instance-arn <arn> \
--name EngineeringRead --session-duration PT4H
Document access review in compliance/soc2/CC6.2/procedure.md; schedule via calendar invite.
SOC2 CC7.2 (Monitoring) — CloudTrail + GuardDuty org-wide:
aws cloudtrail create-trail --name org-trail --s3-bucket-name audit-logs-<acct> \
--is-organization-trail --is-multi-region-trail --enable-log-file-validation
aws cloudtrail start-logging --name org-trail
aws guardduty create-detector --enable --finding-publishing-frequency FIFTEEN_MINUTES
Alert on root account usage via EventBridge → SNS → PagerDuty.
SOC2 CC8.1 (Change management) — branch protection + signed commits:
gh api repos/acme/api/branches/main/protection -X PUT \
-f required_status_checks.strict=true \
-F required_status_checks.contexts='["ci/tests","ci/security-audit"]' \
-f enforce_admins=true \
-F required_pull_request_reviews.required_approving_review_count=2 \
-F required_pull_request_reviews.require_code_owner_reviews=true \
-f required_signatures=true
-
Close Evidence gaps by setting up automatic collection. Every control needs a dated artifact in the evidence locker for every month of the audit period. Examples:
Access review evidence (CC6.2):
okta-awscli --export-groups > compliance/soc2/CC6.2/evidence/$(date +%Y-%m)-okta-groups.json
Backup verification evidence (CC7.5 / A.12.3):
velero backup describe daily-$(date +%F) --details \
> compliance/soc2/CC7.5/evidence/$(date +%F)-restore-test.log
Vulnerability-management evidence (CC7.1):
Archive the weekly output of full-security-audit into compliance/soc2/CC7.1/evidence/<date>-audit.json. Retain for 2 years to cover a Type II audit period.
Monitoring evidence (CC7.2):
Daily AWS Config snapshot proving GuardDuty was enabled:
aws configservice describe-configuration-recorders > \
compliance/soc2/CC7.2/evidence/$(date +%F)-config-state.json
-
Build the evidence locker structure. Per-control-per-framework:
compliance/
└── soc2/
├── CC6.1/
│ ├── policy.md # design
│ ├── procedure.md # operating
│ ├── tests.md # test of effectiveness
│ └── evidence/
│ ├── 2026-01-okta-mfa-export.json
│ ├── 2026-02-okta-mfa-export.json
│ └── ...
└── CC7.2/
├── policy.md
├── procedure.md
├── tests.md
└── evidence/
├── 2026-01-guardduty-config.json
└── ...
-
Document the test of effectiveness per control in tests.md. Auditors need to see HOW you verify the control works, not just that it exists. Template:
# Test of effectiveness — CC6.1 (MFA enforcement)
## Test design
Sample 10 human user accounts across the audit period. For each:
1. Confirm MFA is enrolled in Okta.
2. Attempt login without MFA in a controlled test account; expect denial.
3. Verify CloudTrail logs show no MFA-less API calls for that user.
## Sampling strategy
Stratified random: 5 engineering, 3 non-eng, 2 contractors. Monthly.
## Expected outcome
100% of sampled accounts have MFA enrolled; 0 MFA-less logins in CloudTrail.
-
Write the remediation report. compliance-<framework>-<date>.md contains:
- Summary table: control ID · gap type · status (closed / in-progress / exception) · evidence links.
- Per-gap detail: description, remediation action taken, policy link, procedure link, evidence link, owner.
- Residual risk register (gaps not yet closed with target close date).
- Next audit-period actions (recurring evidence collection schedule).
-
Draft policy (compliance/soc2/CC6.1/policy.md):
# Access Management Policy
## Purpose
Define how access to Acme production systems and sensitive data is granted, reviewed, and revoked.
Ensures least privilege and timely de-provisioning.
## Scope
All employees, contractors, and service accounts with access to any system classified Internal or Confidential.
## Policy
- All human access must use SSO via Okta. No local accounts on production systems.
- All human access must require MFA.
- Access requests must be submitted via the Access Request JIRA template.
- Manager approval is required for any role assignment.
- Quarterly access reviews are performed; removal of unnecessary access within 5 business days of review.
- De-provisioning occurs within 1 hour of termination (automated via HRIS → Okta deactivation).
## Procedure
1. Request: employee files ACCESS-REQ ticket with requested role.
2. Approval: manager + system owner.
3. Grant: Okta admin assigns to the Okta group mapped to the role.
4. Review: quarterly access review run by Security lead.
5. Revoke: HRIS termination hook triggers Okta deactivation; automated.
## Roles (RACI)
| Activity | Responsible | Accountable | Consulted | Informed |
|---|---|---|---|---|
| Request | Employee | Manager | System owner | Security |
| Approval | Manager | System owner | Security | — |
| Review | Security | CTO | Managers | Auditors |
| Revocation | IT/HRIS | Security | — | Managers |
## Exceptions
Time-bound (≤ 30 days), approved by CTO + CISO, with compensating control and review date.
## Review
Owner: Head of Security. Cadence: annual.
## References
- SOC2 CC6.1, CC6.2, CC6.3
- ISO 27001 A.9.1, A.9.2
-
PR review → CTO approval → merge → announce in #all-hands.
-
Implement the technical control (Okta groups mapped to SSO roles):
-
Kick off first quarterly access review; save the output to evidence:
okta-awscli --export-groups > compliance/soc2/CC6.1/evidence/2026-Q2-access-review.json
-
Configure AWS Config to record the GuardDuty configuration-recorder state continuously:
aws configservice put-configuration-recorder \
--configuration-recorder '{"name":"default","roleARN":"arn:aws:iam::...","recordingGroup":{"allSupported":true,"includeGlobalResourceTypes":true}}'
aws configservice start-configuration-recorder --configuration-recorder-name default
-
Daily EventBridge schedule → Lambda → writes a dated snapshot to the evidence S3 bucket with Object Lock:
import boto3, json, datetime
def handler(event, _):
gd = boto3.client("guardduty")
det_ids = gd.list_detectors()["DetectorIds"]
state = [gd.get_detector(DetectorId=d) for d in det_ids]
key = f"soc2/CC7.2/evidence/{datetime.date.today():%Y-%m-%d}-guardduty-state.json"
boto3.client("s3").put_object(
Bucket="audit-evidence-acme",
Key=key,
Body=json.dumps(state, default=str).encode(),
ObjectLockMode="COMPLIANCE",
ObjectLockRetainUntilDate=datetime.datetime.utcnow() + datetime.timedelta(days=730),
)
-
Document the test of effectiveness (compliance/soc2/CC7.2/tests.md):
# Test of effectiveness — CC7.2 (GuardDuty continuous monitoring)
Sampling: pick 3 random days per month of the audit period; fetch the dated snapshot from S3;
verify `Status: ENABLED` and `DataSources.CloudTrail.Status: ENABLED`.
Expected: 100% of sampled days show ENABLED state across all production accounts.