{"dataset_version":"2026.07.14.01","last_updated":"2026-07-14","slice":"aws-evidence","contract_version":"1.0.0","license":{"spdx":"CC-BY-4.0","url":"https://creativecommons.org/licenses/by/4.0/","covers":"The DATA in this response (derived slices and authored overlays). The site code is not licensed by it.","attribution":"ramprules.com"},"data":{"overlay_version":"3.0.0","dataset_version_tested_against":"2026.07.14.01","liveDatasetVersion":"2026.07.14.01","versionMatch":true,"recipes":[{"id":"iam-credential-report","ksi_ids":["KSI-IAM-APM","KSI-IAM-ELP"],"control_ids":["ia-2","ia-5"],"aws_services":["IAM"],"evidence":"Account-wide credential report proving MFA is active per principal and that passwords/access keys are rotated within policy","collection":{"kind":"cli","commands":[{"name":"generate-credential-report","run":"aws iam generate-credential-report"},{"name":"credential-report-generated-time","run":"aws iam get-credential-report --query GeneratedTime --output text"},{"name":"credential-report","run":"aws iam get-credential-report --query Content --output text | base64 --decode"}]},"expected_output":"CSV, one row per IAM principal with columns including password_enabled, mfa_active, password_last_changed, access_key_1_active, access_key_1_last_rotated, access_key_2_last_rotated","cadence":"monthly","govcloud":"identical API and CSV schema; principal ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"Assert mfa_active=true for every password_enabled=true principal, and (now - access_key_N_last_rotated) <= 90d for every active key. generate-credential-report is async; poll get-credential-report until State=COMPLETE (report is regenerated at most every 4 hours). mfa_active is TRUE for a virtual TOTP device as well as a FIDO key, and the CSV covers IAM users and the root user only — a human confirms from the identity provider's own report that console authentication is phishing-resistant (the IA-02 (01)/(02) guidance) and that federated Identity Center or external-IdP sign-ins are covered elsewhere. The 90-day threshold is CIS Benchmark 1.14's, not FedRAMP's. Filed under IA-02 and IA-05 base: key age tests IA-05 (g), not AC-02 (01)'s automated account management or IA-05 (01)'s password rules.","assertions":[{"field":"credential-report-generated-time","op":"max_age_days","value":1,"controls":["ia-2","ia-5"],"description":"The report being read was generated within the last day — a stored report is served for up to four hours, and nothing else bounds its age."},{"field":"credential-report[].mfa_active","op":"eq","value":"TRUE","where":[{"field":"password_enabled","op":"eq","value":"TRUE"}],"controls":["ia-2"],"description":"Every principal with a console password has MFA active."},{"field":"credential-report[].access_key_1_last_rotated","op":"max_age_days","value":90,"where":[{"field":"access_key_1_active","op":"eq","value":"TRUE"}],"controls":["ia-5"],"description":"Every active first access key was rotated within the last 90 days."},{"field":"credential-report[].access_key_2_last_rotated","op":"max_age_days","value":90,"where":[{"field":"access_key_2_active","op":"eq","value":"TRUE"}],"controls":["ia-5"],"description":"Every active second access key was rotated within the last 90 days."}],"scope":{"resource_types":["AWS::IAM::User"],"regions":"global-service","accounts":"single","note":"IAM is global; the credential report is per-account with no organization-wide equivalent, so a multi-account boundary needs one invocation per account."},"references":[{"title":"Generate credential reports for your AWS account (CSV columns; a report can be generated at most once every four hours)","url":"https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_getting-report.html"},{"title":"AWS CLI: iam generate-credential-report","url":"https://docs.aws.amazon.com/cli/latest/reference/iam/generate-credential-report.html"},{"title":"AWS CLI: iam get-credential-report","url":"https://docs.aws.amazon.com/cli/latest/reference/iam/get-credential-report.html"}],"source":"aws","tools":["IAM"]},{"id":"iam-account-authorization-details","ksi_ids":["KSI-IAM-ELP","KSI-IAM-AAM"],"control_ids":["ac-2","ac-3","ac-6.1"],"aws_services":["IAM"],"evidence":"Full snapshot of every IAM user, group, role, and attached/inline policy with their relationships, used to review that granted permissions match least-privilege intent","collection":{"kind":"cli","commands":[{"name":"user-details","run":"aws iam get-account-authorization-details --query 'UserDetailList[].{User:UserName,Groups:GroupList,Attached:AttachedManagedPolicies[].PolicyName,Inline:UserPolicyList[].PolicyName}'"},{"name":"role-details","run":"aws iam get-account-authorization-details --filter Role --query 'RoleDetailList[].{Role:RoleName,Trust:AssumeRolePolicyDocument,Attached:AttachedManagedPolicies[].PolicyName}'"}]},"expected_output":"JSON with UserDetailList, GroupDetailList, RoleDetailList, and Policies arrays; embedded policy documents are URL-encoded (RFC 3986) and must be decoded before diffing against approved baselines","cadence":"quarterly","govcloud":"identical API; policy ARNs and principal ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"The API fully enumerates entitlements (telemetry), but deciding whether a given permission set is 'least privilege' requires human judgement against a documented access baseline. Reviewers should diff this snapshot against the last approved baseline and justify deltas.","references":[{"title":"AWS CLI: iam get-account-authorization-details (UserDetailList / GroupDetailList / RoleDetailList / Policies)","url":"https://docs.aws.amazon.com/cli/latest/reference/iam/get-account-authorization-details.html"},{"title":"Managed policies and inline policies — why an inline-policy review is a separate pass","url":"https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html"}],"source":"aws","tools":["IAM"]},{"id":"iam-access-analyzer-unused-access","ksi_ids":["KSI-IAM-ELP","KSI-IAM-JIT"],"control_ids":["ac-2.3","ac-6.7"],"aws_services":["IAM Access Analyzer"],"evidence":"Active unused-access findings identifying IAM roles, access keys, console passwords, and service/action-level permissions that have not been used within the configured age, driving right-sizing and removal","collection":{"kind":"cli","commands":[{"name":"account-unused-access-analyzers","run":"aws accessanalyzer list-analyzers --type ACCOUNT_UNUSED_ACCESS --query 'analyzers[].{arn:arn,status:status,age:configuration.unusedAccess.unusedAccessAge}'"},{"name":"organization-unused-access-analyzers","run":"aws accessanalyzer list-analyzers --type ORGANIZATION_UNUSED_ACCESS --query 'analyzers[].{arn:arn,status:status,age:configuration.unusedAccess.unusedAccessAge}'"},{"name":"unused-access-findings","run":"aws accessanalyzer list-findings-v2 --analyzer-arn <UNUSED_ACCESS_ANALYZER_ARN> --filter '{\"status\":{\"eq\":[\"ACTIVE\"]}}'"}]},"expected_output":"JSON findings array; each finding has a findingType of UnusedIAMRole, UnusedIAMUserAccessKey, UnusedIAMUserPassword, or UnusedPermission, with the affected resource ARN and status","cadence":"continuous","govcloud":"IAM Access Analyzer is available in AWS GovCloud (US); analyzer and resource ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"Use list-findings-v2 (list-findings is external-access only and does not return unused-access findings). Requires an existing ACCOUNT_UNUSED_ACCESS analyzer; create one with create-analyzer --type ACCOUNT_UNUSED_ACCESS --configuration '{\"unusedAccess\":{\"unusedAccessAge\":90}}'. Target zero ACTIVE unused-permission findings for privileged roles. An empty findings list is meaningful only if an analyzer exists, is ACTIVE, and its unusedAccessAge equals the review period the SSP declares; entities younger than the tracking period, and principals or accounts excluded by tag, never appear as findings. UnusedPermission findings are computed for roles, so IAM users' unused permissions are outside this output, and AC-06 (07)(a)'s periodic review is a human record — which is why this is partial.","assertions":[{"field":"account-unused-access-analyzers[].status","op":"eq","value":"ACTIVE","controls":["ac-2.3","ac-6.7"],"description":"Every unused-access analyzer is ACTIVE — a DISABLED or CREATING analyzer also returns zero findings."},{"field":"account-unused-access-analyzers[].age","op":"lte","value":90,"controls":["ac-2.3","ac-6.7"],"description":"The tracking period is no longer than the 90-day review period, or an entity unused for 91 days is not yet a finding."},{"field":"unused-access-findings.findings[]","op":"count_eq","value":0,"where":[{"field":"status","op":"eq","value":"ACTIVE"},{"field":"findingType","op":"in","value":["UnusedIAMRole","UnusedIAMUserAccessKey","UnusedIAMUserPassword"]}],"controls":["ac-2.3"],"description":"No role, access key or console password has gone unused past the analyzer's configured age."},{"field":"unused-access-findings.findings[]","op":"count_eq","value":0,"where":[{"field":"status","op":"eq","value":"ACTIVE"},{"field":"findingType","op":"eq","value":"UnusedPermission"}],"controls":["ac-6.7"],"description":"No principal retains a service or action permission it has not exercised."}],"scope":{"resource_types":["AWS::IAM::Role","AWS::IAM::User"],"regions":"global-service","accounts":"organization","enumerated_by":["organization-unused-access-analyzers"],"note":"AWS documents that unused-access findings do not vary by Region; organization scope comes from the ORGANIZATION_UNUSED_ACCESS analyzer type, not from iterating accounts."},"references":[{"title":"Create an IAM Access Analyzer unused access analyzer (tracking period, 1–365 days)","url":"https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-create-unused.html"},{"title":"IAM Access Analyzer findings","url":"https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-findings.html"},{"title":"AWS CLI: accessanalyzer list-findings-v2 (findingType UnusedIAMRole / UnusedIAMUserAccessKey / UnusedIAMUserPassword / UnusedPermission; status ACTIVE / ARCHIVED / RESOLVED)","url":"https://docs.aws.amazon.com/cli/latest/reference/accessanalyzer/list-findings-v2.html"}],"source":"aws","tools":["IAM Access Analyzer"]},{"id":"config-mfa-enabled-console-access","ksi_ids":["KSI-IAM-APM"],"control_ids":["ia-2"],"aws_services":["AWS Config","IAM"],"evidence":"AWS Config compliance result for the managed rule proving every IAM user with a console password has MFA enabled","collection":{"kind":"config-rule","commands":[{"name":"mfa-enabled-for-iam-console-access","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name mfa-enabled-for-iam-console-access --compliance-types NON_COMPLIANT"},{"name":"configuration-recorder-status","run":"aws configservice describe-configuration-recorder-status"},{"name":"mfa-enabled-for-iam-console-access-evaluation-status","run":"aws configservice describe-config-rule-evaluation-status --config-rule-names mfa-enabled-for-iam-console-access"}]},"expected_output":"EvaluationResults array; empty NON_COMPLIANT set means all console-enabled IAM users have MFA. Managed rule identifier: MFA_ENABLED_FOR_IAM_CONSOLE_ACCESS Plus describe-configuration-recorder-status showing recording=true and, per rule, describe-config-rule-evaluation-status showing FirstEvaluationStarted=true with a LastSuccessfulEvaluationTime — the proof the empty set was produced by a check that ran.","cadence":"continuous","govcloud":"AWS Config and this managed rule are available in AWS GovCloud (US); resource ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"Substitute your deployed rule name if it differs from the default. An empty NON_COMPLIANT result is the pass condition. Pair with the IAM_USER_MFA_ENABLED rule to also catch programmatic users. MFA_ENABLED_FOR_IAM_CONSOLE_ACCESS is COMPLIANT for any enabled device, including a virtual TOTP app; whether the factor is phishing-resistant (the IA-02 (01)/(02) guidance), and whether workforce console access runs through IAM users at all rather than IAM Identity Center, are judgements outside this output — filed under IA-02 base for that reason, and nothing here partitions privileged from non-privileged accounts.","assertions":[{"field":"mfa-enabled-for-iam-console-access.EvaluationResults","op":"count_eq","value":0,"controls":["ia-2"],"description":"No IAM user with console access lacks MFA."},{"field":"configuration-recorder-status.ConfigurationRecordersStatus[].recording","op":"eq","value":true,"controls":["ia-2"],"description":"The configuration recorder is on — without it an empty NON_COMPLIANT set is indistinguishable from a recorder that never ran."},{"field":"mfa-enabled-for-iam-console-access-evaluation-status.ConfigRulesEvaluationStatus[].FirstEvaluationStarted","op":"eq","value":true,"controls":["ia-2"],"description":"mfa-enabled-for-iam-console-access has evaluated at least once; a deployed-but-never-evaluated rule also returns an empty result."},{"field":"mfa-enabled-for-iam-console-access-evaluation-status.ConfigRulesEvaluationStatus[].LastSuccessfulEvaluationTime","op":"exists","controls":["ia-2"],"description":"mfa-enabled-for-iam-console-access has a successful evaluation on record."}],"scope":{"resource_types":["AWS::IAM::User"],"regions":"single","accounts":"single","note":"AWS::IAM::User and AWS::IAM::Policy are global resource types that AWS documents as recordable only in Regions where Config existed before February 2022, with the advice to deploy periodic IAM rules to exactly one supported Region — so one Region is deliberate here, and each account in the boundary needs its own invocation."},"references":[{"title":"AWS Config managed rule: mfa-enabled-for-iam-console-access (MFA_ENABLED_FOR_IAM_CONSOLE_ACCESS, periodic, AWS::IAM::User)","url":"https://docs.aws.amazon.com/config/latest/developerguide/mfa-enabled-for-iam-console-access.html"},{"title":"AWS Config managed rule: iam-user-mfa-enabled — the companion rule covering users without console access","url":"https://docs.aws.amazon.com/config/latest/developerguide/iam-user-mfa-enabled.html"}],"source":"aws","tools":["AWS Config","IAM"]},{"id":"config-access-keys-rotated","ksi_ids":["KSI-IAM-SNU","KSI-IAM-APM"],"control_ids":["ia-5"],"aws_services":["AWS Config","IAM"],"evidence":"AWS Config compliance result proving long-lived IAM access keys (including those used by service/non-user identities) are rotated within the maximum age","collection":{"kind":"config-rule","commands":[{"name":"access-keys-rotated","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name access-keys-rotated --compliance-types NON_COMPLIANT"},{"name":"configuration-recorder-status","run":"aws configservice describe-configuration-recorder-status"},{"name":"access-keys-rotated-evaluation-status","run":"aws configservice describe-config-rule-evaluation-status --config-rule-names access-keys-rotated"}]},"expected_output":"EvaluationResults array; empty NON_COMPLIANT set means every active access key is within maxAccessKeyAge. Managed rule identifier: ACCESS_KEYS_ROTATED (maxAccessKeyAge parameter, e.g. 90) Plus describe-configuration-recorder-status showing recording=true and, per rule, describe-config-rule-evaluation-status showing FirstEvaluationStarted=true with a LastSuccessfulEvaluationTime — the proof the empty set was produced by a check that ran.","cadence":"continuous","govcloud":"AWS Config and this managed rule are available in AWS GovCloud (US); resource ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"Strongest evidence for non-user auth is the absence of long-lived keys entirely (prefer IAM roles / temporary credentials). Where keys must exist, this rule proves rotation. Set maxAccessKeyAge to your policy (<=90d). The rule's verdict is only as strong as the maxAccessKeyAge an operator set, and the output does not carry the parameter — a human confirms it matches the SSP's rotation period. Root-user access keys are outside the rule by AWS's documented limitation. Filed under IA-05 base: key age tests IA-05 (g), not AC-02 (01).","assertions":[{"field":"access-keys-rotated.EvaluationResults","op":"count_eq","value":0,"controls":["ia-5"],"description":"No active access key is older than the rule's maxAccessKeyAge."},{"field":"configuration-recorder-status.ConfigurationRecordersStatus[].recording","op":"eq","value":true,"controls":["ia-5"],"description":"The configuration recorder is on — without it an empty NON_COMPLIANT set is indistinguishable from a recorder that never ran."},{"field":"access-keys-rotated-evaluation-status.ConfigRulesEvaluationStatus[].FirstEvaluationStarted","op":"eq","value":true,"controls":["ia-5"],"description":"access-keys-rotated has evaluated at least once; a deployed-but-never-evaluated rule also returns an empty result."},{"field":"access-keys-rotated-evaluation-status.ConfigRulesEvaluationStatus[].LastSuccessfulEvaluationTime","op":"exists","controls":["ia-5"],"description":"access-keys-rotated has a successful evaluation on record."}],"scope":{"resource_types":["AWS::IAM::User"],"regions":"single","accounts":"single","note":"AWS::IAM::User and AWS::IAM::Policy are global resource types that AWS documents as recordable only in Regions where Config existed before February 2022, with the advice to deploy periodic IAM rules to exactly one supported Region — so one Region is deliberate here, and each account in the boundary needs its own invocation."},"references":[{"title":"AWS Config managed rule: access-keys-rotated (ACCESS_KEYS_ROTATED, maxAccessKeyAge default 90 days; does not apply to root user access keys)","url":"https://docs.aws.amazon.com/config/latest/developerguide/access-keys-rotated.html"}],"source":"aws","tools":["AWS Config","IAM"]},{"id":"config-iam-policy-no-admin-access","ksi_ids":["KSI-IAM-ELP"],"control_ids":["ac-6"],"aws_services":["AWS Config","IAM"],"evidence":"AWS Config compliance result proving no customer-managed IAM policy grants full administrative access (Allow Action:* on Resource:*)","collection":{"kind":"config-rule","commands":[{"name":"iam-policy-no-statements-with-admin-access","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name iam-policy-no-statements-with-admin-access --compliance-types NON_COMPLIANT"},{"name":"configuration-recorder-status","run":"aws configservice describe-configuration-recorder-status"},{"name":"iam-policy-no-statements-with-admin-access-evaluation-status","run":"aws configservice describe-config-rule-evaluation-status --config-rule-names iam-policy-no-statements-with-admin-access"}]},"expected_output":"EvaluationResults array; empty NON_COMPLIANT set means no evaluated customer-managed policy allows Action:* over Resource:*. Managed rule identifier: IAM_POLICY_NO_STATEMENTS_WITH_ADMIN_ACCESS Plus describe-configuration-recorder-status showing recording=true and, per rule, describe-config-rule-evaluation-status showing FirstEvaluationStarted=true with a LastSuccessfulEvaluationTime — the proof the empty set was produced by a check that ran.","cadence":"continuous","govcloud":"AWS Config and this managed rule are available in AWS GovCloud (US); policy ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"Scope caveat: this rule evaluates only customer-managed policies, not inline or AWS-managed policies. Combine with iam-account-authorization-details review to cover inline policies and admin AWS-managed policy attachments (e.g. AdministratorAccess). A human closes that gap by reading iam-account-authorization-details, which is why this is partial; and nothing in the output records the explicit authorizations AC-06 (01) asks for or partitions privileged from non-privileged users as AC-06 (10) does, so neither enhancement is claimed.","assertions":[{"field":"iam-policy-no-statements-with-admin-access.EvaluationResults","op":"count_eq","value":0,"controls":["ac-6"],"description":"No evaluated customer-managed policy allows Action:* on Resource:*."},{"field":"configuration-recorder-status.ConfigurationRecordersStatus[].recording","op":"eq","value":true,"controls":["ac-6"],"description":"The configuration recorder is on — without it an empty NON_COMPLIANT set is indistinguishable from a recorder that never ran."},{"field":"iam-policy-no-statements-with-admin-access-evaluation-status.ConfigRulesEvaluationStatus[].FirstEvaluationStarted","op":"eq","value":true,"controls":["ac-6"],"description":"iam-policy-no-statements-with-admin-access has evaluated at least once; a deployed-but-never-evaluated rule also returns an empty result."},{"field":"iam-policy-no-statements-with-admin-access-evaluation-status.ConfigRulesEvaluationStatus[].LastSuccessfulEvaluationTime","op":"exists","controls":["ac-6"],"description":"iam-policy-no-statements-with-admin-access has a successful evaluation on record."}],"scope":{"resource_types":["AWS::IAM::Policy"],"regions":"single","accounts":"single","note":"Customer-managed policies only; inline and AWS-managed policies are outside the rule's resource types. AWS::IAM::User and AWS::IAM::Policy are global resource types that AWS documents as recordable only in Regions where Config existed before February 2022, with the advice to deploy periodic IAM rules to exactly one supported Region — so one Region is deliberate here, and each account in the boundary needs its own invocation."},"references":[{"title":"AWS Config managed rule: iam-policy-no-statements-with-admin-access (IAM_POLICY_NO_STATEMENTS_WITH_ADMIN_ACCESS — customer-managed policies only, not inline or AWS-managed)","url":"https://docs.aws.amazon.com/config/latest/developerguide/iam-policy-no-statements-with-admin-access.html"},{"title":"Managed policies and inline policies","url":"https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html"}],"source":"aws","tools":["AWS Config","IAM"]},{"id":"identity-center-jit-elevation-workflow","ksi_ids":["KSI-IAM-JIT","KSI-IAM-AAM"],"control_ids":["ac-3","ac-6","ac-6.5"],"aws_services":["IAM Identity Center"],"evidence":"Documented just-in-time / break-glass privilege-elevation process backed by IAM Identity Center permission sets and account assignments, showing privileged access is role/attribute-based, time-bound, and approval-gated rather than standing","collection":{"kind":"cli","commands":[{"name":"list-instances","run":"aws sso-admin list-instances --query 'Instances[0].InstanceArn' --output text"},{"name":"list-permission-sets","run":"aws sso-admin list-permission-sets --instance-arn <INSTANCE_ARN>"},{"name":"describe-permission-set","run":"aws sso-admin describe-permission-set --instance-arn <INSTANCE_ARN> --permission-set-arn <PERMISSION_SET_ARN> --query 'PermissionSet.{Name:Name,SessionDuration:SessionDuration}'"},{"name":"list-account-assignments","run":"aws sso-admin list-account-assignments --instance-arn <INSTANCE_ARN> --account-id <ACCOUNT_ID> --permission-set-arn <PERMISSION_SET_ARN>"}]},"expected_output":"Permission set names with bounded SessionDuration (e.g. PT1H) and current account assignments; combined with the written approval-workflow runbook and ticket/approval records for each elevation","cadence":"quarterly","govcloud":"IAM Identity Center is available in AWS GovCloud (US); instance and permission-set ARNs use partition arn:aws-us-gov","automatable":"narrative","notes":"The API proves RBAC structure and bounded session duration, but it cannot by itself prove that each elevation was requested, approved, and time-limited just-in-time. The approval/break-glass workflow and its evidence (tickets, approvals, deprovisioning records) are a documented process; attach the runbook and sampled approval records. Do not present the permission-set listing alone as proof of JIT.","references":[{"title":"Manage AWS accounts with permission sets — AWS IAM Identity Center","url":"https://docs.aws.amazon.com/singlesignon/latest/userguide/permissionsetsconcept.html"},{"title":"Set session duration for AWS accounts — the time-bound half of a break-glass elevation","url":"https://docs.aws.amazon.com/singlesignon/latest/userguide/howtosessionduration.html"},{"title":"AWS CLI: sso-admin list-permission-sets","url":"https://docs.aws.amazon.com/cli/latest/reference/sso-admin/list-permission-sets.html"},{"title":"AWS CLI: sso-admin list-account-assignments","url":"https://docs.aws.amazon.com/cli/latest/reference/sso-admin/list-account-assignments.html"}],"source":"aws","tools":["IAM Identity Center"]},{"id":"guardduty-suspicious-iam-activity-response","ksi_ids":["KSI-IAM-SUS"],"control_ids":["ac-2.4","ac-2.13","ac-7"],"aws_services":["GuardDuty","CloudTrail","IAM"],"evidence":"GuardDuty IAM/credential-abuse findings (detection) paired with CloudTrail records of the responsive action taken to disable or secure the affected privileged principal","collection":{"kind":"cli","commands":[{"name":"list-detectors","run":"aws guardduty list-detectors --query 'DetectorIds[0]' --output text"},{"name":"list-findings","run":"aws guardduty list-findings --detector-id <DETECTOR_ID> --finding-criteria '{\"Criterion\":{\"type\":{\"Eq\":[\"UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS\"]}}}'"},{"name":"get-findings","run":"aws guardduty get-findings --detector-id <DETECTOR_ID> --finding-ids <FINDING_ID>"},{"name":"lookup-events-delete-login-profile","run":"aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=DeleteLoginProfile --max-results 10"}]},"expected_output":"GuardDuty finding detail (severity, affected IAM principal, timestamp) plus CloudTrail events (e.g. DeleteLoginProfile, DeleteAccessKey, DeactivateMFADevice, or PutUserPolicy attaching a deny) showing the account was disabled/secured after the finding","cadence":"continuous","govcloud":"GuardDuty and CloudTrail are available in AWS GovCloud (US); principal ARNs use partition arn:aws-us-gov. Some IAM-focused GuardDuty finding types depend on CloudTrail management-event coverage","automatable":"partial","notes":"Detection is pure telemetry; the response (which key/user to disable, whether it was malicious vs. authorized) requires incident-responder judgement, so this is partial. Evidence = finding + a linked responsive CloudTrail event within the IR SLA. cloudtrail lookup-events covers the last 90 days; use the finding type relevant to your workload.","references":[{"title":"GuardDuty IAM finding types (including UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS)","url":"https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-iam.html"},{"title":"AWS CLI: guardduty list-findings","url":"https://docs.aws.amazon.com/cli/latest/reference/guardduty/list-findings.html"},{"title":"AWS CLI: guardduty get-findings","url":"https://docs.aws.amazon.com/cli/latest/reference/guardduty/get-findings.html"},{"title":"AWS CLI: cloudtrail lookup-events — the responsive action's record","url":"https://docs.aws.amazon.com/cli/latest/reference/cloudtrail/lookup-events.html"}],"source":"aws","tools":["GuardDuty","CloudTrail","IAM"]},{"id":"config-encryption-at-rest","ksi_ids":["KSI-SVC-SIN"],"control_ids":["sc-28"],"aws_services":["AWS Config","Amazon S3","Amazon EBS","Amazon RDS","AWS KMS"],"evidence":"AWS Config compliance results across the storage services proving customer data is encrypted at rest — S3 buckets with default server-side encryption, EBS volumes encrypted, and RDS storage encrypted, all backed by KMS","collection":{"kind":"config-rule","commands":[{"name":"encrypted-volumes","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name encrypted-volumes --compliance-types NON_COMPLIANT"},{"name":"rds-storage-encrypted","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name rds-storage-encrypted --compliance-types NON_COMPLIANT"},{"name":"configuration-recorder-status","run":"aws configservice describe-configuration-recorder-status"},{"name":"encrypted-volumes-evaluation-status","run":"aws configservice describe-config-rule-evaluation-status --config-rule-names encrypted-volumes"},{"name":"rds-storage-encrypted-evaluation-status","run":"aws configservice describe-config-rule-evaluation-status --config-rule-names rds-storage-encrypted"},{"name":"describe-regions","run":"aws ec2 describe-regions --query 'Regions[].RegionName' --output text"},{"name":"list-accounts","run":"aws organizations list-accounts --query 'Accounts[].Id' --output text"}]},"expected_output":"Two EvaluationResults arrays; an empty NON_COMPLIANT set from each rule means every evaluated S3 bucket, attached EBS volume, and RDS instance is encrypted at rest. Managed rule identifiers: ENCRYPTED_VOLUMES, RDS_STORAGE_ENCRYPTED Plus describe-configuration-recorder-status showing recording=true and, per rule, describe-config-rule-evaluation-status showing FirstEvaluationStarted=true with a LastSuccessfulEvaluationTime — the proof the empty set was produced by a check that ran.","cadence":"continuous","govcloud":"AWS Config and all three managed rules are available in AWS GovCloud (US); bucket, volume, DB, and KMS key ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"These rules prove encryption is present, not which key backs it. Pass ENCRYPTED_VOLUMES and RDS_STORAGE_ENCRYPTED a kmsId/kmsKeyId parameter to additionally assert a specific CMK rather than any key. These three cover the dominant data stores; extend with the analogous rules for DynamoDB (dynamodb-table-encrypted-kms), EFS (efs-encrypted-check), SNS (sns-encrypted-kms), and other services you actually run — the recipe is the pattern, not the exhaustive list. The S3 rule was removed from this recipe: S3 has applied SSE-S3 to every bucket since 5 January 2023 and it cannot be disabled, so s3-bucket-server-side-encryption-enabled cannot fail and proved nothing. ENCRYPTED_VOLUMES sees attached volumes only, leaving detached volumes and every snapshot unproven, and no CMK is asserted — a human names the data stores in scope, confirms each has a rule, and reads key ownership for SC-28 (01), which is not claimed.","assertions":[{"field":"encrypted-volumes.EvaluationResults","op":"count_eq","value":0,"controls":["sc-28"],"description":"Every attached EBS volume is encrypted."},{"field":"rds-storage-encrypted.EvaluationResults","op":"count_eq","value":0,"controls":["sc-28"],"description":"Every RDS instance has encrypted storage."},{"field":"configuration-recorder-status.ConfigurationRecordersStatus[].recording","op":"eq","value":true,"controls":["sc-28"],"description":"The configuration recorder is on — without it an empty NON_COMPLIANT set is indistinguishable from a recorder that never ran."},{"field":"encrypted-volumes-evaluation-status.ConfigRulesEvaluationStatus[].FirstEvaluationStarted","op":"eq","value":true,"controls":["sc-28"],"description":"encrypted-volumes has evaluated at least once; a deployed-but-never-evaluated rule also returns an empty result."},{"field":"encrypted-volumes-evaluation-status.ConfigRulesEvaluationStatus[].LastSuccessfulEvaluationTime","op":"exists","controls":["sc-28"],"description":"encrypted-volumes has a successful evaluation on record."},{"field":"rds-storage-encrypted-evaluation-status.ConfigRulesEvaluationStatus[].FirstEvaluationStarted","op":"eq","value":true,"controls":["sc-28"],"description":"rds-storage-encrypted has evaluated at least once; a deployed-but-never-evaluated rule also returns an empty result."},{"field":"rds-storage-encrypted-evaluation-status.ConfigRulesEvaluationStatus[].LastSuccessfulEvaluationTime","op":"exists","controls":["sc-28"],"description":"rds-storage-encrypted has a successful evaluation on record."}],"scope":{"resource_types":["AWS::EC2::Volume","AWS::RDS::DBInstance"],"regions":"all-enabled","accounts":"organization","enumerated_by":["describe-regions","list-accounts"],"note":"Evaluate through an organization aggregator (describe-aggregate-compliance-by-config-rules) to cover every account and Region at once; the per-rule call shown reads one Region of one account, which is why the Region and account enumerations are part of the recipe."},"references":[{"title":"AWS Config managed rule: encrypted-volumes","url":"https://docs.aws.amazon.com/config/latest/developerguide/encrypted-volumes.html"},{"title":"AWS Config managed rule: rds-storage-encrypted","url":"https://docs.aws.amazon.com/config/latest/developerguide/rds-storage-encrypted.html"}],"source":"aws","tools":["AWS Config","Amazon S3","Amazon EBS","Amazon RDS","AWS KMS"]},{"id":"config-encryption-in-transit","ksi_ids":["KSI-SVC-SIN"],"control_ids":["sc-8"],"aws_services":["AWS Config","Amazon S3","Elastic Load Balancing","Amazon Redshift"],"evidence":"AWS Config compliance results proving data in transit is protected by TLS — S3 bucket policies denying non-TLS requests, load-balancer listeners restricted to SSL/HTTPS, and Redshift clusters requiring SSL","collection":{"kind":"config-rule","commands":[{"name":"s3-bucket-ssl-requests-only","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name s3-bucket-ssl-requests-only --compliance-types NON_COMPLIANT"},{"name":"elb-tls-https-listeners-only","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name elb-tls-https-listeners-only --compliance-types NON_COMPLIANT"},{"name":"redshift-require-tls-ssl","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name redshift-require-tls-ssl --compliance-types NON_COMPLIANT"},{"name":"configuration-recorder-status","run":"aws configservice describe-configuration-recorder-status"},{"name":"s3-bucket-ssl-requests-only-evaluation-status","run":"aws configservice describe-config-rule-evaluation-status --config-rule-names s3-bucket-ssl-requests-only"},{"name":"elb-tls-https-listeners-only-evaluation-status","run":"aws configservice describe-config-rule-evaluation-status --config-rule-names elb-tls-https-listeners-only"},{"name":"redshift-require-tls-ssl-evaluation-status","run":"aws configservice describe-config-rule-evaluation-status --config-rule-names redshift-require-tls-ssl"},{"name":"describe-regions","run":"aws ec2 describe-regions --query 'Regions[].RegionName' --output text"},{"name":"list-accounts","run":"aws organizations list-accounts --query 'Accounts[].Id' --output text"}]},"expected_output":"Three EvaluationResults arrays; an empty NON_COMPLIANT set from each rule means every evaluated S3 bucket enforces aws:SecureTransport, every Classic Load Balancer listener is SSL/HTTPS, and every Redshift cluster sets require_SSL=true. Managed rule identifiers: S3_BUCKET_SSL_REQUESTS_ONLY, ELB_TLS_HTTPS_LISTENERS_ONLY, REDSHIFT_REQUIRE_TLS_SSL Plus describe-configuration-recorder-status showing recording=true and, per rule, describe-config-rule-evaluation-status showing FirstEvaluationStarted=true with a LastSuccessfulEvaluationTime — the proof the empty set was produced by a check that ran.","cadence":"continuous","govcloud":"AWS Config and all three managed rules are available in AWS GovCloud (US); bucket, load-balancer, and cluster ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"The SC-08.01 enhancement (cryptographic protection in transit) is the TLS requirement these rules assert. ELB_TLS_HTTPS_LISTENERS_ONLY covers Classic Load Balancers only — for Application/Network Load Balancers add elbv2-acm-certificate-required and alb-http-to-https-redirection-check, and pair with acm-certificate-expiration-check so the certs terminating TLS are valid. These three cover the dominant public data paths; extend to the services you actually run (e.g. api-gw-ssl-enabled, elasticsearch-node-to-node-encryption-check). ELB_TLS_HTTPS_LISTENERS_ONLY evaluates Classic Load Balancers only and returns NOT_APPLICABLE where there is no listener, so on an ALB/NLB estate that assertion passes having examined nothing; whether the TLS that terminates is FIPS-validated is carried by the listener's SslPolicy, which no command here reads, and a human confirms it — SC-08 (01) is not claimed.","assertions":[{"field":"s3-bucket-ssl-requests-only.EvaluationResults","op":"count_eq","value":0,"controls":["sc-8"],"description":"Every evaluated S3 bucket policy denies requests without aws:SecureTransport."},{"field":"elb-tls-https-listeners-only.EvaluationResults","op":"count_eq","value":0,"controls":["sc-8"],"description":"Every Classic Load Balancer listener terminates SSL/HTTPS."},{"field":"redshift-require-tls-ssl.EvaluationResults","op":"count_eq","value":0,"controls":["sc-8"],"description":"Every Redshift cluster requires SSL."},{"field":"configuration-recorder-status.ConfigurationRecordersStatus[].recording","op":"eq","value":true,"controls":["sc-8"],"description":"The configuration recorder is on — without it an empty NON_COMPLIANT set is indistinguishable from a recorder that never ran."},{"field":"s3-bucket-ssl-requests-only-evaluation-status.ConfigRulesEvaluationStatus[].FirstEvaluationStarted","op":"eq","value":true,"controls":["sc-8"],"description":"s3-bucket-ssl-requests-only has evaluated at least once; a deployed-but-never-evaluated rule also returns an empty result."},{"field":"s3-bucket-ssl-requests-only-evaluation-status.ConfigRulesEvaluationStatus[].LastSuccessfulEvaluationTime","op":"exists","controls":["sc-8"],"description":"s3-bucket-ssl-requests-only has a successful evaluation on record."},{"field":"elb-tls-https-listeners-only-evaluation-status.ConfigRulesEvaluationStatus[].FirstEvaluationStarted","op":"eq","value":true,"controls":["sc-8"],"description":"elb-tls-https-listeners-only has evaluated at least once; a deployed-but-never-evaluated rule also returns an empty result."},{"field":"elb-tls-https-listeners-only-evaluation-status.ConfigRulesEvaluationStatus[].LastSuccessfulEvaluationTime","op":"exists","controls":["sc-8"],"description":"elb-tls-https-listeners-only has a successful evaluation on record."},{"field":"redshift-require-tls-ssl-evaluation-status.ConfigRulesEvaluationStatus[].FirstEvaluationStarted","op":"eq","value":true,"controls":["sc-8"],"description":"redshift-require-tls-ssl has evaluated at least once; a deployed-but-never-evaluated rule also returns an empty result."},{"field":"redshift-require-tls-ssl-evaluation-status.ConfigRulesEvaluationStatus[].LastSuccessfulEvaluationTime","op":"exists","controls":["sc-8"],"description":"redshift-require-tls-ssl has a successful evaluation on record."}],"scope":{"resource_types":["AWS::S3::Bucket","AWS::ElasticLoadBalancing::LoadBalancer","AWS::Redshift::Cluster","AWS::Redshift::ClusterParameterGroup"],"regions":"all-enabled","accounts":"organization","enumerated_by":["describe-regions","list-accounts"],"note":"Evaluate through an organization aggregator (describe-aggregate-compliance-by-config-rules) to cover every account and Region at once; the per-rule call shown reads one Region of one account, which is why the Region and account enumerations are part of the recipe."},"references":[{"title":"AWS Config managed rule: s3-bucket-ssl-requests-only","url":"https://docs.aws.amazon.com/config/latest/developerguide/s3-bucket-ssl-requests-only.html"},{"title":"AWS Config managed rule: elb-tls-https-listeners-only","url":"https://docs.aws.amazon.com/config/latest/developerguide/elb-tls-https-listeners-only.html"},{"title":"AWS Config managed rule: redshift-require-tls-ssl","url":"https://docs.aws.amazon.com/config/latest/developerguide/redshift-require-tls-ssl.html"}],"source":"aws","tools":["AWS Config","Amazon S3","Elastic Load Balancing","Amazon Redshift"]},{"id":"config-kms-key-management","ksi_ids":["KSI-SVC-SIN","KSI-SVC-VRI"],"control_ids":["sc-13"],"aws_services":["AWS Config","AWS KMS"],"evidence":"AWS Config compliance results proving KMS customer-managed keys are lifecycle-managed — automatic annual rotation enabled and no active key scheduled for deletion — the key-hygiene half of the cryptographic-protection control","collection":{"kind":"config-rule","commands":[{"name":"cmk-backing-key-rotation-enabled","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name cmk-backing-key-rotation-enabled --compliance-types NON_COMPLIANT"},{"name":"kms-cmk-not-scheduled-for-deletion","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name kms-cmk-not-scheduled-for-deletion --compliance-types NON_COMPLIANT"}]},"expected_output":"Two EvaluationResults arrays; empty NON_COMPLIANT sets mean automatic rotation is enabled on every eligible customer-managed CMK and no CMK protecting live data is pending deletion. Managed rule identifiers: CMK_BACKING_KEY_ROTATION_ENABLED, KMS_CMK_NOT_SCHEDULED_FOR_DELETION","cadence":"continuous","govcloud":"AWS Config and both managed rules are available in AWS GovCloud (US); KMS key ARNs use partition arn:aws-us-gov. GovCloud is served by FIPS 140-validated KMS endpoints by default","automatable":"partial","notes":"Rated partial: these rules prove key management (rotation, retention) as telemetry, but SC-13's core requirement — that the cryptographic module itself is FIPS 140-2/3 validated — is satisfied by using AWS KMS FIPS endpoints (kms-fips.<region>.amazonaws.com) and validated modules, which is a documented configuration/architecture assertion the compliance result does not itself carry. Attach the KMS FIPS endpoint usage evidence and the CMP module certificate reference alongside these results. Rotation is not supported for asymmetric/HMAC/imported-material/custom-key-store keys — scope the rotation rule accordingly.","references":[{"title":"AWS Config managed rule: cmk-backing-key-rotation-enabled","url":"https://docs.aws.amazon.com/config/latest/developerguide/cmk-backing-key-rotation-enabled.html"},{"title":"AWS Config managed rule: kms-cmk-not-scheduled-for-deletion","url":"https://docs.aws.amazon.com/config/latest/developerguide/kms-cmk-not-scheduled-for-deletion.html"},{"title":"Data protection in AWS KMS — FIPS 140-3 validated HSMs and FIPS endpoints","url":"https://docs.aws.amazon.com/kms/latest/developerguide/data-protection.html"}],"source":"aws","tools":["AWS Config","AWS KMS"]},{"id":"config-network-boundary-protection","ksi_ids":["KSI-CNA-ULN","KSI-SVC-EIS"],"control_ids":["sc-7"],"aws_services":["AWS Config","Amazon VPC","Amazon EC2"],"evidence":"AWS Config compliance results proving the network boundary is controlled — no security group exposes SSH to the internet, groups open to 0.0.0.0/0 only allow authorized ports, and every VPC's default security group denies all traffic","collection":{"kind":"config-rule","commands":[{"name":"restricted-ssh","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name restricted-ssh --compliance-types NON_COMPLIANT"},{"name":"vpc-sg-open-only-to-authorized-ports","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name vpc-sg-open-only-to-authorized-ports --compliance-types NON_COMPLIANT"},{"name":"vpc-default-security-group-closed","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name vpc-default-security-group-closed --compliance-types NON_COMPLIANT"},{"name":"configuration-recorder-status","run":"aws configservice describe-configuration-recorder-status"},{"name":"restricted-ssh-evaluation-status","run":"aws configservice describe-config-rule-evaluation-status --config-rule-names restricted-ssh"},{"name":"vpc-sg-open-only-to-authorized-ports-evaluation-status","run":"aws configservice describe-config-rule-evaluation-status --config-rule-names vpc-sg-open-only-to-authorized-ports"},{"name":"vpc-default-security-group-closed-evaluation-status","run":"aws configservice describe-config-rule-evaluation-status --config-rule-names vpc-default-security-group-closed"},{"name":"describe-regions","run":"aws ec2 describe-regions --query 'Regions[].RegionName' --output text"},{"name":"list-accounts","run":"aws organizations list-accounts --query 'Accounts[].Id' --output text"}]},"expected_output":"Three EvaluationResults arrays; empty NON_COMPLIANT sets mean no security group leaves SSH (port 22) open to 0.0.0.0/0 or ::/0, any internet-open group is limited to the authorizedTcpPorts/authorizedUdpPorts you set, and every default security group is closed. Managed rule identifiers: INCOMING_SSH_DISABLED (rule name restricted-ssh), VPC_SG_OPEN_ONLY_TO_AUTHORIZED_PORTS, VPC_DEFAULT_SECURITY_GROUP_CLOSED Plus describe-configuration-recorder-status showing recording=true and, per rule, describe-config-rule-evaluation-status showing FirstEvaluationStarted=true with a LastSuccessfulEvaluationTime — the proof the empty set was produced by a check that ran.","cadence":"continuous","govcloud":"AWS Config and all three managed rules are available in AWS GovCloud (US); VPC and security-group ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"Set VPC_SG_OPEN_ONLY_TO_AUTHORIZED_PORTS parameters (authorizedTcpPorts/authorizedUdpPorts) to your documented ingress allow-list, otherwise any 0.0.0.0/0 rule is NON_COMPLIANT. These three cover security-group ingress; for full SC-07 boundary evidence also collect NACL and subnet routing posture and, where used, restricted-common-ports and vpc-flow-logs-enabled. Note VPC_DEFAULT_SECURITY_GROUP_CLOSED may lag on deleted VPCs until the next baselining pass. These three rules read security-group ingress and nothing else; SC-07(b) — the logical separation FedRAMP's own guidance singles out — plus NACLs, route tables, gateways and VPC endpoints are a human's architecture assertion against this output. MAS-CSO-FLO is not claimed: its artifact is the enumeration of permitted connections, and a NON_COMPLIANT filter is empty on a compliant estate.","assertions":[{"field":"restricted-ssh.EvaluationResults","op":"count_eq","value":0,"controls":["sc-7"],"description":"No security group exposes port 22 to 0.0.0.0/0 or ::/0."},{"field":"vpc-sg-open-only-to-authorized-ports.EvaluationResults","op":"count_eq","value":0,"controls":["sc-7"],"description":"Any internet-open security group is limited to the ports you declared authorized."},{"field":"vpc-default-security-group-closed.EvaluationResults","op":"count_eq","value":0,"controls":["sc-7"],"description":"Every VPC default security group denies all inbound and outbound traffic."},{"field":"configuration-recorder-status.ConfigurationRecordersStatus[].recording","op":"eq","value":true,"controls":["sc-7"],"description":"The configuration recorder is on — without it an empty NON_COMPLIANT set is indistinguishable from a recorder that never ran."},{"field":"restricted-ssh-evaluation-status.ConfigRulesEvaluationStatus[].FirstEvaluationStarted","op":"eq","value":true,"controls":["sc-7"],"description":"restricted-ssh has evaluated at least once; a deployed-but-never-evaluated rule also returns an empty result."},{"field":"restricted-ssh-evaluation-status.ConfigRulesEvaluationStatus[].LastSuccessfulEvaluationTime","op":"exists","controls":["sc-7"],"description":"restricted-ssh has a successful evaluation on record."},{"field":"vpc-sg-open-only-to-authorized-ports-evaluation-status.ConfigRulesEvaluationStatus[].FirstEvaluationStarted","op":"eq","value":true,"controls":["sc-7"],"description":"vpc-sg-open-only-to-authorized-ports has evaluated at least once; a deployed-but-never-evaluated rule also returns an empty result."},{"field":"vpc-sg-open-only-to-authorized-ports-evaluation-status.ConfigRulesEvaluationStatus[].LastSuccessfulEvaluationTime","op":"exists","controls":["sc-7"],"description":"vpc-sg-open-only-to-authorized-ports has a successful evaluation on record."},{"field":"vpc-default-security-group-closed-evaluation-status.ConfigRulesEvaluationStatus[].FirstEvaluationStarted","op":"eq","value":true,"controls":["sc-7"],"description":"vpc-default-security-group-closed has evaluated at least once; a deployed-but-never-evaluated rule also returns an empty result."},{"field":"vpc-default-security-group-closed-evaluation-status.ConfigRulesEvaluationStatus[].LastSuccessfulEvaluationTime","op":"exists","controls":["sc-7"],"description":"vpc-default-security-group-closed has a successful evaluation on record."}],"scope":{"resource_types":["AWS::EC2::SecurityGroup"],"regions":"all-enabled","accounts":"organization","enumerated_by":["describe-regions","list-accounts"],"note":"Evaluate through an organization aggregator (describe-aggregate-compliance-by-config-rules) to cover every account and Region at once; the per-rule call shown reads one Region of one account, which is why the Region and account enumerations are part of the recipe."},"references":[{"title":"AWS Config managed rule: restricted-ssh (INCOMING_SSH_DISABLED)","url":"https://docs.aws.amazon.com/config/latest/developerguide/restricted-ssh.html"},{"title":"AWS Config managed rule: vpc-sg-open-only-to-authorized-ports","url":"https://docs.aws.amazon.com/config/latest/developerguide/vpc-sg-open-only-to-authorized-ports.html"},{"title":"AWS Config managed rule: vpc-default-security-group-closed","url":"https://docs.aws.amazon.com/config/latest/developerguide/vpc-default-security-group-closed.html"}],"source":"aws","tools":["AWS Config","Amazon VPC","Amazon EC2"]},{"id":"config-cloudtrail-audit-logging","ksi_ids":["KSI-MLA-OSM","KSI-MLA-LET"],"control_ids":["au-2","au-9","au-12"],"aws_services":["AWS Config","AWS CloudTrail","AWS KMS"],"evidence":"AWS Config compliance results proving the audit trail exists and is protected — CloudTrail enabled and multi-region so management events are captured account-wide, log-file validation on so records are tamper-evident, and SSE-KMS encryption on so the logs themselves are protected at rest","collection":{"kind":"config-rule","commands":[{"name":"cloudtrail-enabled","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name cloudtrail-enabled --compliance-types NON_COMPLIANT"},{"name":"multi-region-cloudtrail-enabled","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name multi-region-cloudtrail-enabled --compliance-types NON_COMPLIANT"},{"name":"cloud-trail-log-file-validation-enabled","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name cloud-trail-log-file-validation-enabled --compliance-types NON_COMPLIANT"},{"name":"cloud-trail-encryption-enabled","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name cloud-trail-encryption-enabled --compliance-types NON_COMPLIANT"},{"name":"configuration-recorder-status","run":"aws configservice describe-configuration-recorder-status"},{"name":"cloudtrail-enabled-evaluation-status","run":"aws configservice describe-config-rule-evaluation-status --config-rule-names cloudtrail-enabled"},{"name":"multi-region-cloudtrail-enabled-evaluation-status","run":"aws configservice describe-config-rule-evaluation-status --config-rule-names multi-region-cloudtrail-enabled"},{"name":"cloud-trail-log-file-validation-enabled-evaluation-status","run":"aws configservice describe-config-rule-evaluation-status --config-rule-names cloud-trail-log-file-validation-enabled"},{"name":"cloud-trail-encryption-enabled-evaluation-status","run":"aws configservice describe-config-rule-evaluation-status --config-rule-names cloud-trail-encryption-enabled"},{"name":"describe-regions","run":"aws ec2 describe-regions --query 'Regions[].RegionName' --output text"},{"name":"list-accounts","run":"aws organizations list-accounts --query 'Accounts[].Id' --output text"}]},"expected_output":"Four EvaluationResults arrays; empty NON_COMPLIANT sets mean a trail is enabled, at least one trail is multi-region, log-file validation (SHA-256 signed digest) is on, and the trail delivers SSE-KMS-encrypted logs. Managed rule identifiers: CLOUD_TRAIL_ENABLED (rule name cloudtrail-enabled), MULTI_REGION_CLOUD_TRAIL_ENABLED (rule name multi-region-cloudtrail-enabled), CLOUD_TRAIL_LOG_FILE_VALIDATION_ENABLED, CLOUD_TRAIL_ENCRYPTION_ENABLED Plus describe-configuration-recorder-status showing recording=true and, per rule, describe-config-rule-evaluation-status showing FirstEvaluationStarted=true with a LastSuccessfulEvaluationTime — the proof the empty set was produced by a check that ran.","cadence":"continuous","govcloud":"AWS Config, CloudTrail, and all four managed rules are available in AWS GovCloud (US); trail, bucket, and KMS key ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"Together these prove audit-record generation (AU-02/AU-12) and protection of the audit information — integrity via log-file validation and confidentiality via SSE-KMS (AU-09). This is the trail's existence and protection, not review of its contents: pair with the log-review/alerting recipe (MLA) for AU-06. Pass the S3/CloudWatch parameters to CLOUD_TRAIL_ENABLED to assert delivery to your specific log destination. A trail existing proves generation, not content — AU-03 is not claimed, because no Config rule reads record content. AU-02's organization-defined event types and AU-12's component list (OS, container and application audit are components) are SSP parameters a human compares this output against, and AU-09's protection from unauthorized access and deletion needs the log bucket's policy, Object Lock and MFA-delete posture, which is not collected here.","assertions":[{"field":"cloudtrail-enabled.EvaluationResults","op":"count_eq","value":0,"controls":["au-2","au-12"],"description":"A CloudTrail trail is enabled, so audit records are generated at all."},{"field":"multi-region-cloudtrail-enabled.EvaluationResults","op":"count_eq","value":0,"controls":["au-2"],"description":"At least one trail is multi-region, so the record covers management events account-wide rather than in one Region."},{"field":"cloud-trail-log-file-validation-enabled.EvaluationResults","op":"count_eq","value":0,"controls":["au-9"],"description":"Log-file validation is on, so delivered logs carry a signed digest and tampering is detectable."},{"field":"cloud-trail-encryption-enabled.EvaluationResults","op":"count_eq","value":0,"controls":["au-9"],"description":"The trail delivers SSE-KMS-encrypted logs, protecting the audit information at rest."},{"field":"configuration-recorder-status.ConfigurationRecordersStatus[].recording","op":"eq","value":true,"controls":["au-2","au-9","au-12"],"description":"The configuration recorder is on — without it an empty NON_COMPLIANT set is indistinguishable from a recorder that never ran."},{"field":"cloudtrail-enabled-evaluation-status.ConfigRulesEvaluationStatus[].FirstEvaluationStarted","op":"eq","value":true,"controls":["au-2","au-9","au-12"],"description":"cloudtrail-enabled has evaluated at least once; a deployed-but-never-evaluated rule also returns an empty result."},{"field":"cloudtrail-enabled-evaluation-status.ConfigRulesEvaluationStatus[].LastSuccessfulEvaluationTime","op":"exists","controls":["au-2","au-9","au-12"],"description":"cloudtrail-enabled has a successful evaluation on record."},{"field":"multi-region-cloudtrail-enabled-evaluation-status.ConfigRulesEvaluationStatus[].FirstEvaluationStarted","op":"eq","value":true,"controls":["au-2","au-9","au-12"],"description":"multi-region-cloudtrail-enabled has evaluated at least once; a deployed-but-never-evaluated rule also returns an empty result."},{"field":"multi-region-cloudtrail-enabled-evaluation-status.ConfigRulesEvaluationStatus[].LastSuccessfulEvaluationTime","op":"exists","controls":["au-2","au-9","au-12"],"description":"multi-region-cloudtrail-enabled has a successful evaluation on record."},{"field":"cloud-trail-log-file-validation-enabled-evaluation-status.ConfigRulesEvaluationStatus[].FirstEvaluationStarted","op":"eq","value":true,"controls":["au-2","au-9","au-12"],"description":"cloud-trail-log-file-validation-enabled has evaluated at least once; a deployed-but-never-evaluated rule also returns an empty result."},{"field":"cloud-trail-log-file-validation-enabled-evaluation-status.ConfigRulesEvaluationStatus[].LastSuccessfulEvaluationTime","op":"exists","controls":["au-2","au-9","au-12"],"description":"cloud-trail-log-file-validation-enabled has a successful evaluation on record."},{"field":"cloud-trail-encryption-enabled-evaluation-status.ConfigRulesEvaluationStatus[].FirstEvaluationStarted","op":"eq","value":true,"controls":["au-2","au-9","au-12"],"description":"cloud-trail-encryption-enabled has evaluated at least once; a deployed-but-never-evaluated rule also returns an empty result."},{"field":"cloud-trail-encryption-enabled-evaluation-status.ConfigRulesEvaluationStatus[].LastSuccessfulEvaluationTime","op":"exists","controls":["au-2","au-9","au-12"],"description":"cloud-trail-encryption-enabled has a successful evaluation on record."}],"scope":{"resource_types":["AWS::CloudTrail::Trail"],"regions":"all-enabled","accounts":"organization","enumerated_by":["describe-regions","list-accounts"],"note":"Evaluate through an organization aggregator (describe-aggregate-compliance-by-config-rules) to cover every account and Region at once; the per-rule call shown reads one Region of one account, which is why the Region and account enumerations are part of the recipe."},"references":[{"title":"AWS Config managed rule: cloudtrail-enabled (CLOUD_TRAIL_ENABLED)","url":"https://docs.aws.amazon.com/config/latest/developerguide/cloudtrail-enabled.html"},{"title":"AWS Config managed rule: multi-region-cloudtrail-enabled","url":"https://docs.aws.amazon.com/config/latest/developerguide/multi-region-cloudtrail-enabled.html"},{"title":"AWS Config managed rule: cloud-trail-log-file-validation-enabled","url":"https://docs.aws.amazon.com/config/latest/developerguide/cloud-trail-log-file-validation-enabled.html"},{"title":"AWS Config managed rule: cloud-trail-encryption-enabled","url":"https://docs.aws.amazon.com/config/latest/developerguide/cloud-trail-encryption-enabled.html"}],"source":"aws","tools":["AWS Config","AWS CloudTrail","AWS KMS"]},{"id":"config-threat-monitoring-enabled","ksi_ids":["KSI-MLA-RVL","KSI-SVC-EIS"],"control_ids":["si-4"],"aws_services":["AWS Config","Amazon GuardDuty","AWS Security Hub"],"evidence":"AWS Config compliance results proving continuous security monitoring is switched on account-wide — GuardDuty threat detection enabled (optionally centralized to a delegated admin) and Security Hub aggregating control findings","collection":{"kind":"config-rule","commands":[{"name":"guardduty-enabled-centralized","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name guardduty-enabled-centralized --compliance-types NON_COMPLIANT"},{"name":"securityhub-enabled","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name securityhub-enabled --compliance-types NON_COMPLIANT"},{"name":"configuration-recorder-status","run":"aws configservice describe-configuration-recorder-status"},{"name":"guardduty-enabled-centralized-evaluation-status","run":"aws configservice describe-config-rule-evaluation-status --config-rule-names guardduty-enabled-centralized"},{"name":"securityhub-enabled-evaluation-status","run":"aws configservice describe-config-rule-evaluation-status --config-rule-names securityhub-enabled"},{"name":"describe-regions","run":"aws ec2 describe-regions --query 'Regions[].RegionName' --output text"},{"name":"list-accounts","run":"aws organizations list-accounts --query 'Accounts[].Id' --output text"}]},"expected_output":"Two EvaluationResults arrays; empty NON_COMPLIANT sets mean GuardDuty is enabled in the account/Region (and results land in the CentralMonitoringAccount if you set one) and Security Hub is enabled. Managed rule identifiers: GUARDDUTY_ENABLED_CENTRALIZED, SECURITYHUB_ENABLED Plus describe-configuration-recorder-status showing recording=true and, per rule, describe-config-rule-evaluation-status showing FirstEvaluationStarted=true with a LastSuccessfulEvaluationTime — the proof the empty set was produced by a check that ran.","cadence":"continuous","govcloud":"AWS Config, GuardDuty, and Security Hub are available in AWS GovCloud (US); detector and hub ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"This proves the monitoring capability is ON, which is the automatable half of SI-04. Whether findings are triaged and acted on within your SLA is the review workflow — surface that with the GuardDuty finding-plus-response recipe (see guardduty-suspicious-iam-activity-response) and AU-06 log review. Set CentralMonitoringAccount to your delegated-administrator account id in a multi-account org so member accounts are evaluated against the aggregation point. Both rules are evaluated per Region, so GuardDuty disabled in any Region other than the one queried is invisible; and whether findings are analyzed and acted on — SI-04(a), (b), (d) — is a human workflow. Partial for both reasons.","assertions":[{"field":"guardduty-enabled-centralized.EvaluationResults","op":"count_eq","value":0,"controls":["si-4"],"description":"GuardDuty is enabled in the account and Region, or in the CentralMonitoringAccount when one is named — the parameter moves where the rule looks; it does not prove this account reports anywhere."},{"field":"securityhub-enabled.EvaluationResults","op":"count_eq","value":0,"controls":["si-4"],"description":"Security Hub is enabled and aggregating control findings."},{"field":"configuration-recorder-status.ConfigurationRecordersStatus[].recording","op":"eq","value":true,"controls":["si-4"],"description":"The configuration recorder is on — without it an empty NON_COMPLIANT set is indistinguishable from a recorder that never ran."},{"field":"guardduty-enabled-centralized-evaluation-status.ConfigRulesEvaluationStatus[].FirstEvaluationStarted","op":"eq","value":true,"controls":["si-4"],"description":"guardduty-enabled-centralized has evaluated at least once; a deployed-but-never-evaluated rule also returns an empty result."},{"field":"guardduty-enabled-centralized-evaluation-status.ConfigRulesEvaluationStatus[].LastSuccessfulEvaluationTime","op":"exists","controls":["si-4"],"description":"guardduty-enabled-centralized has a successful evaluation on record."},{"field":"securityhub-enabled-evaluation-status.ConfigRulesEvaluationStatus[].FirstEvaluationStarted","op":"eq","value":true,"controls":["si-4"],"description":"securityhub-enabled has evaluated at least once; a deployed-but-never-evaluated rule also returns an empty result."},{"field":"securityhub-enabled-evaluation-status.ConfigRulesEvaluationStatus[].LastSuccessfulEvaluationTime","op":"exists","controls":["si-4"],"description":"securityhub-enabled has a successful evaluation on record."}],"scope":{"resource_types":["AWS::GuardDuty::Detector","AWS::SecurityHub::Hub"],"regions":"all-enabled","accounts":"organization","enumerated_by":["describe-regions","list-accounts"],"note":"Evaluate through an organization aggregator (describe-aggregate-compliance-by-config-rules) to cover every account and Region at once; the per-rule call shown reads one Region of one account, which is why the Region and account enumerations are part of the recipe."},"references":[{"title":"AWS Config managed rule: guardduty-enabled-centralized","url":"https://docs.aws.amazon.com/config/latest/developerguide/guardduty-enabled-centralized.html"},{"title":"AWS Config managed rule: securityhub-enabled","url":"https://docs.aws.amazon.com/config/latest/developerguide/securityhub-enabled.html"}],"source":"aws","tools":["AWS Config","Amazon GuardDuty","AWS Security Hub"]},{"id":"config-data-backup-enabled","ksi_ids":["KSI-RPL-ABO"],"control_ids":["cp-9"],"aws_services":["AWS Config","Amazon RDS","AWS Backup"],"evidence":"AWS Config compliance results proving system data is backed up on a defined schedule — RDS automated backups enabled with a minimum retention, and AWS Backup plans meeting a minimum frequency and retention","collection":{"kind":"config-rule","commands":[{"name":"db-instance-backup-enabled","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name db-instance-backup-enabled --compliance-types NON_COMPLIANT"},{"name":"backup-plan-min-frequency-and-min-retention-check","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name backup-plan-min-frequency-and-min-retention-check --compliance-types NON_COMPLIANT"},{"name":"configuration-recorder-status","run":"aws configservice describe-configuration-recorder-status"},{"name":"db-instance-backup-enabled-evaluation-status","run":"aws configservice describe-config-rule-evaluation-status --config-rule-names db-instance-backup-enabled"},{"name":"backup-plan-min-frequency-and-min-retention-check-evaluation-status","run":"aws configservice describe-config-rule-evaluation-status --config-rule-names backup-plan-min-frequency-and-min-retention-check"},{"name":"describe-regions","run":"aws ec2 describe-regions --query 'Regions[].RegionName' --output text"},{"name":"list-accounts","run":"aws organizations list-accounts --query 'Accounts[].Id' --output text"}]},"expected_output":"Two EvaluationResults arrays; empty NON_COMPLIANT sets mean every RDS DB instance has automated backups within your backupRetentionMinimum and every AWS Backup plan meets requiredFrequencyValue/requiredRetentionDays. Managed rule identifiers: DB_INSTANCE_BACKUP_ENABLED, BACKUP_PLAN_MIN_FREQUENCY_AND_MIN_RETENTION_CHECK Plus describe-configuration-recorder-status showing recording=true and, per rule, describe-config-rule-evaluation-status showing FirstEvaluationStarted=true with a LastSuccessfulEvaluationTime — the proof the empty set was produced by a check that ran.","cadence":"continuous","govcloud":"AWS Config, RDS, and AWS Backup are available in AWS GovCloud (US); DB and backup-plan ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"Set db-instance-backup-enabled's backupRetentionMinimum and backup-plan's requiredFrequencyValue/requiredFrequencyUnit/requiredRetentionDays to your CP-09 policy (defaults are 1/days and 35 days). These two cover RDS and AWS Backup-managed resources; for the other stores you run, add dynamodb-pitr-enabled, elasticache-redis-cluster-automatic-backup-check, and s3 versioning/replication as applicable. Backup existence is telemetry; the restore test that proves recoverability is CP-10's separate exercise — do not present a backup config as a successful restore. The frequency and retention that carry CP-09's organization-defined values live in the rule parameters, not in the output, so a human confirms they match the SSP; system documentation under CP-09(c) is never AWS telemetry.","assertions":[{"field":"db-instance-backup-enabled.EvaluationResults","op":"count_eq","value":0,"controls":["cp-9"],"description":"Every RDS DB instance has automated backups meeting your retention minimum."},{"field":"backup-plan-min-frequency-and-min-retention-check.EvaluationResults","op":"count_eq","value":0,"controls":["cp-9"],"description":"Every AWS Backup plan meets your required frequency and retention."},{"field":"configuration-recorder-status.ConfigurationRecordersStatus[].recording","op":"eq","value":true,"controls":["cp-9"],"description":"The configuration recorder is on — without it an empty NON_COMPLIANT set is indistinguishable from a recorder that never ran."},{"field":"db-instance-backup-enabled-evaluation-status.ConfigRulesEvaluationStatus[].FirstEvaluationStarted","op":"eq","value":true,"controls":["cp-9"],"description":"db-instance-backup-enabled has evaluated at least once; a deployed-but-never-evaluated rule also returns an empty result."},{"field":"db-instance-backup-enabled-evaluation-status.ConfigRulesEvaluationStatus[].LastSuccessfulEvaluationTime","op":"exists","controls":["cp-9"],"description":"db-instance-backup-enabled has a successful evaluation on record."},{"field":"backup-plan-min-frequency-and-min-retention-check-evaluation-status.ConfigRulesEvaluationStatus[].FirstEvaluationStarted","op":"eq","value":true,"controls":["cp-9"],"description":"backup-plan-min-frequency-and-min-retention-check has evaluated at least once; a deployed-but-never-evaluated rule also returns an empty result."},{"field":"backup-plan-min-frequency-and-min-retention-check-evaluation-status.ConfigRulesEvaluationStatus[].LastSuccessfulEvaluationTime","op":"exists","controls":["cp-9"],"description":"backup-plan-min-frequency-and-min-retention-check has a successful evaluation on record."}],"scope":{"resource_types":["AWS::RDS::DBInstance","AWS::Backup::BackupPlan"],"regions":"all-enabled","accounts":"organization","enumerated_by":["describe-regions","list-accounts"],"note":"Evaluate through an organization aggregator (describe-aggregate-compliance-by-config-rules) to cover every account and Region at once; the per-rule call shown reads one Region of one account, which is why the Region and account enumerations are part of the recipe."},"references":[{"title":"AWS Config managed rule: db-instance-backup-enabled","url":"https://docs.aws.amazon.com/config/latest/developerguide/db-instance-backup-enabled.html"},{"title":"AWS Config managed rule: backup-plan-min-frequency-and-min-retention-check","url":"https://docs.aws.amazon.com/config/latest/developerguide/backup-plan-min-frequency-and-min-retention-check.html"}],"source":"aws","tools":["AWS Config","Amazon RDS","AWS Backup"]},{"id":"ssm-configuration-baseline-enforced","ksi_ids":["KSI-SVC-ACM","KSI-CMT-RMV","KSI-MLA-EVC","KSI-CNA-IBP"],"control_ids":["cm-2","cm-6"],"aws_services":["AWS Config","AWS Systems Manager"],"evidence":"AWS Config compliance results plus the State Manager association list proving a defined configuration is actually applied and re-applied to every managed node — instances are under SSM management, and the associations that carry your baseline report COMPLIANT on a schedule rather than drifting","collection":{"kind":"cli","commands":[{"name":"ec2-instance-managed-by-systems-manager","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name ec2-instance-managed-by-systems-manager --compliance-types NON_COMPLIANT"},{"name":"ec2-managedinstance-association-compliance-status-check","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name ec2-managedinstance-association-compliance-status-check --compliance-types NON_COMPLIANT"},{"name":"list-associations","run":"aws ssm list-associations --query 'Associations[].{Name:Name,AssociationId:AssociationId,Schedule:ScheduleExpression,Targets:Targets,Status:Overview.Status,LastRun:LastExecutionDate}'"}]},"expected_output":"Two EvaluationResults arrays plus an Associations list. Empty NON_COMPLIANT sets mean every running EC2 instance has a running SSM Agent and every SSM association compliance record reads COMPLIANT after execution; the Associations list names the documents, schedules, and targets that carry the baseline, with Overview.Status and LastExecutionDate showing it ran. Managed rule identifiers: EC2_INSTANCE_MANAGED_BY_SSM (rule name ec2-instance-managed-by-systems-manager), EC2_MANAGEDINSTANCE_ASSOCIATION_COMPLIANCE_STATUS_CHECK","cadence":"continuous","govcloud":"AWS Config and Systems Manager are available in AWS GovCloud (US); instance, document, and association ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"The telemetry proves a configuration is being enforced and drift corrected — it does not prove the enforced content IS your approved baseline. That the association's SSM document encodes the hardened settings you baselined (CIS/STIG content, approved through your change process) is the human judgement half; keep the document version and its approval record alongside this output. Two limits to state plainly: EC2_INSTANCE_MANAGED_BY_SSM does not flag a stopped instance whose agent is running, and this whole recipe is EC2-only — container images, Lambda, and managed-service settings need their own baseline evidence. CM-08 inventory is a separate recipe, not this one.","references":[{"title":"AWS Config managed rule: ec2-instance-managed-by-systems-manager","url":"https://docs.aws.amazon.com/config/latest/developerguide/ec2-instance-managed-by-systems-manager.html"},{"title":"AWS Config managed rule: ec2-managedinstance-association-compliance-status-check","url":"https://docs.aws.amazon.com/config/latest/developerguide/ec2-managedinstance-association-compliance-status-check.html"},{"title":"AWS Systems Manager State Manager (associations define the state to maintain)","url":"https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-state.html"},{"title":"AWS CLI: ssm list-associations","url":"https://docs.aws.amazon.com/cli/latest/reference/ssm/list-associations.html"}],"source":"aws","tools":["AWS Config","AWS Systems Manager"]},{"id":"patch-and-vulnerability-remediation","ksi_ids":["KSI-CMT-VTD","KSI-SCR-MON"],"control_ids":["si-2","ra-5"],"aws_services":["AWS Config","AWS Systems Manager Patch Manager","Amazon Inspector"],"evidence":"Patch Manager compliance state plus Amazon Inspector scan status and coverage — proving flaws are being found continuously (Inspector enabled and actually covering your resources) and that the fixes landed (per-node missing/failed patch counts and the time of the last scan or install)","collection":{"kind":"cli","commands":[{"name":"ec2-managedinstance-patch-compliance-status-check","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name ec2-managedinstance-patch-compliance-status-check --compliance-types NON_COMPLIANT"},{"name":"describe-instance-patch-states","run":"aws ssm describe-instance-patch-states --instance-ids i-0123456789abcdef0 --query 'InstancePatchStates[].{Node:InstanceId,Baseline:BaselineId,Missing:MissingCount,Failed:FailedCount,CriticalNonCompliant:CriticalNonCompliantCount,SecurityNonCompliant:SecurityNonCompliantCount,Operation:Operation,EndTime:OperationEndTime}'"},{"name":"batch-get-account-status","run":"aws inspector2 batch-get-account-status --account-ids <ACCOUNT_ID>"},{"name":"list-coverage","run":"aws inspector2 list-coverage --filter-criteria '{\"resourceType\":[{\"comparison\":\"EQUALS\",\"value\":\"AWS_EC2_INSTANCE\"}]}'"}]},"expected_output":"An EvaluationResults array with an empty NON_COMPLIANT set (every SSM patch-compliance record reads COMPLIANT), InstancePatchStates showing MissingCount/FailedCount/CriticalNonCompliantCount/SecurityNonCompliantCount at zero with a recent OperationEndTime, an account status whose resourceState.ec2/ecr/lambda read ENABLED, and coveredResources whose scanStatus.statusCode is ACTIVE with a recent lastScannedAt. Managed rule identifier: EC2_MANAGEDINSTANCE_PATCH_COMPLIANCE_STATUS_CHECK","cadence":"daily","govcloud":"AWS Config, Systems Manager, and Amazon Inspector are available in AWS GovCloud (US-East) and (US-West); instance and finding ARNs use partition arn:aws-us-gov. Two GovCloud differences to record: Lambda code scanning is not available, and the Inspector plugin for Linux deep inspection is not FIPS compliant.","automatable":"partial","notes":"This proves flaws are detected and shows exactly what is still missing and when patching last ran — it does not prove the remediation clock was met. Whether an open finding sits inside your SI-02 timeframe, or carries an approved deviation or POA&M entry, is a judgement joined against your risk-acceptance record, not an API result. Substitute your real instance ids and account id; describe-instance-patch-states requires --instance-ids (use describe-instance-patch-states-for-patch-group to sweep a patch group). Note also that patch compliance data is a point-in-time snapshot and each successful scan overwrites the previous one, so capture the output at collection time rather than reconstructing history later.","references":[{"title":"AWS Config managed rule: ec2-managedinstance-patch-compliance-status-check","url":"https://docs.aws.amazon.com/config/latest/developerguide/ec2-managedinstance-patch-compliance-status-check.html"},{"title":"AWS CLI: ssm describe-instance-patch-states","url":"https://docs.aws.amazon.com/cli/latest/reference/ssm/describe-instance-patch-states.html"},{"title":"Systems Manager Patch Manager: working with patch compliance reports","url":"https://docs.aws.amazon.com/systems-manager/latest/userguide/patch-manager-compliance-reports.html"},{"title":"AWS CLI: inspector2 batch-get-account-status","url":"https://docs.aws.amazon.com/cli/latest/reference/inspector2/batch-get-account-status.html"},{"title":"AWS CLI: inspector2 list-coverage","url":"https://docs.aws.amazon.com/cli/latest/reference/inspector2/list-coverage.html"},{"title":"Amazon Inspector in AWS GovCloud (US)","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-inspector2.html"}],"source":"aws","tools":["AWS Config","AWS Systems Manager Patch Manager","Amazon Inspector"]},{"id":"cloudwatch-log-review-alerting","ksi_ids":["KSI-MLA-RVL"],"control_ids":["au-6","au-6.1"],"aws_services":["Amazon CloudWatch Logs","Amazon CloudWatch","AWS Config"],"evidence":"The metric filters that turn audit log events into metrics, the alarms built on them, and Config's confirmation that those alarms actually notify someone — the automated-mechanism half of audit review","collection":{"kind":"cli","commands":[{"name":"describe-metric-filters","run":"aws logs describe-metric-filters --query 'metricFilters[].{Filter:filterName,LogGroup:logGroupName,Pattern:filterPattern,Metric:metricTransformations[0].metricName}'"},{"name":"describe-alarms","run":"aws cloudwatch describe-alarms --alarm-types MetricAlarm CompositeAlarm --query 'MetricAlarms[].{Name:AlarmName,Metric:MetricName,State:StateValue,Actions:AlarmActions}'"},{"name":"cloudwatch-alarm-action-check","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name cloudwatch-alarm-action-check --compliance-types NON_COMPLIANT"}]},"expected_output":"A metricFilters list showing each audit pattern you watch for and the metric it publishes, a matching alarm for each with a non-empty AlarmActions (the SNS topic or response action it fires), and an empty NON_COMPLIANT EvaluationResults set meaning no alarm is configured without an action. Managed rule identifier: CLOUDWATCH_ALARM_ACTION_CHECK","cadence":"weekly","govcloud":"CloudWatch, CloudWatch Logs, and AWS Config are available in AWS GovCloud (US); log-group, alarm, and SNS topic ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"AU-06 asks that audit records be reviewed and analysed and findings reported; AU-06.01 asks that the review use automated mechanisms. What these calls prove is the automated half — the pipeline exists, it fires, and it reaches a human. No API proves a person read the alert, judged it, and reported the finding, so pair this with your ticket or case record; do not present an alarm list as a completed review. Metric filters only publish for events after the filter was created and are supported only on Standard-class log groups, so a filter created yesterday says nothing about last month. cloudwatch-alarm-action-check defaults to requiring an ALARM and INSUFFICIENT_DATA action; pass action1..action5 to assert alarms route to a specific SNS topic. Trail existence and protection is the separate cloudtrail recipe (AU-02/03/09/12).","references":[{"title":"Creating metrics from log events using filters (CloudWatch Logs metric filters)","url":"https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/MonitoringLogData.html"},{"title":"AWS CLI: logs describe-metric-filters","url":"https://docs.aws.amazon.com/cli/latest/reference/logs/describe-metric-filters.html"},{"title":"AWS CLI: cloudwatch describe-alarms","url":"https://docs.aws.amazon.com/cli/latest/reference/cloudwatch/describe-alarms.html"},{"title":"AWS Config managed rule: cloudwatch-alarm-action-check","url":"https://docs.aws.amazon.com/config/latest/developerguide/cloudwatch-alarm-action-check.html"}],"source":"aws","tools":["Amazon CloudWatch Logs","Amazon CloudWatch","AWS Config"]},{"id":"config-asset-inventory","ksi_ids":["KSI-PIY-GIV","KSI-CMT-RMV","KSI-SVC-ACM"],"requirement_ids":["MAS-CSO-IIR"],"control_ids":["cm-2.2","cm-8","cm-8.1"],"aws_services":["AWS Config","AWS Systems Manager Inventory"],"evidence":"The machine-maintained component inventory — Config's recorder status and discovered-resource counts proving supported resources are tracked continuously and the list stays current without anyone editing a spreadsheet, plus Systems Manager Inventory's node and installed-application metadata for what runs inside them","collection":{"kind":"cli","commands":[{"name":"configuration-recorder-status","run":"aws configservice describe-configuration-recorder-status --query 'ConfigurationRecordersStatus[].{Name:name,Recording:recording,LastStatus:lastStatus,LastStart:lastStartTime,Error:lastErrorMessage}'"},{"name":"get-discovered-resource-counts","run":"aws configservice get-discovered-resource-counts"},{"name":"select-resource-config","run":"aws configservice select-resource-config --expression \"SELECT resourceId, resourceType, awsRegion WHERE resourceType = 'AWS::EC2::Instance'\""},{"name":"get-inventory","run":"aws ssm get-inventory --aggregators Expression=AWS:InstanceInformation.PlatformType"},{"name":"list-inventory-entries","run":"aws ssm list-inventory-entries --instance-id i-0123456789abcdef0 --type-name AWS:Application"}]},"expected_output":"A recorder status with recording true and lastStatus SUCCESS — read this first, because a stopped or failing recorder makes everything below stale — then a resourceCounts array giving a count per resourceType alongside totalDiscoveredResources, a Results list naming each recorded resource of the type you queried, an aggregation of managed nodes grouped by platform, and an Entries list of installed applications stamped with the CaptureTime they were collected.","cadence":"daily","govcloud":"AWS Config and Systems Manager Inventory are available in AWS GovCloud (US-East) and (US-West); resource and node ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"This proves the inventory is machine-maintained and current (CM-08.01, and the automated-currency half of CM-02.02) — it does not prove the inventory is complete. Config sees only supported resource types, only in the regions and accounts where a recorder runs, and only within the recording group you configured; unsupported types, an un-recorded region, on-premises hosts, SaaS components and in-container software are invisible here and need their own source. SSM Inventory covers only managed nodes with a running agent and an inventory association, collects no more often than every 30 minutes, and the console's Inventory cards hide stopped and terminated nodes even though the API still returns them. The accountability attributes CM-08 asks for — system owner, function, criticality — live in your tags or CMDB, not in a resource count, so join them before calling this an inventory. Substitute your real instance id. Detecting unauthorized components (CM-08.03) is a different question; the prohibited-software half is in the least-functionality recipe.","references":[{"title":"AWS CLI: configservice describe-configuration-recorder-status","url":"https://docs.aws.amazon.com/cli/latest/reference/configservice/describe-configuration-recorder-status.html"},{"title":"AWS CLI: configservice get-discovered-resource-counts","url":"https://docs.aws.amazon.com/cli/latest/reference/configservice/get-discovered-resource-counts.html"},{"title":"AWS CLI: configservice select-resource-config","url":"https://docs.aws.amazon.com/cli/latest/reference/configservice/select-resource-config.html"},{"title":"AWS Config advanced query: SELECT query components","url":"https://docs.aws.amazon.com/config/latest/developerguide/query-components.html"},{"title":"AWS Systems Manager Inventory","url":"https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-inventory.html"},{"title":"AWS CLI: ssm get-inventory","url":"https://docs.aws.amazon.com/cli/latest/reference/ssm/get-inventory.html"},{"title":"AWS CLI: ssm list-inventory-entries","url":"https://docs.aws.amazon.com/cli/latest/reference/ssm/list-inventory-entries.html"}],"source":"aws","tools":["AWS Config","AWS Systems Manager Inventory"]},{"id":"cloudtrail-config-change-history","ksi_ids":["KSI-CMT-LMC","KSI-CMT-RVP","KSI-CMT-RMV","KSI-CMT-VTD"],"control_ids":["cm-3"],"aws_services":["AWS CloudTrail","AWS Config","AWS Systems Manager Change Manager"],"evidence":"Who changed what, when, and from which state to which — CloudTrail's record of every mutating API call and Config's per-resource configuration history, plus, where Change Manager is in use, the change-request executions that carry the approval","collection":{"kind":"cli","commands":[{"name":"lookup-events-false","run":"aws cloudtrail lookup-events --lookup-attributes AttributeKey=ReadOnly,AttributeValue=false --start-time <START_TIME> --end-time 2026-07-31T23:59:59Z --query 'Events[].{Event:EventName,Time:EventTime,User:Username,Source:EventSource,Resources:Resources}'"},{"name":"get-resource-config-history","run":"aws configservice get-resource-config-history --resource-type AWS::EC2::SecurityGroup --resource-id sg-0123456789abcdef0 --chronological-order Reverse"},{"name":"describe-automation-executions","run":"aws ssm describe-automation-executions --filters Key=AutomationSubtype,Values=ChangeRequest --query 'AutomationExecutionMetadataList[].{Id:AutomationExecutionId,Document:DocumentName,Status:AutomationExecutionStatus,Mode:Mode,By:ExecutedBy,Start:ExecutionStartTime,End:ExecutionEndTime}'"}]},"expected_output":"An Events list in which each entry names the EventName, EventTime, Username, EventSource and the resources touched, with CloudTrailEvent carrying the full request parameters; a configurationItems list ordered newest-first whose configurationItemCaptureTime and configurationStateId let you diff the resource across the change and whose relationships show what else it touched; and, where Change Manager applies, an AutomationExecutionMetadataList of AutomationSubtype ChangeRequest showing the runbook, Mode, ExecutedBy and the start and end of the workflow.","cadence":"weekly","govcloud":"CloudTrail and AWS Config are available in AWS GovCloud (US); trail, resource, and automation ARNs use partition arn:aws-us-gov. Systems Manager Change Manager is NOT available in the AWS GovCloud (US) Regions — drop the third command there and evidence approvals from your own change-management system instead.","automatable":"partial","notes":"CloudTrail and Config prove a change was recorded and is reconstructable — actor, time, parameters, and the before/after configuration state — which is CM-03's record-retention and monitoring half. They do not prove the change was proposed, reviewed and approved before it happened; that decision lives in your CCB or ticketing system and has to be joined by ticket or change id. State the retention limits plainly rather than implying full history: lookup-events reaches back only 90 days and returns management (and Insights) events only, so anything older must be read from the trail's S3 objects or a CloudTrail Lake event data store, and get-resource-config-history honours your Config retention period (30 days minimum, up to 7 years) but each call spans at most 7 days, so a long window needs paging by time. Substitute your real resource type, resource id and time window. Change Manager has also been closed to new customers since 2025-11-07, so treat that command as available only if you were already signed up. Whether the post-change configuration is still compliant is a different question answered by the Config rule recipes.","references":[{"title":"AWS CLI: cloudtrail lookup-events (90-day lookup window)","url":"https://docs.aws.amazon.com/cli/latest/reference/cloudtrail/lookup-events.html"},{"title":"AWS CLI: configservice get-resource-config-history","url":"https://docs.aws.amazon.com/cli/latest/reference/configservice/get-resource-config-history.html"},{"title":"AWS Systems Manager Change Manager (change templates, approvals, auditing)","url":"https://docs.aws.amazon.com/systems-manager/latest/userguide/change-manager.html"},{"title":"AWS CLI: ssm describe-automation-executions (AutomationSubtype filter)","url":"https://docs.aws.amazon.com/cli/latest/reference/ssm/describe-automation-executions.html"},{"title":"AWS Systems Manager in AWS GovCloud (US) — Change Manager not available","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-ssm.html"}],"source":"aws","tools":["AWS CloudTrail","AWS Config","AWS Systems Manager Change Manager"]},{"id":"config-least-functionality","ksi_ids":["KSI-CMT-RMV","KSI-SVC-EIS","KSI-CNA-RNT"],"control_ids":["cm-7","cm-7.1"],"aws_services":["AWS Config","AWS Systems Manager Inventory","Amazon EC2"],"evidence":"Config compliance results proving the ports you declared unnecessary are not reachable from the internet and the software you declared prohibited is not installed, plus the actual installed-application set a periodic review has to read","collection":{"kind":"cli","commands":[{"name":"restricted-common-ports","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name restricted-common-ports --compliance-types NON_COMPLIANT"},{"name":"ec2-managedinstance-applications-blacklisted","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name ec2-managedinstance-applications-blacklisted --compliance-types NON_COMPLIANT"},{"name":"list-inventory-entries","run":"aws ssm list-inventory-entries --instance-id i-0123456789abcdef0 --type-name AWS:Application"}]},"expected_output":"Two EvaluationResults arrays plus an inventory listing. Empty NON_COMPLIANT sets mean no security group opens a blocked TCP port to 0.0.0.0/0 or ::/0 and none of the denylisted applications is installed on any evaluated managed node; the Entries list, stamped with its CaptureTime, is the installed-software set your periodic review actually reads. Managed rule identifiers: RESTRICTED_INCOMING_TRAFFIC (rule name restricted-common-ports), EC2_MANAGEDINSTANCE_APPLICATIONS_BLACKLISTED","cadence":"monthly","govcloud":"AWS Config, both managed rules, and Systems Manager Inventory are available in AWS GovCloud (US); security-group and node ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"These prove the negatives you asserted and hand the reviewer the real installed-application set — they do not prove least functionality. That the functions, ports, protocols and services still enabled are the minimum necessary is a judgement against your documented essential-capability list, and CM-07.01's periodic review is a decision someone makes and records, not an API result; keep the review record next to this output. Both rules are only as strong as their parameters. RESTRICTED_INCOMING_TRAFFIC defaults to blocking TCP 20, 21, 3389, 3306 and 4333 — set blockedPorts to your real denylist or you are testing AWS's defaults, not your policy. EC2_MANAGEDINSTANCE_APPLICATIONS_BLACKLISTED needs exact application names (no wildcards, and the name differs per distro) and evaluates AWS::SSM::ManagedInstanceInventory, so a node with no running agent or inventory association is simply not evaluated rather than flagged — pair it with the inventory recipe's coverage check. Security-group ingress deliberately overlaps the SC-07 boundary recipe: there it proves boundary protection, here it proves unnecessary ports are closed.","references":[{"title":"AWS Config managed rule: restricted-common-ports (RESTRICTED_INCOMING_TRAFFIC)","url":"https://docs.aws.amazon.com/config/latest/developerguide/restricted-common-ports.html"},{"title":"AWS Config managed rule: ec2-managedinstance-applications-blacklisted","url":"https://docs.aws.amazon.com/config/latest/developerguide/ec2-managedinstance-applications-blacklisted.html"},{"title":"AWS CLI: ssm list-inventory-entries","url":"https://docs.aws.amazon.com/cli/latest/reference/ssm/list-inventory-entries.html"}],"source":"aws","tools":["AWS Config","AWS Systems Manager Inventory","Amazon EC2"]},{"id":"backup-restore-testing","ksi_ids":["KSI-RPL-ABO","KSI-RPL-ARP","KSI-RPL-RRO","KSI-RPL-TRC"],"control_ids":["cp-10","cp-10.2"],"aws_services":["AWS Backup","AWS Config","Amazon RDS","Amazon DynamoDB"],"evidence":"Restore jobs that actually ran — the schedule they ran on, whether each one completed, how long it took, and what it produced — plus the point-in-time recovery window that makes transaction-level recovery possible","collection":{"kind":"cli","commands":[{"name":"list-restore-testing-plans","run":"aws backup list-restore-testing-plans --query 'RestoreTestingPlans[].{Plan:RestoreTestingPlanName,Schedule:ScheduleExpression,LastRun:LastExecutionTime,StartWindowHours:StartWindowHours}'"},{"name":"list-restore-jobs","run":"aws backup list-restore-jobs --by-created-after 2026-04-01T00:00:00Z --query 'RestoreJobs[].{Id:RestoreJobId,Type:ResourceType,Status:Status,Message:StatusMessage,Created:CreationDate,Completed:CompletionDate,Validation:ValidationStatus,Restored:CreatedResourceArn}'"},{"name":"list-protected-resources","run":"aws backup list-protected-resources --query 'Results[].{Arn:ResourceArn,Type:ResourceType,LastBackup:LastBackupTime,LastRecoveryPoint:LastRecoveryPointArn}'"},{"name":"aurora-resources-protected-by-backup-plan","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name aurora-resources-protected-by-backup-plan --compliance-types NON_COMPLIANT"},{"name":"rds-in-backup-plan","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name rds-in-backup-plan --compliance-types NON_COMPLIANT"},{"name":"describe-db-instances","run":"aws rds describe-db-instances --query 'DBInstances[].{Db:DBInstanceIdentifier,RetentionDays:BackupRetentionPeriod,LatestRestorable:LatestRestorableTime}'"},{"name":"dynamodb-pitr-enabled","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name dynamodb-pitr-enabled --compliance-types NON_COMPLIANT"}]},"expected_output":"A RestoreTestingPlans list whose ScheduleExpression is the cron your recovery policy claims and whose LastExecutionTime proves the schedule is actually firing; a RestoreJobs list in which each job carries a Status of COMPLETED (the other states are PENDING, RUNNING, ABORTED and FAILED, with StatusMessage explaining a failure), a CreationDate and CompletionDate whose difference is the measured restore time, a CreatedResourceArn naming what was rebuilt, and a ValidationStatus of SUCCESSFUL, FAILED, TIMED_OUT or VALIDATING only where you wired validation; a Results list of protected resources each stamped with LastBackupTime; two empty NON_COMPLIANT sets; and a DB instance list whose LatestRestorableTime — the most recent point a PITR restore can target — sits within minutes of now, bounded by BackupRetentionPeriod days of history. Managed rule identifiers: AURORA_RESOURCES_PROTECTED_BY_BACKUP_PLAN, RDS_IN_BACKUP_PLAN, DYNAMODB_PITR_ENABLED","cadence":"monthly","govcloud":"AWS Backup, AWS Config, RDS and DynamoDB are available in AWS GovCloud (US-East) and (US-West); resource, plan and recovery-point ARNs use partition arn:aws-us-gov. Restore testing is NOT available in AWS GovCloud (US) — drop the first command there, run your restore exercises on demand with backup start-restore-job, and evidence them from the same list-restore-jobs output (the jobs simply will not carry a restore testing plan ARN). Backup Audit Manager multi-account, multi-Region reporting is also unavailable, and cross-Region copy works only between the two GovCloud Regions, never in or out of them.","automatable":"partial","notes":"A completed restore job is the strongest recovery evidence AWS emits, and it is still not the whole of CP-10. It proves a recovery point could be materialised into a resource and how long that took; it does not prove the restored system is functionally correct, that the data inside it is intact, or that the reconstitution ran to a known operational state. ValidationStatus does not close that gap either — restore testing validation is an EventBridge-triggered workflow you write, and PutRestoreValidationResult records whatever your own Lambda asserted, so treat it as your test result, not AWS's verdict (and note it cannot be set from the console, and once set it cannot be changed). RTO and RPO conformance is arithmetic you perform, not a field: compare CompletionDate minus CreationDate against your RTO, and the gap between LastBackupTime (or LatestRestorableTime) and the incident against your RPO. Restore testing also deletes the restored resource when the retention window closes, tagging it awsbackup-restore-test, so there is no lasting artifact beyond the job record — capture the output at collection time. Scope caveats: restore testing supports a fixed resource-type list (Aurora, DocumentDB, DynamoDB, EBS, EC2, EFS, FSx, Neptune, RDS, S3), rds-in-backup-plan evaluates AWS::RDS::DBInstance and AWS's own documentation notes it applies only to Aurora DB instances with clusters unsupported (aurora-resources-protected-by-backup-plan is the DB-cluster rule), and aurora-resources-protected-by-backup-plan is unavailable in a handful of Regions, none of them GovCloud. Backup existence is the CP-09 recipe's job; this recipe deliberately starts where that one stops.","references":[{"title":"AWS Backup: Restore testing (plans, jobs, validation window, resource types)","url":"https://docs.aws.amazon.com/aws-backup/latest/devguide/restore-testing.html"},{"title":"AWS Backup: Restore testing validation (PutRestoreValidationResult)","url":"https://docs.aws.amazon.com/aws-backup/latest/devguide/restore-testing-validation.html"},{"title":"AWS CLI: backup list-restore-testing-plans","url":"https://docs.aws.amazon.com/cli/latest/reference/backup/list-restore-testing-plans.html"},{"title":"AWS CLI: backup list-restore-jobs (status and validation fields)","url":"https://docs.aws.amazon.com/cli/latest/reference/backup/list-restore-jobs.html"},{"title":"AWS CLI: backup list-protected-resources","url":"https://docs.aws.amazon.com/cli/latest/reference/backup/list-protected-resources.html"},{"title":"AWS Config managed rule: aurora-resources-protected-by-backup-plan","url":"https://docs.aws.amazon.com/config/latest/developerguide/aurora-resources-protected-by-backup-plan.html"},{"title":"AWS Config managed rule: rds-in-backup-plan","url":"https://docs.aws.amazon.com/config/latest/developerguide/rds-in-backup-plan.html"},{"title":"AWS Config managed rule: dynamodb-pitr-enabled","url":"https://docs.aws.amazon.com/config/latest/developerguide/dynamodb-pitr-enabled.html"},{"title":"AWS CLI: rds describe-db-instances (LatestRestorableTime, BackupRetentionPeriod)","url":"https://docs.aws.amazon.com/cli/latest/reference/rds/describe-db-instances.html"},{"title":"AWS Backup in AWS GovCloud (US) — restore testing not available","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-bkp.html"}],"source":"aws","tools":["AWS Backup","AWS Config","Amazon RDS","Amazon DynamoDB"]},{"id":"audit-log-retention-and-delivery-failure","ksi_ids":["KSI-MLA-OSM"],"control_ids":["au-4","au-5","au-11"],"aws_services":["Amazon CloudWatch Logs","Amazon S3","AWS CloudTrail","AWS Config","Amazon CloudWatch"],"evidence":"How long audit records are kept, how much space they occupy, and whether the pipeline that delivers them is currently failing — retention settings on every log group and log bucket, the storage they consume, and the trail's own delivery-error fields","collection":{"kind":"cli","commands":[{"name":"describe-log-groups","run":"aws logs describe-log-groups --query 'logGroups[].{Group:logGroupName,RetentionDays:retentionInDays,StoredBytes:storedBytes,Class:logGroupClass}'"},{"name":"cw-loggroup-retention-period-check","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name cw-loggroup-retention-period-check --compliance-types NON_COMPLIANT"},{"name":"get-bucket-lifecycle-configuration","run":"aws s3api get-bucket-lifecycle-configuration --bucket <LOG_BUCKET>"},{"name":"s3-lifecycle-policy-check","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name s3-lifecycle-policy-check --compliance-types NON_COMPLIANT"},{"name":"get-metric-statistics","run":"aws cloudwatch get-metric-statistics --namespace AWS/S3 --metric-name BucketSizeBytes --dimensions Name=BucketName,Value=<LOG_BUCKET> Name=StorageType,Value=StandardStorage --start-time <START_TIME> --end-time <END_TIME> --period 86400 --statistics Average"},{"name":"get-trail-status","run":"aws cloudtrail get-trail-status --name <TRAIL_NAME> --query '{Logging:IsLogging,LastDelivery:LatestDeliveryTime,DeliveryError:LatestDeliveryError,DigestDelivery:LatestDigestDeliveryTime,DigestError:LatestDigestDeliveryError,NotificationError:LatestNotificationError}'"},{"name":"cloud-trail-cloud-watch-logs-enabled","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name cloud-trail-cloud-watch-logs-enabled --compliance-types NON_COMPLIANT"}]},"expected_output":"A log-group list in which every group carries the retentionInDays your policy requires and a storedBytes you can trend — a group with no retentionInDays at all never expires; an empty NON_COMPLIANT set from the retention rule, meaning no group falls below MinRetentionTime (default 365 days); a Rules array on the log bucket with an Enabled rule whose Transitions and Expiration match your archive and deletion policy, or a NoSuchLifecycleConfiguration error if none is set; a daily BucketSizeBytes series showing the log store's growth curve against whatever headroom you provisioned; and a trail status with IsLogging true, a LatestDeliveryTime within the last few minutes, and LatestDeliveryError, LatestDigestDeliveryError and LatestNotificationError all absent — a populated error field is the audit-logging-process failure AU-05 is about. Managed rule identifiers: CW_LOGGROUP_RETENTION_PERIOD_CHECK, S3_LIFECYCLE_POLICY_CHECK, CLOUD_TRAIL_CLOUD_WATCH_LOGS_ENABLED","cadence":"daily","govcloud":"CloudWatch Logs, Amazon S3, CloudTrail, AWS Config and CloudWatch metrics are all available in AWS GovCloud (US); log-group, bucket and trail ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"This is the capacity and durability of the audit pipeline, not proof that it is adequate. AU-04 asks that storage capacity be sufficient for your defined requirement — S3 has no fixed ceiling to report, so the honest evidence is the consumption trend plus the retention and lifecycle settings that bound it, and the judgement that the headroom is enough stays yours. AU-05 is answered here only in its detection half: get-trail-status surfaces the delivery failure, and the alerting and the real-time response — notify these people, shut down or overwrite oldest records — is process, with the alarm-side telemetry living in the AU-06 log-review recipe (CLOUDWATCH_ALARM_ACTION_CHECK). AU-11 is the retention setting, which these commands read directly, but conformance to your record-retention period is a comparison against policy: note that cw-loggroup-retention-period-check marks a Never-expire group COMPLIANT, so an unbounded group passes the rule while potentially violating a maximum-retention or data-disposal requirement — read the raw retentionInDays, not just the rule verdict. Substitute your real bucket, trail and window; run get-trail-status per trail (it is a single-trail call) and against the trail's home Region. BucketSizeBytes is a once-daily storage metric and CloudWatch metric delivery is best-effort, so a missing data point is not by itself an incident. Pass expectedDeliveryWindowAge to cloud-trail-cloud-watch-logs-enabled if you want the rule to fail on stale delivery rather than only on an unconfigured CloudWatch Logs destination; the trail's existence, integrity and encryption are the AU-02/03/09/12 recipe's job.","references":[{"title":"AWS CLI: logs describe-log-groups (retentionInDays, storedBytes, logGroupClass)","url":"https://docs.aws.amazon.com/cli/latest/reference/logs/describe-log-groups.html"},{"title":"AWS Config managed rule: cw-loggroup-retention-period-check","url":"https://docs.aws.amazon.com/config/latest/developerguide/cw-loggroup-retention-period-check.html"},{"title":"AWS CLI: s3api get-bucket-lifecycle-configuration","url":"https://docs.aws.amazon.com/cli/latest/reference/s3api/get-bucket-lifecycle-configuration.html"},{"title":"AWS Config managed rule: s3-lifecycle-policy-check","url":"https://docs.aws.amazon.com/config/latest/developerguide/s3-lifecycle-policy-check.html"},{"title":"Amazon S3 CloudWatch metrics and dimensions (BucketSizeBytes, daily storage metrics)","url":"https://docs.aws.amazon.com/AmazonS3/latest/userguide/metrics-dimensions.html"},{"title":"AWS CLI: cloudtrail get-trail-status (LatestDeliveryError, LatestDigestDeliveryError)","url":"https://docs.aws.amazon.com/cli/latest/reference/cloudtrail/get-trail-status.html"},{"title":"AWS Config managed rule: cloud-trail-cloud-watch-logs-enabled (expectedDeliveryWindowAge)","url":"https://docs.aws.amazon.com/config/latest/developerguide/cloud-trail-cloud-watch-logs-enabled.html"}],"source":"aws","tools":["Amazon CloudWatch Logs","Amazon S3","AWS CloudTrail","AWS Config","Amazon CloudWatch"]},{"id":"boundary-access-points-and-default-deny","ksi_ids":["KSI-CNA-MAT","KSI-CNA-RNT"],"requirement_ids":["MAS-CSO-FLO"],"control_ids":["sc-7.3","sc-7.4","sc-7.5"],"aws_services":["Amazon VPC","Amazon EC2","AWS Network Firewall","AWS Config"],"evidence":"Every route in or out of the boundary, named and counted — internet gateways, NAT gateways, VPC endpoints and Site-to-Site VPN tunnels — alongside what each boundary device does with traffic that matched no rule: the network ACL entries, the closed default security group, subnets that hand out public IPs, and the firewall policy's stateless and stateful default actions","collection":{"kind":"cli","commands":[{"name":"describe-internet-gateways","run":"aws ec2 describe-internet-gateways --query 'InternetGateways[].{Igw:InternetGatewayId,Attachments:Attachments[].{Vpc:VpcId,State:State}}'"},{"name":"describe-nat-gateways","run":"aws ec2 describe-nat-gateways --query 'NatGateways[].{Nat:NatGatewayId,State:State,Vpc:VpcId,Subnet:SubnetId,Connectivity:ConnectivityType}'"},{"name":"describe-vpc-endpoints","run":"aws ec2 describe-vpc-endpoints --query 'VpcEndpoints[].{Endpoint:VpcEndpointId,Type:VpcEndpointType,Service:ServiceName,Vpc:VpcId,State:State}'"},{"name":"describe-vpn-connections","run":"aws ec2 describe-vpn-connections --query 'VpnConnections[].{Vpn:VpnConnectionId,State:State,Category:Category,Tunnels:VgwTelemetry[].{Ip:OutsideIpAddress,Status:Status,Changed:LastStatusChange}}'"},{"name":"describe-network-acls","run":"aws ec2 describe-network-acls --query 'NetworkAcls[].{Acl:NetworkAclId,Default:IsDefault,Vpc:VpcId,Entries:Entries[].{Rule:RuleNumber,Action:RuleAction,Egress:Egress,Cidr:CidrBlock,Protocol:Protocol}}'"},{"name":"describe-firewall-policy","run":"aws network-firewall describe-firewall-policy --firewall-policy-name <FIREWALL_POLICY> --query 'FirewallPolicy.{Stateless:StatelessDefaultActions,Fragments:StatelessFragmentDefaultActions,Stateful:StatefulDefaultActions}'"},{"name":"vpc-default-security-group-closed","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name vpc-default-security-group-closed --compliance-types NON_COMPLIANT"},{"name":"subnet-auto-assign-public-ip-disabled","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name subnet-auto-assign-public-ip-disabled --compliance-types NON_COMPLIANT"},{"name":"netfw-policy-default-action-full-packets","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name netfw-policy-default-action-full-packets --compliance-types NON_COMPLIANT"}]},"expected_output":"A short, closed list of access points you can put next to your documented boundary: one internet gateway per VPC that is meant to have one and none attached to a VPC that is not, NAT gateways whose ConnectivityType matches whether that subnet is supposed to reach the internet at all, VPC endpoints that keep service traffic off the internet, and VPN connections whose VgwTelemetry shows both tunnels UP with a LastStatusChange that is not flapping. On the deny side: network ACL Entries in which no allow rule opens a CIDR wider than your documented exceptions, an empty NON_COMPLIANT set from vpc-default-security-group-closed meaning every VPC's default security group carries no inbound or outbound rule at all, an empty set from subnet-auto-assign-public-ip-disabled meaning no subnet hands instances a public address on launch, and a firewall policy whose StatelessDefaultActions and StatelessFragmentDefaultActions are aws:drop (or aws:forward_to_sfe into a stateful engine whose StatefulDefaultActions is aws:drop_strict) rather than aws:pass. Managed rule identifiers: VPC_DEFAULT_SECURITY_GROUP_CLOSED, SUBNET_AUTO_ASSIGN_PUBLIC_IP_DISABLED, NETFW_POLICY_DEFAULT_ACTION_FULL_PACKETS, NETFW_POLICY_DEFAULT_ACTION_FRAGMENT_PACKETS","cadence":"weekly","govcloud":"Amazon VPC, Amazon EC2, AWS Config and AWS Network Firewall are all available in AWS GovCloud (US) — the GovCloud user guide records no differences for Network Firewall — but the two Network Firewall managed rules (NETFW_POLICY_DEFAULT_ACTION_FULL_PACKETS and NETFW_POLICY_DEFAULT_ACTION_FRAGMENT_PACKETS) are explicitly unavailable in AWS GovCloud (US-East) and AWS GovCloud (US-West), so read the default actions straight from describe-firewall-policy there and drop the rule call; gateway, endpoint and firewall ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"The inventory is telemetry; the limit is policy. SC-07.03 asks that external connections be held to the minimum needed and each one routed through a managed interface — these calls enumerate every gateway, endpoint and tunnel exactly, but whether that count is the minimum is a comparison against your documented architecture, and nothing in the API tells you a gateway is unnecessary. SC-07.04 is the weakest half here: describe-vpn-connections proves the tunnels exist and are up, but the control also wants each external telecommunications interface documented with its business need and traffic-flow policy, exceptions reviewed and removed — that record is a document, not a call. This recipe also does not cover carrier links terminated outside these APIs (AWS Direct Connect connections, Transit Gateway peering to another network); enumerate those separately if you use them. SC-07.05 is the closest to full: the default-deny posture of security groups, network ACLs and the firewall policy is directly readable, and vpc-default-security-group-closed is a clean pass/fail. Read the ACL Entries yourself rather than trusting a rule verdict — an allow entry with a low rule number can shadow everything below it, and no managed rule scores ordering. Substitute your real firewall policy name; describe-firewall-policy is one policy per call. Security-group ingress is covered by the SC-07 recipe (restricted-ssh, vpc-sg-open-only-to-authorized-ports) and not repeated here.","references":[{"title":"AWS CLI: ec2 describe-internet-gateways","url":"https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-internet-gateways.html"},{"title":"AWS CLI: ec2 describe-nat-gateways (ConnectivityType)","url":"https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-nat-gateways.html"},{"title":"AWS CLI: ec2 describe-vpc-endpoints","url":"https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-vpc-endpoints.html"},{"title":"AWS CLI: ec2 describe-vpn-connections (VgwTelemetry tunnel status)","url":"https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-vpn-connections.html"},{"title":"AWS CLI: ec2 describe-network-acls (Entries, RuleAction)","url":"https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-network-acls.html"},{"title":"AWS CLI: network-firewall describe-firewall-policy (StatelessDefaultActions)","url":"https://docs.aws.amazon.com/cli/latest/reference/network-firewall/describe-firewall-policy.html"},{"title":"AWS Config managed rule: vpc-default-security-group-closed","url":"https://docs.aws.amazon.com/config/latest/developerguide/vpc-default-security-group-closed.html"},{"title":"AWS Config managed rule: subnet-auto-assign-public-ip-disabled","url":"https://docs.aws.amazon.com/config/latest/developerguide/subnet-auto-assign-public-ip-disabled.html"},{"title":"AWS Config managed rule: netfw-policy-default-action-full-packets (not available in GovCloud)","url":"https://docs.aws.amazon.com/config/latest/developerguide/netfw-policy-default-action-full-packets.html"},{"title":"AWS Config managed rule: netfw-policy-default-action-fragment-packets (not available in GovCloud)","url":"https://docs.aws.amazon.com/config/latest/developerguide/netfw-policy-default-action-fragment-packets.html"},{"title":"AWS Network Firewall in AWS GovCloud (US)","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-nf.html"}],"source":"aws","tools":["Amazon VPC","Amazon EC2","AWS Network Firewall","AWS Config"]},{"id":"ddos-protection-and-rate-limiting","ksi_ids":["KSI-CNA-RVP"],"control_ids":["sc-5"],"aws_services":["AWS Shield Advanced","AWS WAF","Amazon CloudWatch","AWS Config"],"evidence":"The denial-of-service defences that are actually attached to the internet-facing resources — the Shield Advanced subscription and the list of resources it protects, the web ACL's rate-based rules and their limits, whether web ACL logging is on — plus what those defences observed: the attacks Shield recorded over the period and the CloudWatch detection and block counts underneath them","collection":{"kind":"cli","commands":[{"name":"describe-subscription","run":"aws shield describe-subscription --region us-east-1 --query 'Subscription.{Start:StartTime,End:EndTime,AutoRenew:AutoRenew,ProactiveEngagement:ProactiveEngagementStatus}'"},{"name":"list-protections","run":"aws shield list-protections --region us-east-1 --query 'Protections[].{Name:Name,Resource:ResourceArn,AutoAppLayerResponse:ApplicationLayerAutomaticResponseConfiguration.Status}'"},{"name":"list-attacks","run":"aws shield list-attacks --region us-east-1 --start-time FromInclusive=2026-04-27T00:00:00Z,ToExclusive=2026-07-27T00:00:00Z --query 'AttackSummaries[].{Attack:AttackId,Resource:ResourceArn,Start:StartTime,End:EndTime,Vectors:AttackVectors}'"},{"name":"list-web-acls","run":"aws wafv2 list-web-acls --scope REGIONAL --query 'WebACLs[].{Name:Name,Id:Id,Arn:ARN}'"},{"name":"get-web-acl","run":"aws wafv2 get-web-acl --name <WEB_ACL_NAME> --scope REGIONAL --id <WEB_ACL_ID> --query 'WebACL.{Default:DefaultAction,Rules:Rules[].{Name:Name,Priority:Priority,Action:Action,RateBased:Statement.RateBasedStatement}}'"},{"name":"wafv2-logging-enabled","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name wafv2-logging-enabled --compliance-types NON_COMPLIANT"},{"name":"ddos-detected-metric","run":"aws cloudwatch get-metric-statistics --namespace AWS/DDoSProtection --metric-name DDoSDetected --dimensions Name=ResourceArn,Value=<PROTECTED_RESOURCE_ARN> --start-time <START_TIME> --end-time <END_TIME> --period 3600 --statistics Maximum"},{"name":"waf-blocked-requests-metric","run":"aws cloudwatch get-metric-statistics --namespace AWS/WAFV2 --metric-name BlockedRequests --dimensions Name=WebACL,Value=<WEB_ACL_NAME> Name=Rule,Value=<RATE_LIMIT_RULE_NAME> Name=Region,Value=<REGION> --start-time <START_TIME> --end-time <END_TIME> --period 3600 --statistics Sum"}]},"expected_output":"A Shield Advanced subscription whose EndTime is in the future and AutoRenew is ENABLED, and a Protections list that names every internet-facing resource in your boundary — a resource missing from that list has Shield Standard only and no protection record to show an assessor. A web ACL whose Rules include at least one RateBasedStatement carrying the Limit, EvaluationWindowSec and aggregation you documented, with a Block action rather than Count, and a DefaultAction you meant. An empty NON_COMPLIANT set from wafv2-logging-enabled, meaning every web ACL is logging (and, if you passed KinesisFirehoseDeliveryStreamArns, to the destination you named). Then the observation half: an AttackSummaries array — often empty, which is itself the finding for a quiet quarter — and CloudWatch series in which DDoSDetected stays at 0 except during events, and BlockedRequests shows the rate-based rule doing work. Managed rule identifiers: WAFV2_LOGGING_ENABLED, SHIELD_ADVANCED_ENABLED_AUTORENEW. Namespaces: AWS/DDoSProtection (DDoSDetected, DDoSAttackBitsPerSecond, DDoSAttackPacketsPerSecond, DDoSAttackRequestsPerSecond), AWS/WAFV2 (AllowedRequests, BlockedRequests, CountedRequests)","cadence":"daily","govcloud":"AWS WAF is available in both AWS GovCloud (US) Regions, with one documented difference: only AWS-provided managed rule groups are usable — AWS Marketplace third-party rule groups are not. Shield is the awkward one: the Shield Response Team documentation states the SRT serves customers in AWS GovCloud (US-East) and (US-West), but the AWS General Reference lists a single Shield endpoint, shield.us-east-1.amazonaws.com, with no GovCloud entry, and the shield-advanced-enabled-autorenew managed rule is documented as available only in US East (N. Virginia) — so run the aws shield calls and that Config rule against us-east-1 and confirm your own account's GovCloud Shield coverage before you claim it. Web ACL and load-balancer ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"This proves the mechanism, not the outcome. SC-5 asks that denial-of-service attacks be protected against or their effects limited: the configuration calls prove rate limiting and Shield are attached to the right resources, and list-attacks plus the DDoSProtection metrics prove detection and mitigation fired — but 'the effect on availability was limited' is a judgement you make against your own application health, error rates and capacity headroom, which are not in this output. Two honest gaps. Shield Standard, which protects every AWS customer automatically, has no API and no record to collect; if you are not subscribed to Shield Advanced there is nothing here to show beyond the WAF half, and describe-subscription simply errors. And the rate limit itself is a number you chose: a RateBasedStatement set far above real traffic passes every check while limiting nothing, so put the documented threshold next to the configured Limit. Note also that Shield Advanced reports metrics once a minute during an event but only once a day when nothing is happening, so a sparse series is normal and a missing data point is not an outage; and that engaging the SRT requires a Business or Enterprise Support plan. list-attacks covers the window you pass — keep the collected output, since the API's own history is not your retention policy.","references":[{"title":"AWS CLI: shield describe-subscription (AutoRenew, ProactiveEngagementStatus)","url":"https://docs.aws.amazon.com/cli/latest/reference/shield/describe-subscription.html"},{"title":"AWS CLI: shield list-protections","url":"https://docs.aws.amazon.com/cli/latest/reference/shield/list-protections.html"},{"title":"AWS CLI: shield list-attacks (AttackSummaries, AttackVectors)","url":"https://docs.aws.amazon.com/cli/latest/reference/shield/list-attacks.html"},{"title":"AWS CLI: wafv2 list-web-acls","url":"https://docs.aws.amazon.com/cli/latest/reference/wafv2/list-web-acls.html"},{"title":"AWS CLI: wafv2 get-web-acl (Rules, DefaultAction)","url":"https://docs.aws.amazon.com/cli/latest/reference/wafv2/get-web-acl.html"},{"title":"AWS WAF: using rate-based rule statements","url":"https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-type-rate-based.html"},{"title":"AWS Config managed rule: wafv2-logging-enabled","url":"https://docs.aws.amazon.com/config/latest/developerguide/wafv2-logging-enabled.html"},{"title":"AWS Config managed rule: shield-advanced-enabled-autorenew (us-east-1 only)","url":"https://docs.aws.amazon.com/config/latest/developerguide/shield-advanced-enabled-autorenew.html"},{"title":"AWS Shield Advanced metrics (AWS/DDoSProtection namespace)","url":"https://docs.aws.amazon.com/waf/latest/developerguide/shield-metrics.html"},{"title":"AWS WAF metrics and dimensions (AWS/WAFV2 namespace)","url":"https://docs.aws.amazon.com/waf/latest/developerguide/waf-metrics.html"},{"title":"Managed DDoS event response with the Shield Response Team (GovCloud Regions)","url":"https://docs.aws.amazon.com/waf/latest/developerguide/ddos-srt-support.html"},{"title":"AWS Shield Advanced endpoints and quotas","url":"https://docs.aws.amazon.com/general/latest/gr/shield.html"},{"title":"AWS WAF in AWS GovCloud (US)","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-waf.html"}],"source":"aws","tools":["AWS Shield Advanced","AWS WAF","Amazon CloudWatch","AWS Config"]},{"id":"clock-synchronization-and-timestamps","ksi_ids":["KSI-MLA-OSM"],"control_ids":["au-8"],"aws_services":["Amazon EC2","AWS Systems Manager","AWS CloudTrail"],"evidence":"What each instance's clock is actually locked to and how far off it is right now — the chrony daemon's reference source, offset and leap status collected fleet-wide through Run Command — together with the configured time source in chrony.conf and the UTC time stamps CloudTrail already writes on every audit record","collection":{"kind":"cli","commands":[{"name":"send-command","run":"aws ssm send-command --document-name AWS-RunShellScript --targets Key=tag:Environment,Values=<ENVIRONMENT_TAG> --parameters 'commands=[\"chronyc tracking\",\"chronyc sources -v | grep -F ^*\",\"grep -E \\\"^(server|pool|refclock)\\\" /etc/chrony.conf\"]' --output-s3-bucket-name <EVIDENCE_BUCKET> --output-s3-key-prefix au-08"},{"name":"list-command-invocations","run":"aws ssm list-command-invocations --command-id 11111111-2222-3333-4444-555555555555 --details --query 'CommandInvocations[].{Instance:InstanceId,Status:Status,Output:CommandPlugins[].Output}'"},{"name":"lookup-events","run":"aws cloudtrail lookup-events --max-results 5 --query 'Events[].{Name:EventName,Time:EventTime,Source:EventSource}'"}]},"expected_output":"For every managed instance, a chronyc tracking block whose Reference ID is A9FEA97B (169.254.169.123) — the local Amazon Time Sync Service — with System time within your documented granularity of NTP time (microseconds on a healthy instance), a small RMS offset, and Leap status Normal; a chronyc sources line beginning ^*, which marks the preferred source, pointing at 169.254.169.123 (or fd00:ec2::123 on a Nitro instance using the IPv6 endpoint); and a chrony.conf that names that endpoint and no unapproved public NTP pool. Every invocation should come back Status Success — an instance that returns nothing is an instance whose clock you have not evidenced. From CloudTrail, EventTime values in UTC, which is what the record format guarantees: eventTime is documented as 'the date and time the request was completed, in coordinated universal time (UTC)'.","cadence":"daily","govcloud":"The local Amazon Time Sync Service is reachable from any AWS Region, GovCloud included, at the link-local addresses 169.254.169.123 (IPv4) and fd00:ec2::123 (IPv6, Nitro instances only), and Systems Manager and CloudTrail are both available in AWS GovCloud (US); instance, document and trail ARNs use partition arn:aws-us-gov. One difference to plan around: precision time placement groups — the route to the enhanced Amazon Time Sync Service and the PTP hardware clock — are documented as available in all AWS Commercial Regions, so expect NTP-level accuracy rather than microsecond PHC accuracy in GovCloud","automatable":"partial","notes":"The clock is telemetry; the mapping and the granularity are not. AU-8 wants time stamps for audit records that use an internal clock mapped to UTC and meet a granularity you defined — chrony proves the host clock is disciplined to an authoritative UTC source and by how much it is off, and CloudTrail's eventTime is UTC by contract, so the AWS-generated half of your audit trail satisfies the mapping without any work of yours. What no command proves is that your own application writes its records from that clock in UTC: chrony can be flawless while code stamps local time or truncates to the second, and comparing the measured offset against your defined granularity is a judgement against a policy number. Scope honesty matters here too — this is a point-in-time sample of the instances Run Command could reach, so an unmanaged, stopped or unreachable instance is silently absent; join the results against your managed-instance inventory before calling the fleet covered, and remember containers and serverless compute have no chrony to query. If you claim sub-millisecond granularity, note that a PTP hardware clock passes no error bound to chrony (chrony then assumes an error bound of 0): read /sys/bus/pci/devices/<pci-slot>/phc_error_bound and add it, or run ClockBound. Leap seconds are smeared on the NTP endpoints but not on the PHC, so do not configure both smeared and non-smeared sources. Substitute your real targets, bucket and command id; send-command returns the CommandId that the second call consumes, and plugin Output is truncated at 2500 characters, which is why the S3 output bucket is worth setting.","references":[{"title":"Amazon EC2: precision clock and time synchronization (Amazon Time Sync Service, leap smearing)","url":"https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/set-time.html"},{"title":"Amazon EC2: set the time reference to the local Amazon Time Sync Service (169.254.169.123, fd00:ec2::123, chronyc tracking)","url":"https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configure-ec2-ntp.html"},{"title":"Amazon EC2: compare timestamps with ClockBound (clock error bound, phc_error_bound)","url":"https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/compare-timestamps-with-clockbound.html"},{"title":"AWS CLI: ssm send-command (AWS-RunShellScript)","url":"https://docs.aws.amazon.com/cli/latest/reference/ssm/send-command.html"},{"title":"AWS CLI: ssm list-command-invocations (--details, CommandPlugins Output)","url":"https://docs.aws.amazon.com/cli/latest/reference/ssm/list-command-invocations.html"},{"title":"CloudTrail record contents: eventTime is recorded in UTC","url":"https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference-record-contents.html"},{"title":"AWS CLI: cloudtrail lookup-events (90-day window)","url":"https://docs.aws.amazon.com/cli/latest/reference/cloudtrail/lookup-events.html"}],"source":"aws","tools":["Amazon EC2","AWS Systems Manager","AWS CloudTrail"]},{"id":"integrity-verification-and-immutability","ksi_ids":["KSI-SVC-VRI","KSI-SVC-VCM","KSI-SCR-MIT"],"control_ids":["si-7","si-7.1"],"aws_services":["AWS CloudTrail","AWS Config","Amazon S3","AWS Backup","Amazon ECR"],"evidence":"Cryptographic proof that the audit trail CloudTrail delivered has not been altered or deleted, plus the compliance state of the write-once controls that make stored records and container images tamper-evident","collection":{"kind":"cli","commands":[{"name":"cloud-trail-log-file-validation-enabled","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name cloud-trail-log-file-validation-enabled --compliance-types NON_COMPLIANT"},{"name":"validate-logs","run":"aws cloudtrail validate-logs --trail-arn <TRAIL_ARN> --start-time <START_TIME> --verbose"},{"name":"s3-bucket-default-lock-enabled","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name s3-bucket-default-lock-enabled --compliance-types NON_COMPLIANT"},{"name":"describe-backup-vault","run":"aws backup describe-backup-vault --backup-vault-name evidence-vault --query '{Locked:Locked,LockDate:LockDate,MinRetentionDays:MinRetentionDays,MaxRetentionDays:MaxRetentionDays}'"},{"name":"ecr-private-tag-immutability-enabled","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name ecr-private-tag-immutability-enabled --compliance-types NON_COMPLIANT"}]},"expected_output":"Three EvaluationResults arrays, a validation run and a vault description. Empty NON_COMPLIANT sets mean every trail signs digest files, every evaluated bucket has Object Lock on by default and every private ECR repository refuses to move a tag. validate-logs prints the window it actually found and two ratios — for example '3/3 digest files valid' and '15/15 log files valid'; any shortfall names the file. describe-backup-vault returns Locked true with a LockDate, the UTC instant the compliance-mode grace time ends. Managed rule identifiers: CLOUD_TRAIL_LOG_FILE_VALIDATION_ENABLED, S3_BUCKET_DEFAULT_LOCK_ENABLED, ECR_PRIVATE_TAG_IMMUTABILITY_ENABLED","cadence":"weekly","govcloud":"All five calls work in AWS GovCloud (US): none of the three managed rules names a GovCloud Region in its exclusion list, and AWS Backup Vault Lock is documented among the features offered for all supported resources with no Region carve-out — unlike restore testing and logically air-gapped vaults, which are blank for both GovCloud rows in the feature-availability table. Trail, bucket, vault and repository ARNs use partition arn:aws-us-gov, and because CloudTrail uses a different key pair per Region, validate the logs in the Region that produced them","automatable":"partial","notes":"The strong claim here is narrow and worth stating precisely. validate-logs is real cryptography — SHA-256 hashing with SHA-256/RSA signing, an hourly digest file that references the last hour's log files and carries the signature of the previous digest — so a clean run positively asserts that the delivered log files were not modified or deleted, and can even assert that no log files were delivered in a window you believed was empty. What it will not do: validate files you moved, since they must stay where CloudTrail put them; and it cannot report tampering across a gap — disable validation for an hour and no digest exists for that hour, so the chain simply breaks. Enabling the feature is not the same as checking it, which is why both the Config rule and the CLI run belong here: the rule proves digests are being produced, the run is the only thing that verifies them. Object Lock and Vault Lock are prevention, not detection — they make a deletion fail rather than proving none happened, and Object Lock only counts if the mode and period match your policy: the rule's optional mode parameter is what pins GOVERNANCE versus COMPLIANCE, and unset it passes either. Vault Lock in governance mode can be removed by anyone holding the IAM permission, so read Locked together with LockDate — before that date even a compliance-mode lock is still removable. The honest gap is the host: SI-7 asks for integrity verification of software, firmware and information, and nothing above watches a filesystem. ECR tag immutability stops a tag being repointed at a different image but says nothing about drift inside a running instance; file integrity monitoring is third-party or self-built on AWS, and SI-7(1)'s ‘defined frequency’ is a policy number you compare against, not an API result. Finally, a NON_COMPLIANT-only query returns an empty array on success and says nothing about resources Config never evaluated — join it against recorder coverage before reading emptiness as compliance.","references":[{"title":"AWS Config managed rule: cloud-trail-log-file-validation-enabled (CLOUD_TRAIL_LOG_FILE_VALIDATION_ENABLED)","url":"https://docs.aws.amazon.com/config/latest/developerguide/cloud-trail-log-file-validation-enabled.html"},{"title":"CloudTrail: validating log file integrity (SHA-256, SHA-256 with RSA, hourly digest files, per-Region key pairs)","url":"https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-log-file-validation-intro.html"},{"title":"AWS CLI: cloudtrail validate-logs (syntax, output ratios, documented limitations)","url":"https://docs.aws.amazon.com/cli/latest/reference/cloudtrail/validate-logs.html"},{"title":"AWS Config managed rule: s3-bucket-default-lock-enabled (optional mode parameter GOVERNANCE|COMPLIANCE)","url":"https://docs.aws.amazon.com/config/latest/developerguide/s3-bucket-default-lock-enabled.html"},{"title":"AWS Backup Vault Lock (governance vs compliance mode, grace time, DescribeBackupVault Locked and LockDate)","url":"https://docs.aws.amazon.com/aws-backup/latest/devguide/vault-lock.html"},{"title":"AWS Backup feature availability (Vault Lock offered for all supported resources; GovCloud rows for restore testing and air-gapped vaults)","url":"https://docs.aws.amazon.com/aws-backup/latest/devguide/backup-feature-availability.html"},{"title":"AWS Config managed rule: ecr-private-tag-immutability-enabled","url":"https://docs.aws.amazon.com/config/latest/developerguide/ecr-private-tag-immutability-enabled.html"}],"source":"aws","tools":["AWS CloudTrail","AWS Config","Amazon S3","AWS Backup","Amazon ECR"]},{"id":"unauthorized-component-detection-and-response","ksi_ids":["KSI-CMT-LMC","KSI-SVC-VRI"],"control_ids":["cm-8.3"],"aws_services":["AWS Config","AWS Systems Manager Automation","AWS Systems Manager Inventory","Amazon EC2"],"evidence":"Every running instance built from an image you never approved and every node carrying denylisted software, together with proof that an automated action was configured for those findings and a record of what it did when one fired","collection":{"kind":"cli","commands":[{"name":"approved-amis-by-tag","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name approved-amis-by-tag --compliance-types NON_COMPLIANT"},{"name":"ec2-managedinstance-applications-blacklisted","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name ec2-managedinstance-applications-blacklisted --compliance-types NON_COMPLIANT"},{"name":"approved-amis-by-tag-remediation","run":"aws configservice describe-remediation-configurations --config-rule-names approved-amis-by-tag ec2-managedinstance-applications-blacklisted"},{"name":"describe-remediation-execution-status","run":"aws configservice describe-remediation-execution-status --config-rule-name approved-amis-by-tag"}]},"expected_output":"Two EvaluationResults arrays naming each unauthorized component by resource id, then the response half: RemediationConfigurations showing TargetType SSM_DOCUMENT, the TargetId document and version, Automatic true or false, MaximumAutomaticAttempts and RetryAttemptSeconds; and RemediationExecutionStatuses with State QUEUED, IN_PROGRESS, SUCCEEDED, FAILED or UNKNOWN plus per-step StepDetails, InvocationTime and LastUpdatedTime. An empty RemediationConfigurations list is the finding: detection with no configured action does not satisfy CM-8(3)b. Managed rule identifiers: APPROVED_AMIS_BY_TAG, EC2_MANAGEDINSTANCE_APPLICATIONS_BLACKLISTED","cadence":"continuous","govcloud":"AWS Config and both managed rules are available in AWS GovCloud (US) — neither rule's Region list excludes a GovCloud Region — and config.us-gov-east-1 and config.us-gov-west-1 are both listed in the Region-support table for Config remediation actions. But the GovCloud user guide states flatly that AWS Systems Manager documents (SSM documents) for AWS Config remediation actions are not available, so expect to author your own SSM Automation document rather than attach an AWS-managed one, and confirm the document resolves in your Region before you claim the automated half. Instance and document ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"CM-8(3) has two halves and only one comes free. Detection is genuine: APPROVED_AMIS_BY_TAG is configuration-change triggered, so an instance launched from an unapproved image is flagged as it appears rather than at the next sweep, and the remediation records are real evidence of action — TargetId names the document that ran, Automatic separates auto-remediation from a button a human pressed, and StepDetails timestamps each step and quotes the error when one fails. What telemetry cannot supply is the definition of ‘unauthorized’. APPROVED_AMIS_BY_TAG matches on up to ten AMI tag keys or key:value pairs that you assert mean approved, so it tests your tagging discipline as much as your fleet — tag an unvetted image and it becomes compliant. The applications denylist needs exact application names with no wildcards, and the name differs per distribution; it evaluates AWS::SSM::ManagedInstanceInventory, so a node with no agent or no inventory association is simply not evaluated rather than flagged — read it beside the inventory-coverage recipe or you will mistake blindness for cleanliness. Neither rule sees firmware, and neither sees what a container image runs. The response side also carries a judgement someone must record: CM-8(3)b wants a chosen action — disable network access, isolate the component, notify defined personnel — and choosing to isolate a production instance automatically is a risk decision, not a default. Deliberate overlap: EC2_MANAGEDINSTANCE_APPLICATIONS_BLACKLISTED also carries CM-7 and CM-7.01 in the least-functionality recipe, where it proves prohibited software is absent; here the same signal is read as detection of an unauthorized component, with the remediation record attached.","references":[{"title":"AWS Config managed rule: approved-amis-by-tag (APPROVED_AMIS_BY_TAG, amisByTagKeyAndValue, up to 10 tags)","url":"https://docs.aws.amazon.com/config/latest/developerguide/approved-amis-by-tag.html"},{"title":"AWS Config managed rule: ec2-managedinstance-applications-blacklisted","url":"https://docs.aws.amazon.com/config/latest/developerguide/ec2-managedinstance-applications-blacklisted.html"},{"title":"Remediating noncompliant resources with AWS Config (SSM Automation documents; Region support includes both GovCloud Regions)","url":"https://docs.aws.amazon.com/config/latest/developerguide/remediation.html"},{"title":"AWS CLI: configservice describe-remediation-configurations (TargetId, TargetType, Automatic, MaximumAutomaticAttempts)","url":"https://docs.aws.amazon.com/cli/latest/reference/configservice/describe-remediation-configurations.html"},{"title":"AWS CLI: configservice describe-remediation-execution-status (State, StepDetails, InvocationTime)","url":"https://docs.aws.amazon.com/cli/latest/reference/configservice/describe-remediation-execution-status.html"},{"title":"AWS Config in AWS GovCloud (US) — differences (SSM documents for Config remediation actions not available)","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-config.html"}],"source":"aws","tools":["AWS Config","AWS Systems Manager Automation","AWS Systems Manager Inventory","Amazon EC2"]},{"id":"information-location-and-classification","ksi_ids":["KSI-PIY-GIV","KSI-SVC-EIS"],"requirement_ids":["MAS-CSO-IIR","MAS-CSO-MDI"],"control_ids":["cm-12","cm-12.1"],"aws_services":["AWS Config","AWS Resource Groups Tagging API","Amazon Macie","Amazon S3"],"evidence":"A Region-by-Region inventory of the resources that can hold information, the classification tags you asserted on them, and — where Macie exists — a sampled machine judgement about which S3 buckets actually contain sensitive data","collection":{"kind":"cli","commands":[{"name":"select-aggregate-resource-config","run":"aws configservice select-aggregate-resource-config --configuration-aggregator-name <ORG_AGGREGATOR> --expression \"SELECT awsRegion, resourceType, COUNT(*) WHERE resourceType IN ('AWS::S3::Bucket', 'AWS::RDS::DBInstance', 'AWS::DynamoDB::Table', 'AWS::EFS::FileSystem') GROUP BY awsRegion, resourceType\""},{"name":"get-resources","run":"aws resourcegroupstaggingapi get-resources --tag-filters Key=DataClassification --region us-gov-west-1"},{"name":"get-automated-discovery-configuration","run":"aws macie2 get-automated-discovery-configuration"},{"name":"describe-buckets","run":"aws macie2 describe-buckets --query 'buckets[].{bucket:bucketName,region:region,score:sensitivityScore,monitored:automatedDiscoveryMonitoringStatus,lastAnalyzed:lastAutomatedDiscoveryTime,unclassifiable:unclassifiableObjectCount.total}'"}]},"expected_output":"A per-Region, per-type count of information-bearing resources across every account in the aggregator — aggregation queries page at 500 rows by default and plain SELECTs at 25, so page or raise --max-results before treating a result set as the whole estate. Then a ResourceTagMappingList of ARNs carrying your DataClassification key, one Region per call. Then Macie's status ENABLED or DISABLED with firstEnabledAt, lastUpdatedAt, classificationScopeId and sensitivityInspectionTemplateId; and per bucket a sensitivityScore — documented as -1 for a classification error, 1 for an empty bucket, 50 for a bucket excluded from recent analyses, up to 100 for sensitive — alongside automatedDiscoveryMonitoringStatus MONITORED or NOT_MONITORED, lastAutomatedDiscoveryTime and the unclassifiable object count","cadence":"monthly","govcloud":"Amazon Macie is not available in AWS GovCloud (US): the AWS General Reference lists no macie2 endpoint for us-gov-east-1 or us-gov-west-1, and the macie-status-check Config rule is excluded from both GovCloud Regions. In GovCloud the last two commands have nothing to call and CM-12(1)'s automated identification by information type needs another tool. The first two do work — AWS Config in both Regions, and the Resource Groups Tagging API at tagging.us-gov-east-1.amazonaws.com and tagging.us-gov-west-1.amazonaws.com — but Config in GovCloud does not record third-party or custom resource types, so anything you model that way is invisible to the aggregate query. ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"Three different qualities of evidence are stacked here, and conflating them is the trap. The Config aggregate query is solid on where storage lives — resource type by Region, across accounts — and that is the part of CM-12 most often undocumented. The tag query is only as true as your tagging: GetResources by design never returns untagged resources, so an unclassified bucket is absent from the answer rather than flagged, which is precisely backwards for an inventory control; run it beside the aggregate count and treat the difference as your unclassified population. Macie is the only machine-derived opinion about information type, and it is a sample rather than a census — automated sensitive data discovery continually selects representative objects from your buckets and scores each bucket from those, so a MONITORED bucket with a low score means ‘nothing sensitive in what was sampled’, never ‘no sensitive data here’. The unclassifiable object count is the population Macie could not read at all because of storage class or file format, and per-file size quotas mean a large archive can be skipped entirely, so read coverage before reading scores. Macie also only looks at S3: nothing above inspects an RDS table, an EFS volume, a DynamoDB item or a Parameter Store value, and the Region field tells you where a bucket is, which is the CM-12 question, not what is in it. What no command produces is CM-12 itself — the documented location of each information type, the users authorized to access it and the purpose it is held for. That is a record you write and then check against this telemetry, not one you derive from it.","references":[{"title":"AWS CLI: configservice select-aggregate-resource-config (aggregator queries, 500 vs 25 default page size)","url":"https://docs.aws.amazon.com/cli/latest/reference/configservice/select-aggregate-resource-config.html"},{"title":"AWS CLI: resourcegroupstaggingapi get-resources (per-Region; does not return untagged resources)","url":"https://docs.aws.amazon.com/cli/latest/reference/resourcegroupstaggingapi/get-resources.html"},{"title":"Amazon Macie: performing automated sensitive data discovery (sampling, sensitive data findings and discovery results)","url":"https://docs.aws.amazon.com/macie/latest/user/discovery-asdd.html"},{"title":"AWS CLI: macie2 get-automated-discovery-configuration (status, classificationScopeId, sensitivityInspectionTemplateId)","url":"https://docs.aws.amazon.com/cli/latest/reference/macie2/get-automated-discovery-configuration.html"},{"title":"AWS CLI: macie2 describe-buckets (sensitivityScore, automatedDiscoveryMonitoringStatus, unclassifiableObjectCount)","url":"https://docs.aws.amazon.com/cli/latest/reference/macie2/describe-buckets.html"},{"title":"Amazon Macie endpoints and quotas (no AWS GovCloud (US) endpoint listed; per-file size quotas)","url":"https://docs.aws.amazon.com/general/latest/gr/macie.html"},{"title":"AWS Config managed rule: macie-status-check (excluded from AWS GovCloud (US-East) and (US-West))","url":"https://docs.aws.amazon.com/config/latest/developerguide/macie-status-check.html"},{"title":"AWS Resource Groups and Tagging endpoints and quotas (tagging.us-gov-east-1, tagging.us-gov-west-1)","url":"https://docs.aws.amazon.com/general/latest/gr/arg.html"},{"title":"AWS Config in AWS GovCloud (US) — differences (no recording of third-party or custom resource types)","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-config.html"}],"source":"aws","tools":["AWS Config","AWS Resource Groups Tagging API","Amazon Macie","Amazon S3"]},{"id":"remote-access-authorization-and-monitoring","ksi_ids":["KSI-IAM-ELP","KSI-MLA-LET","KSI-SVC-ASM","KSI-CNA-ULN"],"requirement_ids":["MAS-CSO-FLO"],"control_ids":["ac-17","ac-17.1","ac-17.2","ac-17.3"],"aws_services":["AWS Systems Manager Session Manager","AWS Client VPN","AWS Config","Amazon EC2"],"evidence":"How operators actually reach the environment from outside it: the managed access paths that exist, the logging and encryption configured on them, the session-by-session record of who used them, and the negative check that no instance is directly reachable instead","collection":{"kind":"cli","commands":[{"name":"get-document","run":"aws ssm get-document --name SSM-SessionManagerRunShell --document-version '$LATEST' --query Content --output text"},{"name":"describe-sessions","run":"aws ssm describe-sessions --state History --query 'Sessions[].{owner:Owner,target:Target,start:StartDate,end:EndDate,document:DocumentName,accessType:AccessType,maxDuration:MaxSessionDuration}'"},{"name":"describe-client-vpn-endpoints","run":"aws ec2 describe-client-vpn-endpoints --query 'ClientVpnEndpoints[].{id:ClientVpnEndpointId,transport:TransportProtocol,auth:AuthenticationOptions[].Type,connectionLog:ConnectionLogOptions,splitTunnel:SplitTunnel,sessionTimeoutHours:SessionTimeoutHours,serverCert:ServerCertificateArn,selfServicePortal:SelfServicePortalUrl}'"},{"name":"describe-client-vpn-connections","run":"aws ec2 describe-client-vpn-connections --client-vpn-endpoint-id <CLIENT_VPN_ENDPOINT_ID> --query 'Connections[].{user:Username,commonName:CommonName,clientIp:ClientIp,established:ConnectionEstablishedTime,ended:ConnectionEndTime,status:Status,posture:PostureComplianceStatuses}'"},{"name":"ec2-instance-no-public-ip","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name ec2-instance-no-public-ip --compliance-types NON_COMPLIANT"}]},"expected_output":"The Session Manager preferences document as JSON — s3BucketName, s3KeyPrefix, s3EncryptionEnabled, cloudWatchLogGroupName, cloudWatchEncryptionEnabled, cloudWatchStreamingEnabled, kmsKeyId, runAsEnabled, idleSessionTimeout and maxSessionDuration — which is your Region's entire remote-access logging and encryption configuration in one object. Then one row per terminated session from the past 30 days carrying Owner, Target, StartDate, EndDate, DocumentName, MaxSessionDuration and AccessType of Standard or JustInTime. Then per Client VPN endpoint the transport protocol tcp or udp, the authentication types in use (certificate-authentication, directory-service-authentication or federated-authentication), ConnectionLogOptions with Enabled plus CloudwatchLogGroup and CloudwatchLogStream, SplitTunnel, SessionTimeoutHours of 8, 10, 12 or 24 (default 24) and the server certificate ARN. Then per connection Username (Active Directory authentication only), CommonName, ClientIp, ConnectionEstablishedTime, ConnectionEndTime, Status and any PostureComplianceStatuses — active connections plus only those terminated within the last 60 minutes. Finally the EC2 instances AWS Config evaluated NON_COMPLIANT because a publicIp field is present in their configuration item.","cadence":"weekly","govcloud":"Both access paths exist in AWS GovCloud (US-East) and (US-West). Client VPN endpoints there operate using FIPS 140-3 validated cryptographic modules and a fixed cipher set — TLS 1.3 TLS_AES_256_GCM_SHA384 and TLS_AES_128_GCM_SHA256; TLS 1.2 TLS-ECDHE-RSA/ECDSA-WITH-AES-256-GCM-SHA384 and the AES-128-GCM-SHA256 variants; data channel AES-256-GCM — and AWS advises using the exported client configuration file unmodified rather than configuring other ciphers, which makes AC-17(2) there largely a matter of not breaking the default. Systems Manager runs in both Regions; Change Manager and Incident Manager do not, and State Manager association history cannot be viewed, none of which this recipe touches. ec2-instance-no-public-ip is documented for all supported AWS Regions. Calls to these services must use SSL (HTTPS), and ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"The commands split cleanly across the control family, and the gap in the middle is the one to be honest about. AC-17(1) — automated monitoring and control of remote access — is what session history and connection logs deliver, with two documented blind spots. Session Manager does not log sessions that connect through port forwarding or SSH, because SSH encrypts the session data inside the TLS connection and Session Manager is only the tunnel; an operator who port-forwards leaves a session record with no command content behind it. And describe-sessions reaches back 30 days only, so anything longer is an S3 or CloudWatch Logs query against the destinations named in the preferences document, not an SSM call. Client VPN retention is shorter still — terminated connections drop out of the API after 60 minutes, which makes the log group named in ConnectionLogOptions the only durable record, and Username is populated only for Active Directory authentication, so certificate-authenticated users are identified by CommonName or not at all. AC-17(2) is the strongest link in GovCloud, where the endpoints are FIPS 140-3 modules by construction; the Session Manager equivalent is kmsKeyId in the preferences document, and it is empty unless you set it, so an empty kmsKeyId is a finding rather than a default. AC-17(3) — routing remote access through managed network access control points — is the one nothing here proves. Session Manager and a Client VPN endpoint are managed access points, and ec2-instance-no-public-ip is the closest negative check, but that rule applies only to IPv4 and only to AWS::EC2::Instance: an IPv6-reachable instance, a load balancer fronting SSH, or a third-party jump host is invisible to it. Read the result as 'no EC2 instance carries a public IPv4 address', which is a useful sentence and not the control. AC-17 itself — the documented usage restrictions, configuration requirements and per-type authorization — is a record you write, and this telemetry only shows whether the estate matches it. One warning to carry into the evidence package: Session Manager logs the commands entered and their output, so a credential typed into a session lands in the log group you are about to hand an assessor.","references":[{"title":"AWS Systems Manager: enabling and disabling session logging (no logging for port forwarding or SSH sessions)","url":"https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-logging.html"},{"title":"AWS Systems Manager: update Session Manager preferences (SSM-SessionManagerRunShell inputs — kmsKeyId, s3/cloudWatch logging, timeouts)","url":"https://docs.aws.amazon.com/systems-manager/latest/userguide/getting-started-configure-preferences-cli.html"},{"title":"AWS Systems Manager: configure session preferences (KMS key encryption of session data, session history logs, timeouts)","url":"https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-getting-started-configure-preferences.html"},{"title":"AWS CLI: ssm get-document (returns Content for a named document version)","url":"https://docs.aws.amazon.com/cli/latest/reference/ssm/get-document.html"},{"title":"AWS CLI: ssm describe-sessions (--state Active|History; history covers the past 30 days)","url":"https://docs.aws.amazon.com/cli/latest/reference/ssm/describe-sessions.html"},{"title":"AWS CLI: ec2 describe-client-vpn-endpoints (ConnectionLogOptions, AuthenticationOptions, SessionTimeoutHours, SplitTunnel)","url":"https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-client-vpn-endpoints.html"},{"title":"AWS CLI: ec2 describe-client-vpn-connections (active plus connections terminated in the last 60 minutes)","url":"https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-client-vpn-connections.html"},{"title":"AWS Config managed rule: ec2-instance-no-public-ip (EC2_INSTANCE_NO_PUBLIC_IP, IPv4 only, all supported Regions)","url":"https://docs.aws.amazon.com/config/latest/developerguide/ec2-instance-no-public-ip.html"},{"title":"AWS Client VPN in AWS GovCloud (US) — FIPS 140-3 validated modules and the permitted cipher list","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-vpnclient.html"},{"title":"AWS Systems Manager in AWS GovCloud (US) — capability and feature differences","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-ssm.html"}],"source":"aws","tools":["AWS Systems Manager Session Manager","AWS Client VPN","AWS Config","Amazon EC2"]},{"id":"malicious-code-protection","ksi_ids":["KSI-CNA-DFP","KSI-CMT-RMV"],"control_ids":["si-3"],"aws_services":["Amazon GuardDuty","Amazon EC2","Amazon S3","AWS Config"],"evidence":"Whether malware scanning is switched on for compute and for the buckets that accept uploads, plus the scan-by-scan record of what was actually examined and what came back INFECTED","collection":{"kind":"cli","commands":[{"name":"list-detectors","run":"aws guardduty list-detectors"},{"name":"get-detector","run":"aws guardduty get-detector --detector-id <DETECTOR_ID> --query '{status:Status,publishingFrequency:FindingPublishingFrequency,features:Features[].{name:Name,status:Status,additional:AdditionalConfiguration}}'"},{"name":"describe-malware-scans","run":"aws guardduty describe-malware-scans --detector-id <DETECTOR_ID> --query 'Scans[].{id:ScanId,type:ScanType,status:ScanStatus,result:ScanResultDetails,started:ScanStartTime,ended:ScanEndTime,files:FileCount,bytes:TotalBytes,resource:ResourceDetails,trigger:TriggerDetails,failure:FailureReason}'"},{"name":"list-malware-protection-plans","run":"aws guardduty list-malware-protection-plans"},{"name":"get-malware-protection-plan","run":"aws guardduty get-malware-protection-plan --malware-protection-plan-id <PLAN_ID> --query '{protected:ProtectedResource,status:Status,statusReasons:StatusReasons,actions:Actions,role:Role,created:CreatedAt}'"},{"name":"guardduty-malware-protection-enabled","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name guardduty-malware-protection-enabled --compliance-types NON_COMPLIANT"}]},"expected_output":"A detector id per Region — an empty list means GuardDuty was never enabled there, which is itself the finding. Then the detector's Status ENABLED or DISABLED, its FindingPublishingFrequency of FIFTEEN_MINUTES, ONE_HOUR or SIX_HOURS, and the Features list in which EBS_MALWARE_PROTECTION is the entry that matters, alongside FLOW_LOGS, CLOUD_TRAIL, DNS_LOGS, S3_DATA_EVENTS, EKS_AUDIT_LOGS, RDS_LOGIN_EVENTS, LAMBDA_NETWORK_LOGS, EKS_RUNTIME_MONITORING and RUNTIME_MONITORING with its EC2_AGENT_MANAGEMENT, EKS_ADDON_MANAGEMENT and ECS_FARGATE_AGENT_MANAGEMENT sub-configuration. Then one row per malware scan: ScanId, ScanType GUARDDUTY_INITIATED or ON_DEMAND, ScanStatus RUNNING, COMPLETED, FAILED or SKIPPED, a FailureReason when it failed, ScanStartTime and ScanEndTime, the scanned InstanceArn and its attached volumes, FileCount and TotalBytes actually examined, TriggerDetails carrying the GuardDutyFindingId and a TriggerType of GUARDDUTY or BACKUP, and a ScanResultDetails of CLEAN or INFECTED. Then the Malware Protection plan ids, and per plan the protected S3 bucket with its object prefixes, the scanning role, whether result tagging is on, and a Status of ACTIVE, WARNING or ERROR with StatusReasons naming the problem. Finally, outside GovCloud, the detectors AWS Config marks NON_COMPLIANT for GUARDDUTY_MALWARE_PROTECTION_ENABLED.","cadence":"weekly","govcloud":"GuardDuty runs in both AWS GovCloud (US) Regions and Malware Protection for EC2 works there with one documented gap: instances whose productCode is marketplace are not scanned — GuardDuty skips them and logs the skip reason UNSUPPORTED_PRODUCT_CODE_TYPE, so a SKIPPED scan in GovCloud may be that rather than a misconfiguration. Malware Protection for Backup cannot scan EC2 or EBS recovery points there. The GovCloud differences page records no carve-out for Malware Protection for S3. The last command has nothing to call: GUARDDUTY_MALWARE_PROTECTION_ENABLED is excluded from both AWS GovCloud (US-East) and (US-West) — as it is from the China Regions, Mexico (Central), Asia Pacific (Thailand), (Malaysia) and (Taipei) — so drop it and take the enablement fact from the EBS_MALWARE_PROTECTION feature status in get-detector instead. Also unavailable in GovCloud: the entity lists customisation (IP address lists still work) and the GuardDuty Investigation preview. ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"The trap in SI-3 is reading GuardDuty Malware Protection as antivirus. It is not a scheduled sweep of your file systems. A GuardDuty-initiated scan fires only after GuardDuty has already produced a finding indicative of malware on that resource, at most once every 24 hours per resource, and it works agentlessly against snapshots of the attached EBS volumes — so an empty describe-malware-scans list is the expected steady state of a healthy estate and proves nothing about coverage. The two enablement reads prove capability; the scan list proves exercise; neither proves protection. Coverage has a second silent hole: the global GuardDutyExcluded:true tag and your own inclusion or exclusion scan-option tags make GuardDuty initiate a scan and then skip it, so read the scan options next to the tag inventory or a deliberately excluded estate looks like a clean one, and Fargate workloads under EKS or ECS are not scanned at all. Malware Protection for S3 is the closest thing here to SI-3's entry-point requirement — it scans each newly uploaded object and each new version in a configured bucket — but it covers only buckets with an active plan, in the same Region as the plan, in your own account (a delegated GuardDuty administrator cannot enable it on a member account's bucket), and when run independently of GuardDuty there is no detector, so malware produces an EventBridge event, a CloudWatch metric and the optional object tag rather than a GuardDuty finding. What no command here produces is the rest of SI-3: signature or engine currency, since AWS operates the scan engines and exposes no version for you to attest to; periodic full scans; false-positive handling; and the documented response when malicious code is found. Rate those from the plan and the incident record, and keep these reads as the machine half of the answer.","references":[{"title":"Amazon GuardDuty: Malware Protection for EC2 (GuardDuty-initiated vs on-demand scans, Fargate unsupported)","url":"https://docs.aws.amazon.com/guardduty/latest/ug/malware-protection.html"},{"title":"Amazon GuardDuty: GuardDuty-initiated malware scan (fires only on a malware-indicative finding, 24-hour interval, GuardDutyExcluded tag)","url":"https://docs.aws.amazon.com/guardduty/latest/ug/gdu-initiated-malware-scan.html"},{"title":"Amazon GuardDuty: Malware Protection for S3 (scans newly uploaded objects; independent mode has no detector and so no finding)","url":"https://docs.aws.amazon.com/guardduty/latest/ug/gdu-malware-protection-s3.html"},{"title":"AWS CLI: guardduty get-detector (Features list including EBS_MALWARE_PROTECTION and its status)","url":"https://docs.aws.amazon.com/cli/latest/reference/guardduty/get-detector.html"},{"title":"AWS CLI: guardduty describe-malware-scans (ScanStatus, ScanType, TriggerDetails, ScanResultDetails, FileCount, TotalBytes)","url":"https://docs.aws.amazon.com/cli/latest/reference/guardduty/describe-malware-scans.html"},{"title":"AWS CLI: guardduty list-malware-protection-plans (plan ids for protected resources)","url":"https://docs.aws.amazon.com/cli/latest/reference/guardduty/list-malware-protection-plans.html"},{"title":"AWS CLI: guardduty get-malware-protection-plan (ProtectedResource, Status ACTIVE/WARNING/ERROR, StatusReasons)","url":"https://docs.aws.amazon.com/cli/latest/reference/guardduty/get-malware-protection-plan.html"},{"title":"AWS Config managed rule: guardduty-malware-protection-enabled (GUARDDUTY_MALWARE_PROTECTION_ENABLED, excluded from both AWS GovCloud (US) Regions)","url":"https://docs.aws.amazon.com/config/latest/developerguide/guardduty-malware-protection-enabled.html"},{"title":"Amazon GuardDuty in AWS GovCloud (US) — Malware Protection for EC2 marketplace productCode skip, Backup recovery point limits","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-guardduty.html"}],"source":"aws","tools":["Amazon GuardDuty","Amazon EC2","Amazon S3","AWS Config"]},{"id":"audit-reduction-and-report-generation","ksi_ids":["KSI-MLA-OSM","KSI-MLA-LET"],"control_ids":["au-7","au-7.1"],"aws_services":["Amazon CloudWatch Logs","AWS CloudTrail","Amazon Athena","Amazon S3"],"evidence":"A demonstration run against the live log estate that audit records can be reduced, sorted and searched on demand by event criteria — the standing saved queries, the query that ran, and the report it returned","collection":{"kind":"cli","commands":[{"name":"describe-query-definitions","run":"aws logs describe-query-definitions --query 'queryDefinitions[].{id:queryDefinitionId,name:name,language:queryLanguage,logGroups:logGroupNames}'"},{"name":"logs-insights-start-query","run":"aws logs start-query --log-group-names <LOG_GROUP_NAME> --start-time <START_EPOCH> --end-time <END_EPOCH> --query-string 'fields @timestamp, userIdentity.arn, eventName, sourceIPAddress | filter eventName = \"ConsoleLogin\" | sort @timestamp desc | limit 1000'"},{"name":"get-query-results","run":"aws logs get-query-results --query-id <QUERY_ID>"},{"name":"list-event-data-stores","run":"aws cloudtrail list-event-data-stores --query 'EventDataStores[].{name:Name,arn:EventDataStoreArn,status:Status,retentionDays:RetentionPeriod,multiRegion:MultiRegionEnabled,organization:OrganizationEnabled,terminationProtection:TerminationProtectionEnabled}'"},{"name":"lake-start-query","run":"aws cloudtrail start-query --query-statement \"SELECT eventTime, eventName, userIdentity.arn, sourceIPAddress FROM <EVENT_DATA_STORE_ID> WHERE eventName = 'ConsoleLogin' ORDER BY eventTime DESC LIMIT 1000\" --delivery-s3-uri s3://<EVIDENCE_BUCKET>"},{"name":"start-query-execution","run":"aws athena start-query-execution --work-group primary --query-execution-context Database=cloudtrail_logs --result-configuration OutputLocation=s3://audit-reports-bucket/athena/ --query-string \"SELECT eventtime, useridentity.arn, eventname, sourceipaddress FROM cloudtrail_logs WHERE eventname = 'ConsoleLogin' ORDER BY eventtime DESC LIMIT 1000\""}]},"expected_output":"First the saved CloudWatch Logs Insights query definitions — queryDefinitionId, name, queryLanguage of CWLI, SQL or PPL, the queryString itself and the log groups each is scoped to — which is the standing reduction-and-reporting capability as configured rather than as claimed. Then a queryId, and against it the matching records: at most 100,000 log events per query and 10,000 returned per get-query-results call, one query spanning at most 50 log groups, a 60-minute runtime ceiling and a Region-wide limit of 100 concurrent Insights queries. Then the event data stores with Status (CREATED, ENABLED, PENDING_DELETION, STARTING_INGESTION, STOPPING_INGESTION or STOPPED_INGESTION), RetentionPeriod in days from 7 to 3,653, MultiRegionEnabled, OrganizationEnabled and TerminationProtectionEnabled. Then a QueryId from CloudTrail Lake whose results are delivered to the S3 URI you named, and a QueryExecutionId from Athena whose results land in the workgroup's OutputLocation. Keep the query text beside its output — the pair is the evidence and neither half is evidence alone.","cadence":"quarterly","govcloud":"All three query paths run in both AWS GovCloud (US) Regions. Athena's only documented difference is that granting AWS Lake Formation permissions to Athena users who authenticate through the JDBC or ODBC driver with a SAML identity provider is unavailable. CloudWatch Logs is available with Live Tail missing and the logGroupNamePattern parameter unsupported on DescribeLogGroups — neither affects an Insights query. CloudTrail Lake is available, but Lake integrations, query generation, query results summarization, event data stores for AWS Config configuration items, AWS Audit Manager evidence and events from outside AWS, and the Activity summary widget are not: you write the SQL yourself, and non-AWS audit records cannot be pulled into the same store for reduction. One scoping trap: since 22 November 2021 CloudFront, IAM and AWS STS events are recorded in AWS GovCloud (US-West), so a single-Region search from US-East silently misses every global-service event unless the trail is multi-Region. Calls must use SSL (HTTPS) and ARNs use partition arn:aws-us-gov","automatable":"partial","notes":"AU-7 asks for a capability, so the honest evidence is a query that ran rather than a configuration that exists — run one and keep the query text beside its output. AU-7(1), processing and sorting records by event criteria, is exactly what the filter and sort clauses in the Insights query and the WHERE and ORDER BY in the Lake and Athena statements demonstrate; pick criteria an assessor cares about — a named principal, a source IP, an event name, a bounded window — rather than a bare SELECT *. The half of AU-7 that no output proves is the requirement that reduction not alter the original content or time ordering of the records: these are read APIs, and Athena queries the CloudTrail objects in place in S3 rather than rewriting them, but that is an argument from the API contract, not a line in the result set — the property is actually carried by log file validation and object immutability, which belong to the integrity recipe, not this one. Two limits deserve to be read before the query power is. A query can only reduce records that reached the log group or the event data store, so a 90-day retention makes an annual report impossible no matter how good the SQL, which is why the retention fields are pulled alongside. And the Insights ceilings — 100,000 events per query, 10,000 per page, 50 log groups, 60 minutes — mean a broad search across a year of CloudTrail truncates silently rather than failing, so a result set sitting exactly at the limit is a truncated report and must not be filed as a complete one; Athena and CloudTrail Lake have no such row ceiling and are the right tools for a long look-back. Whether the resulting report actually supports after-the-fact investigation is a human judgement about the query, which is what keeps this partial rather than full.","references":[{"title":"AWS CLI: logs start-query (log group and time-range parameters, queryId, 100 concurrent queries, 100,000 event and 10,000 page limits, 50 log groups, 60-minute timeout)","url":"https://docs.aws.amazon.com/cli/latest/reference/logs/start-query.html"},{"title":"AWS CLI: logs describe-query-definitions (saved Insights queries — queryDefinitionId, name, queryLanguage, queryString, logGroupNames)","url":"https://docs.aws.amazon.com/cli/latest/reference/logs/describe-query-definitions.html"},{"title":"AWS CLI: cloudtrail start-query (CloudTrail Lake SQL query against an event data store, --delivery-s3-uri, returns QueryId)","url":"https://docs.aws.amazon.com/cli/latest/reference/cloudtrail/start-query.html"},{"title":"AWS CLI: cloudtrail list-event-data-stores (Status, RetentionPeriod 7–3653 days, MultiRegionEnabled, OrganizationEnabled, TerminationProtectionEnabled)","url":"https://docs.aws.amazon.com/cli/latest/reference/cloudtrail/list-event-data-stores.html"},{"title":"AWS CLI: athena start-query-execution (--query-string, --work-group, --query-execution-context, OutputLocation; returns QueryExecutionId)","url":"https://docs.aws.amazon.com/cli/latest/reference/athena/start-query-execution.html"},{"title":"Amazon Athena: query AWS CloudTrail logs (query log files directly from Amazon S3 by LOCATION)","url":"https://docs.aws.amazon.com/athena/latest/ug/cloudtrail-logs.html"},{"title":"Amazon Athena in AWS GovCloud (US) — Region availability and documented differences","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-athena.html"},{"title":"Amazon CloudWatch Logs in AWS GovCloud (US) — Live Tail and logGroupNamePattern unavailable","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-cwl.html"},{"title":"AWS CloudTrail in AWS GovCloud (US) — CloudTrail Lake feature carve-outs and global service events recorded in us-gov-west-1","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-ct.html"}],"source":"aws","tools":["Amazon CloudWatch Logs","AWS CloudTrail","Amazon Athena","Amazon S3"]},{"id":"guardduty-incident-after-action","ksi_ids":["KSI-INR-AAR"],"control_ids":["ir-3","ir-4","ir-4.1","ir-8"],"aws_services":["Amazon GuardDuty","AWS Security Hub","Amazon S3"],"evidence":"GuardDuty findings and Security Hub insight summaries that were reviewed, together with evidence that the findings fed into an after-action review cycle — AAR documents stored as versioned S3 objects with the incident IDs they reference","collection":{"kind":"cli","commands":[{"name":"guardduty-list-findings","run":"aws guardduty list-findings --detector-id <DETECTOR_ID> --finding-criteria '{\"Criterion\":{\"severity\":{\"Gte\":[4]},\"updatedAt\":{\"Gte\":[\"<SINCE_EPOCH>\"]}}}' --query 'FindingIds[]'"},{"name":"guardduty-finding-detail","run":"aws guardduty get-findings --detector-id <DETECTOR_ID> --finding-ids <FINDING_ID_1> <FINDING_ID_2> --query 'Findings[].{Id:Id,Type:Type,Severity:Severity,Resource:Resource.ResourceType,Region:Region,Time:UpdatedAt,Desc:Description}'"},{"name":"securityhub-insights","run":"aws securityhub get-insights --query 'Insights[].{Arn:InsightArn,Name:Name,Filters:Filters,GroupBy:GroupByAttr}'"},{"name":"securityhub-insight-results","run":"aws securityhub get-insight-results --insight-arn <INSIGHT_ARN>"},{"name":"after-action-reports","run":"aws s3api list-objects-v2 --bucket <EVIDENCE_BUCKET> --prefix after-action-reports/ --query 'Contents[].{Key:Key,Size:Size,Modified:LastModified}'"}]},"expected_output":"A list of GuardDuty findings with severity >= 4 (medium+), their IDs, types, affected resource types and update timestamps; Security Hub insight names and result counts showing which insights are actively matching; and a non-empty S3 object list under after-action-reports/ whose keys include finding IDs or incident reference numbers, proving findings are linked to AARs rather than reviewed in isolation and forgotten.","cadence":"quarterly","govcloud":"GuardDuty, Security Hub and S3 are available in both AWS GovCloud (US-East) and (US-West); detector, hub and bucket ARNs use partition arn:aws-us-gov.","automatable":"partial","notes":"IR-3 wants incident response tested, IR-4 wants incidents handled and lessons learned applied, IR-4.1 wants automated handling support, and IR-8 wants an incident response plan. GuardDuty findings and Security Hub insights are telemetry, not proof that anyone acted on them. The S3 AAR objects close the loop: they show someone reviewed the finding, and that the review produced a document. What the API cannot prove is that the lessons were actually incorporated into the plan — that is a human judgement from the AAR content. An after-action report without a recommendation-to-plan-change linkage is a finding without a fix. If no AARs exist yet, this recipe is the trigger to create them, not evidence that the control is satisfied. Substitute your detector ID, evidence bucket name and insight ARNs. GuardDuty list-findings returns at most 50 findings per page; add --max-results and --next-token pagination for a full quarter.","assertions":[{"field":"after-action-reports","op":"exists","controls":["ir-3","ir-4","ir-8"],"description":"At least one after-action report exists as a versioned S3 object, proving findings were reviewed beyond the event itself."}],"references":[{"title":"AWS CLI: guardduty list-findings (severity and date filters)","url":"https://docs.aws.amazon.com/cli/latest/reference/guardduty/list-findings.html"},{"title":"AWS CLI: securityhub get-insights","url":"https://docs.aws.amazon.com/cli/latest/reference/securityhub/get-insights.html"},{"title":"AWS CLI: s3api list-objects-v2","url":"https://docs.aws.amazon.com/cli/latest/reference/s3api/list-objects-v2.html"}],"source":"aws","tools":["Amazon GuardDuty","AWS Security Hub","Amazon S3"]},{"id":"securityhub-incident-review-procedures","ksi_ids":["KSI-INR-RIR"],"control_ids":["ir-4","ir-4.1","ir-6","ir-6.1","ir-6.3","ir-7","ir-7.1","ir-8","ir-8.1","si-4.5"],"aws_services":["AWS Security Hub","Amazon GuardDuty","AWS Systems Manager","Amazon CloudWatch"],"evidence":"Security Hub finding workflow status plus GuardDuty detection coverage, showing incident response procedures are exercised: enabled insights, automated response actions routed through EventBridge, automated notifications, and Systems Manager Automation documents that encode response steps","collection":{"kind":"cli","commands":[{"name":"resolved-high-findings","run":"aws securityhub get-findings --filters '{\"WorkflowStatus\":[{\"Value\":\"RESOLVED\",\"Comparison\":\"EQUALS\"}],\"SeverityLabel\":[{\"Value\":\"HIGH\",\"Comparison\":\"EQUALS\"}]}' --query 'Findings[].{Id:Id,Title:Title,Type:Types[0],Severity:Severity.Label,Resource:Resources[0].Id,Updated:UpdatedAt}'"},{"name":"guardduty-detector-id","run":"aws guardduty list-detectors --query 'DetectorIds[0]'"},{"name":"guardduty-detector","run":"aws guardduty get-detector --detector-id <DETECTOR_ID> --query '{Status:Status,Features:Features[].{Name:Name,Status:Status}}'"},{"name":"guardduty-event-rules","run":"aws events list-rules --name-prefix guardduty --query 'Rules[].{Name:Name,Pattern:EventPattern,Targets:State}'"},{"name":"automation-documents","run":"aws ssm list-documents --filters Key=Owner,Values=Self --filters Key=DocumentType,Values=Automation --query 'DocumentIdentifiers[].{Name:Name,Owner:Owner,Version:DocumentVersion,Type:DocumentType}'"},{"name":"securityhub-alarms","run":"aws cloudwatch describe-alarms --alarm-name-prefix SecurityHub --query 'MetricAlarms[].{Name:AlarmName,State:StateValue,Actions:AlarmActions,Metric:MetricName}'"}]},"expected_output":"Security Hub findings that reached RESOLVED status (proving the workflow was exercised end-to-end), GuardDuty detector status with features showing which data sources are enabled, EventBridge rules prefixed with guardduty that route findings to targets, Systems Manager Automation documents owned by the account that encode response steps, and CloudWatch alarms that fire on Security Hub metrics.","cadence":"monthly","govcloud":"Security Hub, GuardDuty, EventBridge, Systems Manager and CloudWatch are available in both AWS GovCloud (US-East) and (US-West); findings and document ARNs use partition arn:aws-us-gov. GovCloud FIPS endpoints are used by default for all services.","automatable":"partial","notes":"IR-4 wants incident handling capability, IR-4.1 wants automated incident handling, IR-6/6.1/6.3 want reporting to authorities with automated support, IR-7/7.1 want response assistance/coordination, IR-8/8.1 want the incident response plan and its updates, and SI-4.5 wants system monitoring. The evidence here is the infrastructure of response, not the act of responding: resolved findings show the workflow was used, enabled GuardDuty features show detection is active, EventBridge rules show automated routing exists, SSM Automation documents show response steps are encoded as code, and CloudWatch alarms show monitoring. What none of this proves is that the procedures are effective, that notifications reached the right authorities within SLA, or that the incident response plan was actually updated after the event. Those are narrative gaps filled by the plan document, notification records, and AARs. The workflow status is the only pass/fail field here — a finding stuck in NEW or NOTIFIED rather than RESOLVED is a control failure. Substitute your detector ID.","assertions":[{"field":"resolved-high-findings.Findings","op":"exists","controls":["ir-4","ir-4.1","ir-8"],"description":"The RESOLVED/HIGH get-findings call returns a non-empty Findings array within the review window, proving the incident response workflow was exercised end to end rather than merely configured."}],"references":[{"title":"AWS CLI: securityhub get-findings (WorkflowStatus and SeverityLabel filters)","url":"https://docs.aws.amazon.com/cli/latest/reference/securityhub/get-findings.html"},{"title":"AWS CLI: events list-rules (EventBridge guardduty rules)","url":"https://docs.aws.amazon.com/cli/latest/reference/events/list-rules.html"},{"title":"AWS CLI: ssm list-documents (Automation documents)","url":"https://docs.aws.amazon.com/cli/latest/reference/ssm/list-documents.html"},{"title":"AWS CLI: cloudwatch describe-alarms","url":"https://docs.aws.amazon.com/cli/latest/reference/cloudwatch/describe-alarms.html"}],"source":"aws","tools":["AWS Security Hub","Amazon GuardDuty","AWS Systems Manager","Amazon CloudWatch"]},{"id":"guardduty-pattern-review-past-incidents","ksi_ids":["KSI-INR-RPI"],"control_ids":["ir-3","ir-4","ir-4.1","ir-5","ir-8"],"aws_services":["Amazon GuardDuty","AWS CloudTrail","AWS Security Hub","Amazon Athena","Amazon S3"],"evidence":"Recurring GuardDuty finding types and patterns queried across a multi-quarter window, plus Athena SQL over CloudTrail data, surfacing whether the same attack vectors repeat and whether the same misconfigurations are reintroduced after remediation","collection":{"kind":"cli","commands":[{"name":"list-findings","run":"aws guardduty list-findings --detector-id <DETECTOR_ID> --finding-criteria '{\"Criterion\":{\"updatedAt\":{\"Gte\":[\"<SINCE_EPOCH>\"]}}}' --query 'FindingIds[]'"},{"name":"guardduty-finding-detail","run":"aws guardduty get-findings --detector-id <DETECTOR_ID> --finding-ids <IDS> --query 'Findings[].{Id:Id,Type:Type,Severity:Severity,Resource:Resource.ResourceType,Region:Region,Count:Service.Count,Account:AccountId}'"},{"name":"securityhub-active-findings","run":"aws securityhub get-findings --filters '{\"RecordState\":[{\"Value\":\"ACTIVE\",\"Comparison\":\"EQUALS\"}]}' --query 'Findings[].{Id:Id,Title:Title,Type:Types[0],Severity:Severity.Label,Resource:Resources[0].Type,First:FirstObservedAt,Last:LastObservedAt}'"},{"name":"lookup-events-signin-amazonaws-com","run":"aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventSource,AttributeValue=signin.amazonaws.com --start-time 2024-01-01T00:00:00Z --end-time 2024-12-31T23:59:59Z --max-results 50 --query 'Events[].{Name:EventName,Time:EventTime,User:Username}'"}]},"expected_output":"GuardDuty findings showing their findingType and Count fields (indicating repeat occurrences of the same threat), Security Hub ACTIVE findings with FirstObservedAt and LastObservedAt showing persistence windows, and CloudTrail signin events showing authentication patterns over the year that may indicate repeated lateral-movement or credential-abuse attempts.","cadence":"quarterly","govcloud":"GuardDuty, Security Hub, CloudTrail, Athena and S3 are available in both AWS GovCloud (US-East) and (US-West); findings and trail ARNs use partition arn:aws-us-gov. CloudTrail lookup-events covers 90 days only; older data must come from Athena queries against the trail S3 bucket.","automatable":"partial","notes":"IR-3 wants testing for repeated flaws, IR-4/4.1 wants incident handling, IR-5 wants incident monitoring, and IR-8 wants the response plan. The pattern review is the quarterly audit of whether the environment is getting healthier or sicker: a recurring finding type across quarters means the root cause was not fixed, and a high Count on a GuardDuty finding means the same attack vector struck multiple times. The honest gap: finding telemetry does not prove anyone reviewed the pattern — the review is the human step of comparing this quarter against last quarter and recording the conclusion. If the same finding type appears in every quarterly pull, the control is failing even when the telemetry is clean, because the root cause is unaddressed. The CloudTrail signin query is a sample, not exhaustive — replace with an Athena query against your trail S3 bucket for a full year of authentication events. Security Hub FirstObservedAt and LastObservedAt show how long a finding has persisted; a gap between them and the current date means the finding was fixed and then reintroduced. This recipe deliberately states no assertion: the check that matters — this quarter's finding types compared against last quarter's, read against the prior after-action report's root-cause fix — spans two collections and a document, so no single command output decides it. A quarter-over-quarter diff is the reviewer's work, and writing it down as a machine assertion would claim a check nothing can run.","references":[{"title":"AWS CLI: guardduty get-findings (Type, Severity, Service.Count for repeat detection)","url":"https://docs.aws.amazon.com/cli/latest/reference/guardduty/get-findings.html"},{"title":"AWS CLI: securityhub get-findings (FirstObservedAt, LastObservedAt for persistence)","url":"https://docs.aws.amazon.com/cli/latest/reference/securityhub/get-findings.html"},{"title":"AWS CLI: cloudtrail lookup-events (90-day window, signin events)","url":"https://docs.aws.amazon.com/cli/latest/reference/cloudtrail/lookup-events.html"}],"source":"aws","tools":["Amazon GuardDuty","AWS CloudTrail","AWS Security Hub","Amazon Athena","Amazon S3"]},{"id":"route53-dnssec-signing","ksi_ids":["KSI-SVC-SIN"],"control_ids":["sc-20"],"aws_services":["Amazon Route 53","AWS KMS"],"evidence":"DNSSEC signing status of every public hosted zone, with the key-signing key state and the DS record that carries the chain of trust to the parent","collection":{"kind":"cli","commands":[{"name":"public-hosted-zones","run":"aws route53 list-hosted-zones --query 'HostedZones[?Config.PrivateZone==`false`].{Id:Id,Name:Name}'"},{"name":"public-zone-dnssec","run":"aws route53 get-dnssec --hosted-zone-id <PUBLIC_ZONE_ID>"},{"name":"domain-dnssec-keys","run":"aws route53domains get-domain-detail --region us-east-1 --domain-name <DOMAIN> --query 'DnssecKeys'"}]},"expected_output":"One get-dnssec response per public hosted zone under the projection root public-zone-dnssec: Status.ServeSignature (SIGNING | NOT_SIGNING | DELETING | ACTION_NEEDED | INTERNAL_FAILURE) and KeySigningKeys[] with Status (ACTIVE | INACTIVE | DELETING | ACTION_NEEDED | INTERNAL_FAILURE), DSRecord, DNSKEYRecord, KeyTag and KmsArn. get-domain-detail returns DnssecKeys[] with the Digest, KeyTag, Algorithm and Flags actually lodged at the registry.","cadence":"weekly","govcloud":"Route 53 public and private DNS are available in both GovCloud (US) Regions and DNSSEC signing is supported, with two placement constraints AWS documents: the customer managed key used for signing must be in GovCloud (US-West), and the Route 53 control plane for GovCloud is in GovCloud (US-West). Zone ARNs use partition arn:aws-us-gov. route53domains is a commercial-partition registrar service, so the third command applies only where the domain is registered with Route 53 Domains.","automatable":"partial","notes":"Private hosted zones cannot be DNSSEC-signed, which is why the first command narrows to Config.PrivateZone==false before the per-zone loop; asserting SIGNING over every hosted zone would fail on private zones for a reason that is not a finding. Drive the loop from list-hosted-zones rather than from whatever get-dnssec happens to return, so a zone that was never configured is a visible failure instead of an absent row.\n\nSC-20's second limb — the means to indicate the security status of child zones and enable verification of a chain of trust — is carried by KeySigningKeys[].DSRecord, which get-dnssec returns and the assertions cover. Whether that DS record has actually been published by the parent is a fact about the registrar, not about the zone: where the domain is registered with Route 53 Domains, get-domain-detail's DnssecKeys evidences it directly, and where it is registered elsewhere the registrar's own DS confirmation is the artifact to attach. No assertion is written against the third command for that reason — it is decisive for some tenants and inapplicable to others, and an assertion that silently does not apply is worse than a named gap.\n\nACTION_NEEDED on either the zone status or a KSK is an outage risk, not a paperwork state; AWS recommends a CloudWatch alarm on DNSSECInternalFailure and DNSSECKeySigningKeysNeedingAction, and a collector that only samples weekly should not be the first thing to notice it. list-hosted-zones returning no public zones passes all three assertions vacuously — a human confirms this account is the authoritative DNS for the offering's names, and confirms registrar-side DS publication where the registrar is not Route 53 Domains. Partial for the second limb of SC-20.","assertions":[{"field":"public-zone-dnssec.Status.ServeSignature","op":"eq","value":"SIGNING","controls":["sc-20"],"description":"Every public hosted zone is serving DNSSEC signatures, so authoritative responses carry data-origin authentication and integrity artifacts."},{"field":"public-zone-dnssec.KeySigningKeys[].Status","op":"eq","value":"ACTIVE","controls":["sc-20"],"description":"Every key-signing key on a signed zone is ACTIVE, so the signatures are being produced by a live key rather than by one left INACTIVE or in ACTION_NEEDED."},{"field":"public-zone-dnssec.KeySigningKeys[].DSRecord","op":"exists","controls":["sc-20"],"description":"Every signed zone emits a delegation signer record — the means by which a parent indicates the security status of this zone and a resolver verifies the chain of trust."}],"scope":{"resource_types":["AWS::Route53::HostedZone"],"regions":"global-service","accounts":"single","enumerated_by":["public-hosted-zones"],"note":"Route 53 is a global service; the per-zone loop is driven from list-hosted-zones so an unconfigured zone is a visible failure rather than an absent row. Registrar-side DS publication is outside the account."},"references":[{"title":"AWS CLI: route53 get-dnssec (Status.ServeSignature and KeySigningKeys[].Status value sets, DSRecord)","url":"https://docs.aws.amazon.com/cli/latest/reference/route53/get-dnssec.html"},{"title":"Configuring DNSSEC signing in Amazon Route 53 (KSK is an asymmetric KMS key you own; parent zone must support DS records; alarm on DNSSECInternalFailure and DNSSECKeySigningKeysNeedingAction)","url":"https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-configuring-dnssec.html"},{"title":"AWS CLI: route53domains get-domain-detail (DnssecKeys — the DS information passed to the registry to establish the chain of trust)","url":"https://docs.aws.amazon.com/cli/latest/reference/route53domains/get-domain-detail.html"},{"title":"Amazon Route 53 in AWS GovCloud (US) — DNSSEC signing KMS key must be in GovCloud (US-West); control plane is in GovCloud (US-West)","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-r53.html"}],"source":"aws","tools":["Amazon Route 53","AWS KMS"]},{"id":"route53-resolver-dnssec-validation","ksi_ids":["KSI-SVC-SIN"],"control_ids":["sc-21"],"aws_services":["Amazon Route 53","Amazon VPC"],"evidence":"Per-VPC DNSSEC validation status of the Route 53 Resolver, joined against the full VPC inventory so that a VPC which never enabled validation is visible rather than absent","collection":{"kind":"cli","commands":[{"name":"describe-vpcs","run":"aws ec2 describe-vpcs --query 'Vpcs[].VpcId'"},{"name":"list-resolver-dnssec-configs","run":"aws route53resolver list-resolver-dnssec-configs"}]},"expected_output":"describe-vpcs yields the VpcId inventory for the Region. list-resolver-dnssec-configs yields ResolverDnssecConfigs[] with Id, OwnerId, ResourceId and ValidationStatus (ENABLING | ENABLED | DISABLING | DISABLED | UPDATING_TO_USE_LOCAL_RESOURCE_SETTING | USE_LOCAL_RESOURCE_SETTING); the array is paginated and carries NextToken.","cadence":"weekly","govcloud":"Route 53 is available in both GovCloud (US) Regions and the GovCloud difference page documents no carve-out for Resolver DNSSEC validation; it does record that Route 53 Resolver delegation is unavailable for private hosted zones, which is a different feature. The Route 53 control plane for GovCloud is in GovCloud (US-West). VPC ARNs use partition arn:aws-us-gov.","automatable":"partial","notes":"Rated partial for a reason that is in AWS's own words rather than in our judgement of the control: ListResolverDnssecConfigs returns one element per DNSSEC validation configuration associated with the account and \"doesn't contain disabled DNSSEC configurations for the resource\". A VPC with validation off is therefore ABSENT from the array, not reported as DISABLED — so a check of the form \"every returned entry is ENABLED\" is vacuously true on an account that has enabled validation nowhere. That is the exact shape of a check that passes while proving nothing, so it is not written as an assertion.\n\nThe decidable question is a set comparison: every VpcId from describe-vpcs must appear as a ResourceId in ResolverDnssecConfigs with ValidationStatus ENABLED. That spans two collections, and the assertion vocabulary compares a field against a constant rather than one command's output against another's, so the completeness half is a human read. The telemetry is real and specific; what it cannot do by itself is prove coverage.\n\nTwo further limits worth attaching to the evidence. Validation is applied by the VPC Resolver when it performs recursive resolution, so if the VPC forwards to another resolver, that resolver is the one doing recursion and must validate — the AWS status says nothing about it. And the VPC Resolver ignores the DO and CD bits and does not set AD or return DNSSEC records, so a workload cannot perform its own validation downstream of it; where a system owes that, the artifact is the resolver it runs instead.","references":[{"title":"ListResolverDnssecConfigs API — \"It doesn't contain disabled DNSSEC configurations for the resource\"; paginated with NextToken","url":"https://docs.aws.amazon.com/Route53/latest/APIReference/API_route53resolver_ListResolverDnssecConfigs.html"},{"title":"AWS CLI: route53resolver get-resolver-dnssec-config (ResolverDNSSECConfig fields and the ValidationStatus value set)","url":"https://docs.aws.amazon.com/cli/latest/reference/route53resolver/get-resolver-dnssec-config.html"},{"title":"Enabling DNSSEC validation in Amazon Route 53 (applied by the VPC Resolver during recursion; a forwarding resolver must validate itself; the VPC Resolver ignores DO/CD and does not set AD)","url":"https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver-dnssec-validation.html"},{"title":"Amazon Route 53 in AWS GovCloud (US) — Region availability and documented differences","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-r53.html"}],"source":"aws","tools":["Amazon Route 53","Amazon VPC"]},{"id":"identity-sources-and-root-credential-lockdown","ksi_ids":["KSI-IAM-APM","KSI-IAM-ELP"],"control_ids":["ia-2"],"aws_services":["AWS IAM","AWS IAM Identity Center","AWS Config"],"evidence":"Every way a workforce user can authenticate into the account, counted and named in one pass — how many IAM users and federated trusts exist, which SAML and OIDC providers are registered, whether an IAM Identity Center instance is the workforce entry path — together with the state of the two credentials that belong to no person: the root user's access key and the account's X.509 signing certificate","collection":{"kind":"cli","commands":[{"name":"get-account-summary","run":"aws iam get-account-summary --query 'SummaryMap.{Users:Users,Providers:Providers,MFADevices:MFADevices,MFADevicesInUse:MFADevicesInUse,AccountAccessKeysPresent:AccountAccessKeysPresent,AccountSigningCertificatesPresent:AccountSigningCertificatesPresent}'"},{"name":"list-saml-providers","run":"aws iam list-saml-providers --query 'SAMLProviderList[].{Arn:Arn,ValidUntil:ValidUntil,CreateDate:CreateDate}'"},{"name":"list-open-id-connect-providers","run":"aws iam list-open-id-connect-providers"},{"name":"list-instances","run":"aws sso-admin list-instances --query 'Instances[].{InstanceArn:InstanceArn,IdentityStoreId:IdentityStoreId,Status:Status}'"},{"name":"iam-user-mfa-enabled","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name iam-user-mfa-enabled --compliance-types NON_COMPLIANT"},{"name":"iam-root-access-key-check","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name iam-root-access-key-check --compliance-types NON_COMPLIANT"}]},"expected_output":"Under the projection root account-summary, a SummaryMap of integers: Users and Providers size the two populations that can authenticate, MFADevices and MFADevicesInUse size the authenticator estate, and AccountAccessKeysPresent and AccountSigningCertificatesPresent are 0/1 flags for account-level credentials. Under saml-providers and oidc-providers, one entry per registered federation trust — SAMLProviderList carries Arn, ValidUntil and CreateDate (tags are not returned; GetSAMLProvider is the call for those). Under identity-center, zero or one instance with InstanceArn, IdentityStoreId and Status (CREATE_IN_PROGRESS | CREATE_FAILED | DELETE_IN_PROGRESS | ACTIVE). From AWS Config, two EvaluationResults arrays; empty NON_COMPLIANT sets mean every IAM user has an MFA device and the root user holds no access key. Managed rule identifiers: IAM_USER_MFA_ENABLED (rule name iam-user-mfa-enabled) and IAM_ROOT_ACCESS_KEY_CHECK (rule name iam-root-access-key-check).","cadence":"monthly","govcloud":"IAM, IAM Identity Center and AWS Config are all available in both GovCloud (US) Regions, and both managed rules used here are in their supported-Region lists; user, provider and instance ARNs use partition arn:aws-us-gov, and an Identity Center instance ARN takes the form arn:aws-us-gov:sso:::instance/<SSOInstanceId>. Two GovCloud facts to plan around: the Identity Center administrative console, SDK and CLI must be reached over FIPS endpoints, and multi-Region Identity Center support is not available there, so one instance is the whole answer rather than one per Region. Note also that ROOT_ACCOUNT_MFA_ENABLED — the obvious companion rule — is explicitly NOT available in AWS GovCloud (US-East) or (US-West), which is why root MFA is not asserted here from AWS Config; evidence it from the IAM credential report's <root_account> row instead.","automatable":"partial","notes":"This enumerates the authentication paths and closes the credentials that belong to nobody. What it cannot do is the word IA-02 turns on: unique. No API reports that an IAM user is one named human rather than a login three engineers share, or that a federated subject maps one-to-one onto a person on the roster — that binding lives in the personnel record and the joiner/mover/leaver process, and a reviewer establishes it by joining this output against the account inventory (see the AC-02 account-authorization-details recipe), not by reading this output alone. Rated partial for that reason: the authentication half is decided here outright, the identification half is not.\n\nProviders counts SAML and OIDC providers together, so the two list calls are what tell you which is which. An account with Users at 0 and one Identity Center instance is the strong shape — no standing workforce credentials at all — and an account with both is the one worth explaining. Substitute nothing here: every command runs as written.\n\nThe two AWS Config rules are periodic and evaluate global IAM resource types, so deploy them in exactly one Region; adding them in several does not add coverage and does add duplicate evaluations.","references":[{"title":"IAM API: GetAccountSummary — the SummaryMap key set (AccountAccessKeysPresent, AccountSigningCertificatesPresent, Providers, MFADevicesInUse)","url":"https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetAccountSummary.html"},{"title":"AWS CLI: iam list-saml-providers (SAMLProviderList — Arn, ValidUntil, CreateDate; tags are not returned)","url":"https://docs.aws.amazon.com/cli/latest/reference/iam/list-saml-providers.html"},{"title":"AWS CLI: sso-admin list-instances (InstanceArn, IdentityStoreId, Status)","url":"https://docs.aws.amazon.com/cli/latest/reference/sso-admin/list-instances.html"},{"title":"AWS Config managed rule: iam-user-mfa-enabled (IAM_USER_MFA_ENABLED)","url":"https://docs.aws.amazon.com/config/latest/developerguide/iam-user-mfa-enabled.html"},{"title":"AWS Config managed rule: iam-root-access-key-check (IAM_ROOT_ACCESS_KEY_CHECK — COMPLIANT when the root user access key does not exist)","url":"https://docs.aws.amazon.com/config/latest/developerguide/iam-root-access-key-check.html"},{"title":"AWS Config managed rule: root-account-mfa-enabled — Region list excludes AWS GovCloud (US-East) and (US-West)","url":"https://docs.aws.amazon.com/config/latest/developerguide/root-account-mfa-enabled.html"},{"title":"AWS IAM Identity Center in AWS GovCloud (US) — FIPS endpoints for console/SDK/CLI, arn:aws-us-gov:sso:::instance/ pattern, no multi-Region support","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-sso.html"}],"source":"aws","tools":["AWS IAM","AWS IAM Identity Center","AWS Config"]},{"id":"cognito-external-user-authentication","ksi_ids":["KSI-IAM-APM"],"control_ids":["ia-8"],"aws_services":["Amazon Cognito","AWS IAM"],"evidence":"For every Amazon Cognito directory that fronts non-organizational users: the user pool's multi-factor configuration, and every identity pool's guest-access flag together with the named external providers it will exchange a token for","collection":{"kind":"cli","commands":[{"name":"user-pools","run":"aws cognito-idp list-user-pools --max-results 60 --query 'UserPools[].{Id:Id,Name:Name,Status:Status}'"},{"name":"user-pool-mfa","run":"aws cognito-idp get-user-pool-mfa-config --user-pool-id <USER_POOL_ID>"},{"name":"identity-pools","run":"aws cognito-identity list-identity-pools --max-results 60 --query 'IdentityPools[].{Id:IdentityPoolId,Name:IdentityPoolName}'"},{"name":"identity-pool","run":"aws cognito-identity describe-identity-pool --identity-pool-id <IDENTITY_POOL_ID> --query '{Id:IdentityPoolId,AllowUnauthenticatedIdentities:AllowUnauthenticatedIdentities,AllowClassicFlow:AllowClassicFlow,Cognito:CognitoIdentityProviders,Login:SupportedLoginProviders,Saml:SamlProviderARNs,Oidc:OpenIdConnectProviderARNs}'"}]},"expected_output":"One get-user-pool-mfa-config response per user pool under the projection root user-pool-mfa: MfaConfiguration (OFF | ON | OPTIONAL) plus whichever factor blocks are configured — SoftwareTokenMfaConfiguration.Enabled for TOTP, SmsMfaConfiguration for SMS, EmailMfaConfiguration for email OTP. One describe-identity-pool response per identity pool under the projection root identity-pool: AllowUnauthenticatedIdentities (TRUE if the pool supports unauthenticated logins), AllowClassicFlow, and the four trust fields — CognitoIdentityProviders, SupportedLoginProviders (provider name to app id), SamlProviderARNs and OpenIdConnectProviderARNs.","cadence":"weekly","govcloud":"Amazon Cognito user pools and identity pools are available in both GovCloud (US) Regions, with differences that change what this collection sees and how it is wired: Cognito in GovCloud uses FIPS endpoints only (cognito-idp-fips.us-gov-west-1.amazonaws.com and cognito-idp-fips.us-gov-east-1.amazonaws.com), custom domains for user pools are not available, and Amazon Cognito Sync is absent. Identity-pool role trust policies must grant AssumeRoleWithWebIdentity to the cognito-identity-us-gov.amazonaws.com service principal in GovCloud (US-West) and to cognito-identity.us-gov-east-1.amazonaws.com in GovCloud (US-East) — a trust policy copied from a commercial account names the wrong principal and the pool silently issues nothing. In GovCloud (US-East), role name plus role session name longer than 24 characters can stop an identity pool assuming the role at all. Pool and provider ARNs use partition arn:aws-us-gov.","automatable":"partial","notes":"Drive both loops from the list calls, not from whatever the describe calls happen to return, so a pool nobody configured is a visible failure rather than an absent row.\n\nAllowUnauthenticatedIdentities is the assertion that carries the control: an identity pool with guest access on hands AWS credentials to a caller who never authenticated, which is precisely the population IA-08 exists to exclude. It is the one setting here that can silently undo everything upstream, which is why the cadence is weekly rather than quarterly.\n\nMfaConfiguration is asserted against ON rather than merely not-OFF on purpose. OPTIONAL does not require anything — it delegates the decision to the client application, so a pool set to OPTIONAL and an application that never prompts is indistinguishable from OFF in this output. Scope the loop to the pools that actually serve non-organizational users: a pool used only as a directory for an internal service has no external population and asserting ON over it is a finding about the wrong thing.\n\nThat scoping sentence is why this recipe is partial rather than full, and the reason is worth stating plainly. Both assertions range over lists, and no AWS call reports which directory serves the external population — or whether external users reach the system through Cognito at all rather than through an API Gateway authorizer or an ALB fronting an IdP. On an account with no user pools and no identity pools, both assertions are vacuously true and the control is unevidenced. The second gap is the control's first verb: MfaConfiguration decides how strongly an external identity authenticates, never that it is UNIQUE to one human, and a pool with MFA ON and self-service sign-up admits as many accounts per person as they care to open. Both gaps close with a human naming the external population and the registration path, which is the definition of partial.\n\nWhat this does not reach is the external identity provider's own registration and proofing — whether the SAML or OIDC provider behind SamlProviderARNs vetted the human before issuing them a subject. That is IA-08(1), (2) and (4) territory and is evidenced by the provider's assurance-level attestation, not by any AWS call.","assertions":[{"field":"identity-pool.AllowUnauthenticatedIdentities","op":"eq","value":false,"controls":["ia-8"],"description":"No identity pool supports unauthenticated logins, so every non-organizational principal holding AWS credentials authenticated to get them."},{"field":"user-pool-mfa.MfaConfiguration","op":"eq","value":"ON","controls":["ia-8"],"description":"Every user pool the assessor names as serving non-organizational users requires MFA, so an external identity authenticates with a second factor rather than a password alone. This is the authentication limb of IA-08 only — uniqueness is not in this output."}],"references":[{"title":"AWS CLI: cognito-idp get-user-pool-mfa-config (MfaConfiguration OFF | ON | OPTIONAL; SoftwareTokenMfaConfiguration, SmsMfaConfiguration, EmailMfaConfiguration)","url":"https://docs.aws.amazon.com/cli/latest/reference/cognito-idp/get-user-pool-mfa-config.html"},{"title":"AWS CLI: cognito-idp list-user-pools","url":"https://docs.aws.amazon.com/cli/latest/reference/cognito-idp/list-user-pools.html"},{"title":"AWS CLI: cognito-identity describe-identity-pool (AllowUnauthenticatedIdentities — TRUE if the identity pool supports unauthenticated logins; SamlProviderARNs, OpenIdConnectProviderARNs, SupportedLoginProviders)","url":"https://docs.aws.amazon.com/cli/latest/reference/cognito-identity/describe-identity-pool.html"},{"title":"AWS CLI: cognito-identity list-identity-pools (IdentityPoolId, IdentityPoolName)","url":"https://docs.aws.amazon.com/cli/latest/reference/cognito-identity/list-identity-pools.html"},{"title":"Amazon Cognito in AWS GovCloud (US) — FIPS-only endpoints, no custom user pool domains, cognito-identity-us-gov.amazonaws.com trust principal, US-East role/session name length limit","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-cog.html"}],"source":"aws","tools":["Amazon Cognito","AWS IAM"]},{"id":"session-lifetime-and-reauthentication","ksi_ids":["KSI-IAM-ELP"],"control_ids":["ia-11"],"aws_services":["AWS IAM","AWS IAM Identity Center","AWS STS"],"evidence":"The configured ceiling on how long any credential stays valid before its holder must present an authenticator again — MaxSessionDuration on every IAM role, SessionDuration on every IAM Identity Center permission set, and the aws:MultiFactorAuthAge conditions in policy that expire an MFA-backed session independently of the session itself","collection":{"kind":"cli","commands":[{"name":"list-roles","run":"aws iam list-roles --query 'Roles[].{Role:RoleName,MaxSessionDuration:MaxSessionDuration}'"},{"name":"list-instances","run":"aws sso-admin list-instances --query 'Instances[].InstanceArn'"},{"name":"list-permission-sets","run":"aws sso-admin list-permission-sets --instance-arn <INSTANCE_ARN>"},{"name":"describe-permission-set","run":"aws sso-admin describe-permission-set --instance-arn <INSTANCE_ARN> --permission-set-arn <PERMISSION_SET_ARN> --query 'PermissionSet.{Name:Name,SessionDuration:SessionDuration}'"},{"name":"get-account-authorization-details","run":"aws iam get-account-authorization-details --query '{Managed:Policies[].PolicyVersionList[?IsDefaultVersion].Document,UserInline:UserDetailList[].UserPolicyList[].PolicyDocument,RoleInline:RoleDetailList[].RolePolicyList[].PolicyDocument,GroupInline:GroupDetailList[].GroupPolicyList[].PolicyDocument}'"}]},"expected_output":"Under the projection root roles, one row per IAM role with MaxSessionDuration in SECONDS — the API floor is 3600 and the ceiling 43200, and the value caps what DurationSeconds an AssumeRole call may ask for. Under permission-sets, one row per permission set with SessionDuration as an ISO-8601 duration string (PT1H, PT12H). From get-account-authorization-details, four blocks of policy documents — the default version of every managed policy under Managed, and every INLINE policy under UserInline, RoleInline and GroupInline — each URL-encoded per RFC 3986 and needing a decode before you can search it for aws:MultiFactorAuthAge, a numeric condition key measured in SECONDS since the principal was authorized using MFA. The inline blocks are the reason for the projection: an MFA-age condition written inline on one role is invisible to a query that reads managed policies alone, and reads as an absence rather than as a miss.","cadence":"quarterly","govcloud":"IAM, STS and IAM Identity Center are available in both GovCloud (US) Regions; role and permission-set ARNs use partition arn:aws-us-gov, with permission sets taking the form arn:aws-us-gov:sso:::permissionSet/<SSOInstanceID>/<PermissionSetID>. The Identity Center console, SDK and CLI must be reached over FIPS endpoints in GovCloud, and multi-Region Identity Center is not available there, so a single instance holds every permission set in scope.","automatable":"partial","notes":"IA-11 asks for re-authentication under organization-defined circumstances. Only one of those circumstances — elapsed time — is in any of these outputs, and even that arrives as a ceiling rather than as an event: MaxSessionDuration caps what a caller may request, it does not report that a session ended or that a human presented an authenticator again. The other circumstances FedRAMP expects an organization to name — a change of role, a change of authenticator, before executing a privileged function, after a defined idle period — have no field here at all. Deciding whether the configured ceilings match the documented circumstances is the human judgement, which is why this is partial and not full.\n\nTwo scope gaps worth writing into the assessment rather than discovering later. A console session reached through an external identity provider ends on that provider's session policy, and no AWS API reports it: the IdP's own configuration is the artifact. And aws:MultiFactorAuthAge is absent from the request context for federated identities and for requests signed with long-term access keys, so a policy that expires an MFA session governs exactly the principals using temporary MFA-backed credentials and silently governs nobody else — pair it with BoolIfExists on aws:MultiFactorAuthPresent so the not-present case denies rather than passes.\n\nA role left at the 3600-second default is not evidence of a decision; a role at 43200 is a decision someone should have written down. Read the distribution, not the extremes. Substitute the instance and permission-set ARNs, which the two list calls supply.","references":[{"title":"AWS CLI: iam list-roles (MaxSessionDuration in seconds; 3600 minimum, 43200 maximum)","url":"https://docs.aws.amazon.com/cli/latest/reference/iam/list-roles.html"},{"title":"AWS CLI: sso-admin describe-permission-set (SessionDuration — the length of time application user sessions are valid for, in the ISO-8601 standard)","url":"https://docs.aws.amazon.com/cli/latest/reference/sso-admin/describe-permission-set.html"},{"title":"AWS CLI: sso-admin list-instances (InstanceArn)","url":"https://docs.aws.amazon.com/cli/latest/reference/sso-admin/list-instances.html"},{"title":"IAM global condition keys: aws:MultiFactorAuthAge (seconds since the principal was authorized using MFA) and aws:MultiFactorAuthPresent (absent for federated identities and access-key-signed requests; use BoolIfExists)","url":"https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html"},{"title":"AWS CLI: iam get-account-authorization-details (policy documents are URL-encoded and must be decoded before inspection)","url":"https://docs.aws.amazon.com/cli/latest/reference/iam/get-account-authorization-details.html"},{"title":"AWS IAM Identity Center in AWS GovCloud (US) — permission set ARN pattern arn:aws-us-gov:sso:::permissionSet/, FIPS endpoints, no multi-Region support","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-sso.html"}],"source":"aws","tools":["AWS IAM","AWS IAM Identity Center","AWS STS"]},{"id":"temporary-account-automatic-revocation","ksi_ids":["KSI-IAM-AAM","KSI-IAM-SNU"],"control_ids":["ac-2.2"],"aws_services":["AWS Config","AWS IAM","AWS Systems Manager Automation"],"evidence":"The mechanism that ends a temporary or emergency account without anyone deciding to: the AWS Config rule that measures how long an IAM credential has gone unused, the period it is configured with, the remediation configuration proving the revocation fires automatically, and an empty non-compliant set showing nothing has outlived the period","collection":{"kind":"config-rule","commands":[{"name":"iam-user-unused-credentials-check-rule","run":"aws configservice describe-config-rules --config-rule-names iam-user-unused-credentials-check"},{"name":"iam-user-unused-credentials-check","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name iam-user-unused-credentials-check --compliance-types NON_COMPLIANT"},{"name":"iam-user-unused-credentials-check-remediation","run":"aws configservice describe-remediation-configurations --config-rule-names iam-user-unused-credentials-check"},{"name":"get-account-authorization-details","run":"aws iam get-account-authorization-details --filter User"}]},"expected_output":"The four responses are collected unprojected, so every field below is the name AWS returns and the name the assertions address. From describe-config-rules, ConfigRules[] with one entry: Source.SourceIdentifier IAM_USER_UNUSED_CREDENTIALS_CHECK, ConfigRuleState ACTIVE, and InputParameters as a JSON-formatted STRING (not an object) that must be parsed before maxCredentialUsageAge can be read from it — an int, a number of DAYS, defaulting to 90. From get-compliance-details-by-config-rule, an EvaluationResults array whose every entry is one IAM user still holding a password or an active access key unused beyond that period; empty is the passing shape. From describe-remediation-configurations, RemediationConfigurations[] — a list whose minimum length is ZERO, so a rule with no remediation attached returns an empty array rather than an error, which is why one assertion tests that the first entry exists at all before the others read Automatic, TargetType SSM_DOCUMENT and TargetId. From get-account-authorization-details, UserDetailList[] with one entry per IAM user carrying UserName, CreateDate as an ISO-8601 timestamp, and Tags as a list of key/value pairs.","cadence":"continuous","govcloud":"AWS Config and IAM_USER_UNUSED_CREDENTIALS_CHECK are available in both GovCloud (US) Regions — the rule's published exclusion list names no GovCloud Region — as is Systems Manager Automation. User and role ARNs, and the AutomationAssumeRole the runbook assumes, use partition arn:aws-us-gov. The rule reports on a global IAM resource type, so deploy it in exactly one Region: a periodic rule on a global type evaluates in every Region it is added to, and duplicating it duplicates the evaluations rather than the coverage.","automatable":"partial","notes":"AC-02 (02) asks for something narrower than it looks: not that temporary and emergency accounts are reviewed, but that they END on their own after a stated period. Most of that sentence is in this output. The period is the rule's maxCredentialUsageAge. The ending is the remediation configuration — Automatic true and TargetId AWSConfigRemediation-RevokeUnusedIAMUserCredentials, a runbook whose documented behaviour is to deactivate expired access keys and delete expired login profiles. That nothing has outlived the period is the empty NON_COMPLIANT set. A screenshot of a console page proves none of those; these four calls prove all of them.\n\nWhat they do not prove is the control's subject. The Config rule measures every IAM user in the account; nothing in this output says WHICH users are the temporary and emergency ones, and the control is about those. A human names that population — which is exactly why this recipe is partial and not full, and why the CreateDate assertion is written as a filter over your own tagging rather than as a claim over the account.\n\nTwo scope edges to write into the assessment rather than discover during it. The rule sees IAM users, so an account whose privileges live in an assumed role or an IAM Identity Center permission set is out of its reach — those expire by session duration instead, which is a different artifact (see the IA-11 recipe). And the rule measures INACTIVITY, not age: a temporary account that is used every day is compliant no matter how long ago it should have been closed. The fourth call is what narrows that gap, by reading CreateDate against your own tagging of which accounts were meant to be temporary.\n\nThat last check is only as complete as the tagging behind it, and no AWS Config rule can make it complete: REQUIRED_TAGS does not support AWS::IAM::User, so tag coverage over IAM users cannot be asserted from Config at all. Treat the tagging standard as a written control with a manual sample. Read the CreateDate assertion for what it is — a check over exactly the accounts your tagging labels, which passes without saying anything on an estate that labels none.\n\nThe first remediation assertion is not redundant with the two that follow it. describe-remediation-configurations returns a list whose minimum length is zero, so a rule with NO remediation attached returns an empty array, and a clause of the form 'every entry is Automatic' is true over it. Testing that the first entry exists is what turns 'the revocation fires by itself' from a claim into a field.\n\nThe 90 in the assertions is the AWS default, not a FedRAMP number. Replace it in both places with the period your SSP commits to, and keep the runbook's own MaxCredentialUsageAge parameter equal to the period of the rule that triggers it — AWS documents that discipline for the access-keys-rotated pairing (match MaxCredentialUsageAge to that rule's maxAccessKeyAge) rather than for this rule, but the failure it prevents is the same one: a mismatch means the detection window and the revocation window disagree silently. Re-evaluating the rule within 4 hours of its last evaluation returns the previous result, so collect on the rule's own cadence rather than on demand.","assertions":[{"field":"iam-user-unused-credentials-check-rule.ConfigRules[].ConfigRuleState","op":"eq","value":"ACTIVE","controls":["ac-2.2"],"description":"The rule that measures credential dormancy is evaluating, not deleting or stopped."},{"field":"iam-user-unused-credentials-check-rule.ConfigRules[].InputParameters.maxCredentialUsageAge","op":"lte","value":90,"controls":["ac-2.2"],"description":"The configured period is no longer than the organization-defined maximum; 90 is the AWS default — substitute the shorter period if your SSP states one. InputParameters arrives as a JSON string and must be parsed before this field exists."},{"field":"iam-user-unused-credentials-check.EvaluationResults","op":"count_eq","value":0,"controls":["ac-2.2"],"description":"No IAM user holds a password or an active access key that has gone unused beyond that period."},{"field":"iam-user-unused-credentials-check-remediation.RemediationConfigurations[0].TargetId","op":"exists","controls":["ac-2.2"],"description":"A remediation configuration is attached to the rule at all — the list is empty when none is, and every clause below it passes vacuously over an empty list."},{"field":"iam-user-unused-credentials-check-remediation.RemediationConfigurations[].Automatic","op":"eq","value":true,"controls":["ac-2.2"],"description":"The revocation fires on non-compliance without a human starting it — the word 'automatically' in the control, as a field."},{"field":"iam-user-unused-credentials-check-remediation.RemediationConfigurations[].TargetId","op":"eq","value":"AWSConfigRemediation-RevokeUnusedIAMUserCredentials","controls":["ac-2.2"],"description":"What fires is the runbook that deactivates expired access keys and deletes expired login profiles, not an alert that someone has to read."},{"field":"get-account-authorization-details.UserDetailList[].CreateDate","op":"max_age_days","value":90,"where":[{"field":"Tags[?Key=='AccountType'].Value","op":"in","value":["temporary","emergency"]}],"controls":["ac-2.2"],"description":"No IAM user your tagging marks as temporary or emergency is older than the same organization-defined period — the still-active account the dormancy rule cannot see."}],"references":[{"title":"AWS Config managed rule: iam-user-unused-credentials-check (IAM_USER_UNUSED_CREDENTIALS_CHECK; periodic; maxCredentialUsageAge default 90 days; re-evaluation within 4 hours has no effect)","url":"https://docs.aws.amazon.com/config/latest/developerguide/iam-user-unused-credentials-check.html"},{"title":"AWSConfigRemediation-RevokeUnusedIAMUserCredentials — revokes unused IAM passwords and active access keys, deactivates expired access keys, deletes expired login profiles; its documented parameter pairing is MaxCredentialUsageAge to the access-keys-rotated rule's maxAccessKeyAge","url":"https://docs.aws.amazon.com/systems-manager-automation-runbooks/latest/userguide/automation-aws-revoke-iam-user.html"},{"title":"AWS CLI: configservice describe-remediation-configurations (Automatic, TargetType SSM_DOCUMENT, TargetId, MaximumAutomaticAttempts)","url":"https://docs.aws.amazon.com/cli/latest/reference/configservice/describe-remediation-configurations.html"},{"title":"AWS CLI: configservice describe-config-rules (InputParameters is a JSON-formatted string; ConfigRuleState; Source.SourceIdentifier)","url":"https://docs.aws.amazon.com/cli/latest/reference/configservice/describe-config-rules.html"},{"title":"AWS Config managed rule: required-tags (REQUIRED_TAGS) — its supported resource types do not include AWS::IAM::User","url":"https://docs.aws.amazon.com/config/latest/developerguide/required-tags.html"},{"title":"IAM API: UserDetail (CreateDate and Tags on every user returned by GetAccountAuthorizationDetails)","url":"https://docs.aws.amazon.com/IAM/latest/APIReference/API_UserDetail.html"}],"source":"aws","tools":["AWS Config","AWS IAM","AWS Systems Manager Automation"]},{"id":"external-access-inventory-and-trust-boundary","ksi_ids":["KSI-SCR-MIT","KSI-IAM-ELP"],"control_ids":["ac-20"],"aws_services":["AWS IAM Access Analyzer","AWS Organizations","AWS Config"],"evidence":"The machine-generated inventory of every resource an external entity can reach — IAM Access Analyzer's active ExternalAccess findings — read against the declared zone of trust, so the terms-and-conditions review has a list to work from rather than a memory","collection":{"kind":"cli","commands":[{"name":"list-analyzers","run":"aws accessanalyzer list-analyzers --query 'analyzers[].{Name:name,Type:type,Status:status,Arn:arn,LastAnalyzedAt:lastResourceAnalyzedAt}'"},{"name":"list-findings-v2","run":"aws accessanalyzer list-findings-v2 --analyzer-arn <ANALYZER_ARN> --filter '{\"status\":{\"eq\":[\"ACTIVE\"]},\"findingType\":{\"eq\":[\"ExternalAccess\"]}}'"},{"name":"describe-organization","run":"aws organizations describe-organization --query 'Organization.{Id:Id,FeatureSet:FeatureSet,ManagementAccount:MasterAccountId}'"},{"name":"iam-external-access-analyzer-enabled","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name iam-external-access-analyzer-enabled --compliance-types NON_COMPLIANT"}]},"expected_output":"From list-analyzers, one row per analyzer with type ACCOUNT or ORGANIZATION for external access (the ACCOUNT_UNUSED_ACCESS, ORGANIZATION_UNUSED_ACCESS, ACCOUNT_INTERNAL_ACCESS and ORGANIZATION_INTERNAL_ACCESS types answer different questions and do not produce ExternalAccess findings) and status one of ACTIVE, CREATING, DISABLED, FAILED. From list-findings-v2, one finding per resource shared outside the zone of trust, each with resource, resourceType, resourceOwnerAccount, findingType, status, createdAt and analyzedAt. From describe-organization, the organization id, FeatureSet ALL or CONSOLIDATED_BILLING, and the management account id — the boundary an ORGANIZATION analyzer treats as internal. From the Config rule, an empty NON_COMPLIANT set means an external-access analyzer is enabled and ACTIVE in that Region.","cadence":"quarterly","govcloud":"IAM Access Analyzer and Organizations operate in GovCloud (US); analyzer and resource ARNs use partition arn:aws-us-gov. The Config managed rule IAM_EXTERNAL_ACCESS_ANALYZER_ENABLED is NOT available in either GovCloud (US) Region — its published availability excludes GovCloud (US-East) and GovCloud (US-West) — so in GovCloud the fourth call has no rule to query and the analyzer's own status field from list-analyzers is the evidence that it is enabled.","automatable":"partial","notes":"AC-20 is a two-limb control and only one limb is in this output. The limb that is here: which external entities can reach organization-controlled information. Access Analyzer answers that by logic-based reasoning over resource-based policies, and every access by a principal INSIDE the zone of trust is trusted by definition, so a finding is exactly an access that crosses the boundary the organization declared. The limb that is not here: whether each of those crossings is covered by terms and conditions consistent with the trust relationship. That is an agreement — a contract, an interconnection security agreement, an authorization to connect — and no call returns it. Attach the agreement register and reconcile it finding by finding; the reconciliation, not the finding list, is the AC-20 artifact.\n\nThe inventory also has a direction. Access Analyzer sees resources you share OUT. It does not see an external system your people use to process organization information — a SaaS tool reached from a workstation leaves no resource policy in your account and produces no finding. That half of AC-20 has to come from your own third-party register, and inventing a join between the two would make the coverage claim wider than the evidence.\n\nThree scope facts worth pinning down before the count is quoted. External-access analysis is REGIONAL: an analyzer evaluates only resources in the Region where it is enabled, so one analyzer per Region in use, or the inventory is silently partial. Fifteen resource types are analyzed for external access — S3 buckets and directory buckets, IAM roles, KMS keys, Lambda functions and layers, SQS queues, Secrets Manager secrets, SNS topics, EBS volume snapshots, RDS DB and DB cluster snapshots, ECR repositories, EFS file systems, DynamoDB streams and tables — and a resource type outside that list is not covered by the analyzer at all. And findings refresh within about 30 minutes of a policy change but can lag up to 24 hours when a change notification is missed, so a finding list is a recent state, not a live one.","references":[{"title":"Using IAM Access Analyzer — zone of trust, ExternalAccess findings, the fifteen analyzed resource types, and the regional scope of external-access analysis","url":"https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html"},{"title":"AWS CLI: accessanalyzer list-analyzers (type ACCOUNT | ORGANIZATION | *_UNUSED_ACCESS | *_INTERNAL_ACCESS; status ACTIVE | CREATING | DISABLED | FAILED)","url":"https://docs.aws.amazon.com/cli/latest/reference/accessanalyzer/list-analyzers.html"},{"title":"AWS CLI: accessanalyzer list-findings-v2 (filterable on status and findingType; returns resource, resourceType, resourceOwnerAccount, analyzedAt)","url":"https://docs.aws.amazon.com/cli/latest/reference/accessanalyzer/list-findings-v2.html"},{"title":"AWS Config managed rule: iam-external-access-analyzer-enabled (IAM_EXTERNAL_ACCESS_ANALYZER_ENABLED) — unavailable in both AWS GovCloud (US) Regions","url":"https://docs.aws.amazon.com/config/latest/developerguide/iam-external-access-analyzer-enabled.html"},{"title":"AWS CLI: organizations describe-organization (Id, FeatureSet, management account; AvailablePolicyTypes is deprecated — use list-roots instead)","url":"https://docs.aws.amazon.com/cli/latest/reference/organizations/describe-organization.html"}],"source":"aws","tools":["AWS IAM Access Analyzer","AWS Organizations","AWS Config"]},{"id":"session-authenticity-tls-termination","ksi_ids":["KSI-SVC-SIN","KSI-SVC-VCM"],"control_ids":["sc-23"],"aws_services":["Elastic Load Balancing","Amazon CloudFront","AWS Certificate Manager","AWS Config"],"evidence":"Every place a client session terminates, with the protocol and negotiated policy it terminates under: listener protocol and SslPolicy on each load balancer, HTTP listeners that redirect rather than serve, an ACM certificate behind each one, and CloudFront viewer policies that refuse plain HTTP","collection":{"kind":"cli","commands":[{"name":"describe-load-balancers","run":"aws elbv2 describe-load-balancers --query 'LoadBalancers[].{LB:LoadBalancerName,Arn:LoadBalancerArn,Type:Type,Scheme:Scheme}'"},{"name":"describe-listeners","run":"aws elbv2 describe-listeners --load-balancer-arn <LOAD_BALANCER_ARN> --query 'Listeners[].{Port:Port,Protocol:Protocol,SslPolicy:SslPolicy,Certificates:Certificates[].CertificateArn,Action:DefaultActions[0].Type,Redirect:DefaultActions[0].RedirectConfig}'"},{"name":"describe-ssl-policies","run":"aws elbv2 describe-ssl-policies --query \"SslPolicies[?contains(Name,'FIPS')].Name\""},{"name":"alb-http-to-https-redirection-check","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name alb-http-to-https-redirection-check --compliance-types NON_COMPLIANT"},{"name":"cloudfront-viewer-policy-https","run":"aws configservice get-compliance-details-by-config-rule --config-rule-name cloudfront-viewer-policy-https --compliance-types NON_COMPLIANT"}]},"expected_output":"From describe-listeners, one row per listener: Protocol from HTTP, HTTPS, TCP, TLS, UDP, TCP_UDP, GENEVE, QUIC or TCP_QUIC; SslPolicy present only on HTTPS and TLS listeners and naming a policy such as ELBSecurityPolicy-TLS13-1-2-2021-06 (TLS 1.3 and 1.2 only) or ELBSecurityPolicy-TLS13-1-2-FIPS-2023-04; Certificates carrying the default certificate ARN; and for a plain HTTP listener a DefaultActions entry of Type redirect whose RedirectConfig names Protocol HTTPS with StatusCode HTTP_301. Note the CLI default: a listener created by CLI, CloudFormation or CDK without an explicit policy gets ELBSecurityPolicy-2016-08, which still negotiates TLS 1.0. From the two Config rules, empty NON_COMPLIANT sets mean no Application Load Balancer serves an HTTP listener without redirection and no CloudFront distribution leaves ViewerProtocolPolicy at allow-all. Run the CloudFront call in US East (N. Virginia): CLOUDFRONT_VIEWER_POLICY_HTTPS evaluates only there, matching CloudFront's global scope, so the same command in any other Region returns nothing at all — a silence that reads exactly like a pass and is not one.","cadence":"quarterly","govcloud":"Elastic Load Balancing, the security policies including the FIPS families, and AWS Certificate Manager are available in GovCloud (US); load-balancer and certificate ARNs use partition arn:aws-us-gov. ALB_HTTP_TO_HTTPS_REDIRECTION_CHECK is available in all supported Regions. Two rules in this family are not usable there: CLOUDFRONT_VIEWER_POLICY_HTTPS evaluates only in US East (N. Virginia), matching CloudFront's global scope and leaving GovCloud with no CloudFront distributions to evaluate, and ELBV2_ACM_CERTIFICATE_REQUIRED is excluded from both GovCloud (US) Regions — so in GovCloud the certificate check comes from the Certificates field of describe-listeners directly, not from Config.","automatable":"partial","notes":"SC-23 says protect the authenticity of communications sessions, and authenticity has a floor and a ceiling. The floor is transport: a session that can be read or injected into on the wire is not authentic in any sense, and the floor is entirely in this output — which listeners terminate TLS, under which negotiated policy, with which certificate, and whether the HTTP door redirects instead of answering. The ceiling is session identity above the transport: regenerating a session identifier on privilege change, invalidating it at logout, binding it to the principal it was issued to, refusing one that was replayed. That lives in application code and emits no AWS telemetry, which is what keeps this partial. Say which half you are evidencing, or a reader will assume the larger one.\n\nRead SslPolicy as a version floor, not a checkmark. The policy name encodes the oldest protocol it will negotiate: ELBSecurityPolicy-TLS13-1-2-2021-06 admits TLS 1.3 and 1.2 only, while ELBSecurityPolicy-TLS13-1-0-2021-06 and the 2016-08 default still admit TLS 1.0. The FIPS families — listed by the describe-ssl-policies query above — use the AWS-LC FIPS validated module, and AWS marks ELBSecurityPolicy-TLS13-1-1-FIPS-2023-04 and ELBSecurityPolicy-TLS13-1-0-FIPS-2023-04 as legacy compatibility only, FIPS cryptography that, in AWS's own words, may not conform to the latest NIST guidance for TLS configuration. FIPS in the name is therefore not by itself the answer to a FIPS question.\n\nThe redirection rule is narrower than it sounds: ALB_HTTP_TO_HTTPS_REDIRECTION_CHECK is NON_COMPLIANT both when an HTTP listener has no redirect and when it forwards to another HTTP listener instead of redirecting — but it covers Application Load Balancers only. Network Load Balancer TLS listeners, API Gateway, and anything terminating TLS on an instance are outside it and have to be enumerated by hand.","references":[{"title":"AWS CLI: elbv2 describe-listeners (Protocol, SslPolicy, Certificates, DefaultActions with RedirectConfig Protocol/Port/StatusCode)","url":"https://docs.aws.amazon.com/cli/latest/reference/elbv2/describe-listeners.html"},{"title":"Security policies for your Application Load Balancer — protocols by policy, the FIPS families on the AWS-LC validated module, the legacy-only TLS13-1-1/1-0 FIPS policies, and the ELBSecurityPolicy-2016-08 CLI default","url":"https://docs.aws.amazon.com/elasticloadbalancing/latest/application/describe-ssl-policies.html"},{"title":"AWS Config managed rule: alb-http-to-https-redirection-check (ALB_HTTP_TO_HTTPS_REDIRECTION_CHECK; also NON_COMPLIANT when an HTTP listener forwards to an HTTP listener)","url":"https://docs.aws.amazon.com/config/latest/developerguide/alb-http-to-https-redirection-check.html"},{"title":"AWS Config managed rule: cloudfront-viewer-policy-https (CLOUDFRONT_VIEWER_POLICY_HTTPS; NON_COMPLIANT when ViewerProtocolPolicy is allow-all; evaluates only in US East (N. Virginia))","url":"https://docs.aws.amazon.com/config/latest/developerguide/cloudfront-viewer-policy-https.html"},{"title":"AWS Config managed rule: elbv2-acm-certificate-required (ELBV2_ACM_CERTIFICATE_REQUIRED) — excluded from both AWS GovCloud (US) Regions","url":"https://docs.aws.amazon.com/config/latest/developerguide/elbv2-acm-certificate-required.html"}],"source":"aws","tools":["Elastic Load Balancing","Amazon CloudFront","AWS Certificate Manager","AWS Config"]},{"id":"change-authority-restrictions-and-enforcement","ksi_ids":["KSI-CMT-RMV","KSI-IAM-JIT"],"control_ids":["cm-5"],"aws_services":["AWS Organizations","AWS IAM"],"evidence":"The enforced half of who may change what: the service control policy type actually enabled in the organization root, the customer-authored SCPs and the roots, OUs and accounts each one is attached to, and the permissions boundary carried by every principal your own tagging marks as a change authority","collection":{"kind":"cli","commands":[{"name":"describe-organization","run":"aws organizations describe-organization"},{"name":"list-roots","run":"aws organizations list-roots"},{"name":"list-policies","run":"aws organizations list-policies --filter SERVICE_CONTROL_POLICY"},{"name":"describe-policy","run":"aws organizations describe-policy --policy-id <CUSTOMER_AUTHORED_SCP_ID>"},{"name":"list-targets-for-policy","run":"aws organizations list-targets-for-policy --policy-id <CUSTOMER_AUTHORED_SCP_ID>"},{"name":"get-account-authorization-details","run":"aws iam get-account-authorization-details --filter User Role"}]},"expected_output":"All six responses are collected unprojected, so every field below is the name AWS returns and the name the assertions address. The first three calls take no argument; calls four and five take a policy id that MUST be one of the AwsManaged false ids returned by call three — a human substitution the assertion grammar cannot make, and the reason the placeholder is named CUSTOMER_AUTHORED_SCP_ID rather than POLICY_ID. Call six must be issued with MANAGEMENT-ACCOUNT credentials: Organizations operations may also be called from a member account designated as a delegated administrator, and a collection run there returns that member account's principals while reading exactly like the management account's. From describe-organization, an Organization object with Id, Arn, MasterAccountId, MasterAccountEmail and FeatureSet, which is ALL or CONSOLIDATED_BILLING; AvailablePolicyTypes is also returned and is DEPRECATED by AWS, which documents that it omits every policy type other than SCPs and directs you to ListRoots instead. From list-roots, Roots[] with Id, Arn, Name and PolicyTypes[], each entry carrying Type and a Status that is ENABLED, PENDING_ENABLE or PENDING_DISABLE — there is no DISABLED value, because a policy type that is off is ABSENT from the list rather than reported as off. From list-policies, Policies[] with Id, Arn, Name, Description, Type and AwsManaged, a boolean that is true for policies you cannot edit; AWS attaches the managed FullAWSAccess policy to every root, OU and account when it is created, so the list is never empty on an organization with SCPs enabled and its length says nothing about whether anyone has authored a restriction. From describe-policy, a Policy object with a PolicySummary carrying the same six fields as a list entry, and Content — the policy document itself, returned as a JSON-formatted STRING that must be parsed before any statement in it can be read. From list-targets-for-policy, Targets[] with TargetId, Arn, Name and a Type of ACCOUNT, ORGANIZATIONAL_UNIT or ROOT. From get-account-authorization-details, UserDetailList[] and RoleDetailList[] with UserName/RoleName, CreateDate, Tags, the attached and inline policy lists, and PermissionsBoundary — an optional AttachedPermissionsBoundary object carrying PermissionsBoundaryType and PermissionsBoundaryArn, absent entirely on a principal that has none.","cadence":"continuous","govcloud":"AWS Organizations is available in both AWS GovCloud (US) Regions and SCPs are one of the policy types a GovCloud organization may use, alongside RCPs, tag policies and declarative policies for EC2 and S3; backup, chat application and AI services opt-out policies cannot be created there. Organization, root, OU, account, policy and IAM ARNs use partition arn:aws-us-gov. Three GovCloud facts change how this recipe is run rather than what it means. All features are MANDATORY — the consolidated billing feature set is not offered — so FeatureSet reads ALL in every GovCloud organization and the first assertion is satisfied by the Region rather than by a decision anyone made. The SECOND call is the one with a Region constraint: AWS restricts any operation that references the organization root, naming ListRoots as its example, to the AWS GovCloud (US-West) Region, so list-roots must be issued against us-gov-west-1 regardless of where the workload runs. The other five calls carry no such restriction. And a GovCloud organization is INDEPENDENT of the commercial organization its accounts are paired with: SCPs attached in the commercial organization do not restrict the GovCloud accounts, and an assessor handed a commercial organization's policy list has been handed evidence about a different boundary.","automatable":"partial","notes":"CM-5 asks for physical AND logical access restrictions associated with changes that are defined, documented, approved and enforced. This recipe reaches one adjective and one verb.\n\nThe adjective it does not reach is PHYSICAL. No AWS API returns anything about physical access to the hardware a change is made on; under the shared responsibility model that half belongs to the IaaS provider and is inherited — read it from the provider's own authorization package and the FedRAMP customer responsibility matrix, not from this output. A collection that presents these six calls as CM-5 evidence without saying so has answered half a control and labelled it whole.\n\nThe verb it does reach is ENFORCE, and it reaches it properly: an SCP is not a description of a restriction, it is the restriction, evaluated by AWS on every request from every member account, and list-targets-for-policy is the difference between a policy that exists and a policy that applies to something.\n\nTwo gaps keep this partial, not one. The first is approval — that the enforced set IS the documented and approved set is a comparison against a change-management record, and no call returns it. The second is subject matter, and it is the easier one to miss: the assertions below show THAT a customer-authored SCP is enforced, never WHAT it restricts. An SCP denying mechanicalturk:* satisfies every clause here exactly as well as one denying ec2:ModifyInstanceAttribute. The fourth call exists to close that by putting the policy document in the evidence — Content is returned as a JSON string and must be parsed — but no assertion can grade a policy document, so the phrase that makes this CM-5 rather than AC-3, 'associated with changes', stays a human read. Both gaps are reconciliations against your own records; the reconciliation is the CM-5 artifact and this output is the column it is reconciled against.\n\nFour ways this evidence is vacuous if it is read naively. Three are behaviour AWS documents in as many words; the fourth is an inference from a field being a list, flagged as such rather than dressed up as a citation. A policy type that has never been enabled is ABSENT from Roots[].PolicyTypes rather than present with a Status of DISABLED — the enum has no such value — so a clause of the form 'every SCP policy type entry is ENABLED' is true over an organization where SCPs were never turned on at all, which is why one assertion tests that the entry exists before another reads it. AWS attaches the managed FullAWSAccess policy to EVERY root, OU and account when it is created, so both a non-empty Policies[] and a non-empty Targets[] are the default state of a working organization rather than a restriction anyone wrote — which is why one assertion counts only the AwsManaged false subset, and why calls four and five are bound to a customer-authored policy id instead of any policy id. FeatureSet CONSOLIDATED_BILLING makes SCPs unavailable outright, so on such an organization every policy in the list is inert. And an SCP attached to nothing returns an empty Targets[]: that one AWS does not state, it follows from Targets being a list, and the assertion that rests on it should be read as an inference.\n\nThe scope edge that matters most to an assessor is that SCPs do not restrict the management account. AWS states this three times on its own page and lists it first among the tasks SCPs cannot restrict: SCPs affect only member accounts, including member accounts designated as delegated administrators, and they have no effect on users or roles in the management account. The account with the broadest reach over the organization is the one account this evidence says nothing about, and its change restrictions have to come from identity-based policy and permission boundaries inside it — which is what the sixth call collects, and why the sixth call has to be run there. SCPs also do not affect service-linked roles at all, and they do not affect principals from accounts outside the organization even when a resource-based policy in your account grants those principals access.\n\nRead an SCP for what it is: a ceiling, never a grant. AWS is explicit that no permissions are granted by an SCP and that effective permissions are the intersection of what the SCP allows with what identity-based and resource-based policies allow — a principal with no IAM permissions has no access under the most permissive SCP in the world. Where a permissions boundary is also present, AWS documents that the boundary, the SCP and the identity-based policy must ALL allow the action, which is why the sixth call is collected beside the first five rather than instead of them.\n\nOne failure mode worth writing into the assessment because it is silent and total: disabling the SCP policy type in a root automatically detaches every SCP from every OU, account and organization in that root, and re-enabling it does not restore the attachments — the root reverts to FullAWSAccess alone and the previous attachments are lost and not automatically recoverable. After such an event list-policies still returns every authored policy, unchanged, while nothing is enforced anywhere. The Roots[].PolicyTypes reading and the Targets[] reading are what separate those two worlds, and a collection that skips them cannot tell them apart.\n\nThe permissions-boundary assertion is scoped by your own tagging rather than written as a claim over the account, for the same reason as in the AC-02 (02) recipe: no AWS call knows which of your roles are supposed to be the change authorities. It passes without saying anything on an estate that tags none, so treat the tagging standard as a written control with a manual sample rather than as coverage.\n\nWhat actually happened, as opposed to what was permitted, is a different recipe: CloudTrail's non-read-only event history, collected under CM-03 alongside the Config resource timeline and the Systems Manager change-request records. This one is about the restriction; that one is about the change.","assertions":[{"field":"describe-organization.Organization.FeatureSet","op":"eq","value":"ALL","controls":["cm-5"],"description":"The organization has all features enabled. SCPs are available only in an organization with all features enabled; on CONSOLIDATED_BILLING every policy this recipe collects is inert. Mandatory, and therefore uninformative, in GovCloud."},{"field":"list-roots.Roots[0].PolicyTypes[?Type=='SERVICE_CONTROL_POLICY'] | [0].Status","op":"exists","controls":["cm-5"],"description":"The SCP policy type appears in the root at all. A policy type that is off is absent from PolicyTypes[] rather than carrying a DISABLED status — the enum has no such value — so the clause below passes vacuously over an organization where SCPs were never enabled. The pipe is load-bearing: without it the [0] indexes each matched string rather than the filtered list, and the expression is empty whether the entry is present or not."},{"field":"list-roots.Roots[].PolicyTypes[?Type=='SERVICE_CONTROL_POLICY'].Status","op":"eq","value":"ENABLED","controls":["cm-5"],"description":"The SCP policy type is enabled rather than mid-transition; PENDING_DISABLE is an organization on its way to enforcing nothing."},{"field":"list-policies.Policies[?AwsManaged==`false`] | [0].Id","op":"exists","controls":["cm-5"],"description":"At least one SCP in the organization was authored by you. AWS attaches the managed FullAWSAccess policy to every root, OU and account at creation, so a non-empty Policies[] is the default state of a working organization and not a restriction anyone wrote. The pipe is load-bearing for the same reason as the root clause above."},{"field":"list-targets-for-policy.Targets[0].TargetId","op":"exists","controls":["cm-5"],"description":"The customer-authored SCP passed to call five is attached to a root, an OU or an account. This clause proves attachment ONLY because the command is bound to an AwsManaged false id: run against any policy id it is satisfied by FullAWSAccess, which AWS attaches to every root, OU and account at creation. Binding it is a human step — the grammar compares a field to a constant, never one command's output to another's — so a collector that substitutes an arbitrary policy id has produced a green result about the AWS default."},{"field":"get-account-authorization-details.RoleDetailList[?not_null(Tags[?Key=='ChangeAuthority' && Value=='true'] | [0]) && !PermissionsBoundary]","op":"count_eq","controls":["cm-5"],"description":"No role your own tagging marks as a change authority is missing a permissions boundary — the per-principal ceiling that applies inside the management account, where SCPs do not reach. Written as an OFFENDER LIST rather than as 'every role has a boundary', because a field projection over a list DROPS the rows that lack the field instead of reporting them false, so the every-row form silently answers about the compliant rows only. Zero on an estate that tags no roles, so read it beside the tag count. The call must be run with management-account credentials or it describes a different account's roles.","value":0}],"references":[{"title":"AWS Organizations User Guide: service control policies — SCPs require all features enabled, never grant permissions, do not affect the management account or service-linked roles, and disabling the policy type detaches every SCP irrecoverably","url":"https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps.html"},{"title":"AWS Organizations User Guide: SCP evaluation — AWS attaches the managed FullAWSAccess policy to every root, OU and account when it is created, and SCP evaluation is deny-by-default with allow required at every level","url":"https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps_evaluation.html"},{"title":"AWS CLI: organizations list-roots (Roots[].PolicyTypes[] with Type and Status ENABLED | PENDING_ENABLE | PENDING_DISABLE — no DISABLED value)","url":"https://docs.aws.amazon.com/cli/latest/reference/organizations/list-roots.html"},{"title":"AWS CLI: organizations list-policies (--filter SERVICE_CONTROL_POLICY; Policies[] carries AwsManaged, true for policies you can attach but cannot edit)","url":"https://docs.aws.amazon.com/cli/latest/reference/organizations/list-policies.html"},{"title":"AWS CLI: organizations list-targets-for-policy (Targets[] with TargetId, Arn, Name and Type ACCOUNT | ORGANIZATIONAL_UNIT | ROOT)","url":"https://docs.aws.amazon.com/cli/latest/reference/organizations/list-targets-for-policy.html"},{"title":"AWS CLI: organizations describe-organization (FeatureSet ALL | CONSOLIDATED_BILLING; AvailablePolicyTypes is deprecated — use ListRoots)","url":"https://docs.aws.amazon.com/cli/latest/reference/organizations/describe-organization.html"},{"title":"IAM API: RoleDetail — PermissionsBoundary is an optional AttachedPermissionsBoundary (PermissionsBoundaryType, PermissionsBoundaryArn) returned by GetAccountAuthorizationDetails","url":"https://docs.aws.amazon.com/IAM/latest/APIReference/API_RoleDetail.html"},{"title":"AWS GovCloud (US) User Guide: AWS Organizations — all features mandatory, ListRoots and root-referencing operations only in GovCloud (US-West), GovCloud organizations independent of commercial ones, and the permitted policy types","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-organizations.html"},{"title":"AWS CLI: organizations describe-policy (Policy.PolicySummary with Id, Arn, Name, Description, Type, AwsManaged; Policy.Content is the policy document as a string)","url":"https://docs.aws.amazon.com/cli/latest/reference/organizations/describe-policy.html"},{"title":"AWS CLI: iam get-account-authorization-details (--filter possible values User | Role | Group | LocalManagedPolicy | AWSManagedPolicy; the page describes the parameter as a comma-separated list while the CLI synopsis passes them space-separated, which is the form used here)","url":"https://docs.aws.amazon.com/cli/latest/reference/iam/get-account-authorization-details.html"}],"source":"aws","tools":["AWS Organizations","AWS IAM"]},{"id":"internal-connection-inventory-and-authorization","ksi_ids":["KSI-CNA-ULN"],"control_ids":["ca-9"],"aws_services":["Amazon VPC","AWS Transit Gateway","AWS PrivateLink"],"evidence":"The machine-generated inventory of every plane on which one part of the system reaches another — VPC peering connections, Transit Gateway attachments, the interface and gateway endpoints this account consumes, the endpoint connections other accounts have made INTO your endpoint service, and the security-group rules that name another group rather than a CIDR — each narrowed to the states that are actually live","collection":{"kind":"cli","commands":[{"name":"describe-vpc-peering-connections","run":"aws ec2 describe-vpc-peering-connections"},{"name":"describe-transit-gateway-attachments","run":"aws ec2 describe-transit-gateway-attachments"},{"name":"describe-vpc-endpoints","run":"aws ec2 describe-vpc-endpoints"},{"name":"describe-vpc-endpoint-connections","run":"aws ec2 describe-vpc-endpoint-connections"},{"name":"describe-security-group-rules","run":"aws ec2 describe-security-group-rules"}]},"expected_output":"All five responses are collected unprojected and per-Region — every one of these is a Regional call, so an inventory taken in one Region is an inventory of one Region. From describe-vpc-peering-connections, VpcPeeringConnections[] with VpcPeeringConnectionId, Tags, ExpirationTime, and RequesterVpcInfo and AccepterVpcInfo each carrying OwnerId, VpcId, Region, CidrBlock and CidrBlockSet — the account on the far side is OwnerId, and it is the field that says whether a connection is internal at all. Status.Code is one of initiating-request, pending-acceptance, active, deleted, rejected, failed, expired, provisioning, deleting: the list is a history, not a live inventory, and only `active` is a connection. From describe-transit-gateway-attachments, TransitGatewayAttachments[] with TransitGatewayAttachmentId, TransitGatewayId, TransitGatewayOwnerId, ResourceOwnerId, ResourceId, CreationTime, Tags, an Association carrying TransitGatewayRouteTableId and its own State, a ResourceType of vpc, vpn, vpn-concentrator, direct-connect-gateway, connect, peering, tgw-peering (deprecated) or client-vpn, and a State among initiating (deprecated), initiatingRequest, pendingAcceptance, rollingBack, pending, available, modifying, deleting, deleted, failed, rejected, rejecting and failing — `available` is the live one, and `pendingAcceptance` is a cross-account attachment nobody has adjudicated. From describe-vpc-endpoints, VpcEndpoints[] with VpcEndpointId, VpcEndpointType (Interface, Gateway, GatewayLoadBalancer, Resource, ServiceNetwork), VpcId, ServiceName, OwnerId, CreationTimestamp, PolicyDocument, PrivateDnsEnabled, RouteTableIds for gateway endpoints, SubnetIds and Groups for interface endpoints, Tags, and a State among PendingAcceptance, Pending, Available, Deleting, Deleted, Rejected, Failed, Expired and Partial. From describe-vpc-endpoint-connections — the provider side, and the only one of the five that shows connections INTO you — VpcEndpointConnections[] with ServiceId, VpcEndpointId, VpcEndpointOwner (the consumer's account id), CreationTimestamp, DnsEntries, IpAddressType and a VpcEndpointState of PendingAcceptance, Pending, Available, Deleting, Deleted, Rejected, Failed, Expired or Partial — PascalCase, unlike the Transit Gateway attachment state beside it, and unlike the lowercase forms the CLI page lists as --filters ARGUMENT values. From describe-security-group-rules, SecurityGroupRules[] with SecurityGroupRuleId, SecurityGroupRuleArn, GroupId, IsEgress, IpProtocol, FromPort, ToPort, Description, Tags, and a peer field naming what the rule permits traffic to or from — CidrIpv4, CidrIpv6, PrefixListId or ReferencedGroupInfo, and IsEgress is what says which direction that peer sits in; a rule carrying ReferencedGroupInfo is one component reaching another by identity rather than by address, which is the internal connection CA-09 is about.","cadence":"continuous","govcloud":"Amazon VPC, Transit Gateway and PrivateLink all operate in both AWS GovCloud (US) Regions and every call here is available; VPC, endpoint, attachment and security-group ARNs use partition arn:aws-us-gov. Two documented GovCloud differences touch this recipe and neither blocks it. Security group rule IDs are not shown in the Amazon VPC CONSOLE in GovCloud — the API returns SecurityGroupRuleId normally, so collect this evidence through the CLI and do not expect a reviewer to reconcile it against a console screenshot. And not all VPC endpoints in GovCloud support VPC endpoint policies, so PolicyDocument may be absent on an endpoint where the commercial Regions would carry one; read its absence as an unsupported endpoint type rather than as a removed control, and say which it was. Note also that VPC metadata in GovCloud is not permitted to contain export-controlled data, and AWS names security group rule descriptions, tag keys and values, and VPC endpoint service names among the free-text fields this applies to — which is a constraint on the very fields this recipe asks you to write an authorization reference into.","automatable":"partial","notes":"CA-09 has four elements and this output serves parts of two of them. The part it holds: which internal connections exist. Every plane on which one component of the system reaches another leaves a resource behind — a peering connection, a Transit Gateway attachment, an endpoint, an endpoint connection, or a security-group rule that names another group instead of a CIDR — and five calls enumerate them completely for a Region. That enumeration is worth having on its own: it is the list the control's documentation requirement is written against, and most providers do not have it.\n\nWhat it does not hold is everything CA-09 asks you to record about each connection. The interface characteristics, the security AND PRIVACY requirements agreed for the connection, the nature of the information communicated, and the authorization to connect at all — none of them is a field. Two further elements of the control are not addressed here at all and should not be read as covered: terminating internal connections when the stated conditions are met, and reviewing the continued need for each connection on a stated frequency. The second of those is the one this inventory most nearly serves — a continuously collected enumeration is the substrate a periodic review runs on — but the review itself is a decision with a date and a reviewer, and none of that is in the response. They live in an interconnection agreement or an internal approval record, and the reconciliation of that record against this inventory is the CA-09 artifact. This recipe produces the column you reconcile against, which is why it is partial and why no assertion below claims more than attachment and state.\n\nRead every one of these lists as a history rather than an inventory. Peering connections persist in the response with Status.Code deleted, rejected, expired or failed; Transit Gateway attachments persist as deleted, failed or rejected; endpoints persist as Deleted or Rejected. A count taken over the raw response counts connections that do not exist, and — the direction that actually matters — a clause of the form 'every connection carries an authorization tag' evaluated over the raw response is answering about the dead ones too. Every assertion here is therefore narrowed by state, and the narrowing is the assertion.\n\nThe completeness clauses are written as OFFENDER LISTS — a filter that selects the rows which fail, asserted to be empty — and not as 'every connection carries a tag'. This is not a style preference. A field projection over a list drops the rows where the field is absent instead of reporting them as false, so an every-row clause of the obvious form evaluates only the rows that already comply and is green on an estate where half the connections are untagged. The offender form was verified against synthetic responses before it was written down.\n\nThe vacuity to state plainly: all five lists are empty on an account that has none of these, and an every-entry clause over an empty list is true. This evidence cannot distinguish 'no internal connections' from 'no connections in THIS Region' from 'the collection ran with a role that cannot see them'. Record the Region set the collection covered beside the output, because every one of these calls is Regional and nothing in the response says which Region produced it.\n\nTwo directions, and only one of them is intuitive. Four of the five calls show what this account reaches OUT to. describe-vpc-endpoint-connections is the other direction — it is the provider-side view of who has connected IN to an endpoint service you publish — and a pendingAcceptance entry there is a connection request nobody has adjudicated. The same is true of a peering connection in pending-acceptance: an unaccepted request is not yet a connection, but it is a decision someone owes, and it belongs in the review the KSIs describe as persistent rather than in a quarterly surprise.\n\nOwnerId and ResourceOwnerId are what make 'internal' checkable at all, and they are also the join this grammar cannot do: deciding whether a peer account is inside the authorization boundary means comparing OwnerId against your own account list, and an assertion compares a field to a constant. Write the boundary's account set into the assessment and reconcile by hand; a tag naming the authorization record is the closest a field gets, which is what the assertions ask for.\n\nSecurity-group rules are included for a reason worth stating: they are the only one of the five that shows a connection with no resource of its own. A rule whose ReferencedGroupInfo names another security group is one component permitted to reach another by identity, and it is invisible to any inventory that looks only for peering connections and attachments. Description is free text and no AWS call validates it, so treat the description assertion as a documentation-completeness check rather than as evidence about the connection itself.","assertions":[{"field":"describe-vpc-peering-connections.VpcPeeringConnections[?Status.Code=='active'] | [?!not_null(Tags[?Key=='ConnectionAuthorization'] | [0])]","op":"count_eq","value":0,"controls":["ca-9"],"description":"No ACTIVE peering connection is missing the tag naming the authorization record that permitted it. Narrowed by state because the response also carries deleted, rejected, expired and failed connections. Zero on an account with no peering connections, so read it beside the length of the list, never instead of it."},{"field":"describe-vpc-peering-connections.VpcPeeringConnections[?Status.Code=='pending-acceptance'] | [0].VpcPeeringConnectionId","op":"not_exists","controls":["ca-9"],"description":"No peering request is sitting unaccepted. An unaccepted request is not a connection, but it is an authorization decision nobody has made, and ExpirationTime means it resolves itself by lapsing if the review never happens. The pipe is load-bearing: without it the [0] indexes each matched object rather than the filtered list, and the expression is empty whether such a request exists or not."},{"field":"describe-transit-gateway-attachments.TransitGatewayAttachments[?State=='available'] | [?!not_null(Tags[?Key=='ConnectionAuthorization'] | [0])]","op":"count_eq","value":0,"controls":["ca-9"],"description":"No AVAILABLE Transit Gateway attachment is missing its authorization tag. ResourceOwnerId is what says whether the far side is inside the boundary, and no assertion can compare it to your account list."},{"field":"describe-transit-gateway-attachments.TransitGatewayAttachments[?State=='pendingAcceptance'] | [0].TransitGatewayAttachmentId","op":"not_exists","controls":["ca-9"],"description":"No cross-account Transit Gateway attachment is waiting to be accepted. Note the case: the attachment state is camelCase pendingAcceptance, while the VPC endpoint connection state below is PascalCase PendingAcceptance — the two enums are spelled differently and a literal copied from one to the other matches nothing while still returning a green result."},{"field":"describe-vpc-endpoint-connections.VpcEndpointConnections[?VpcEndpointState=='PendingAcceptance'] | [0].VpcEndpointId","op":"not_exists","controls":["ca-9"],"description":"No consumer account is waiting on a decision to connect INTO an endpoint service this account publishes. This is the only inbound clause in the recipe, and VpcEndpointOwner on such an entry is the account asking. The literal is PascalCase per the VpcEndpointConnection API type; the lowercase spelling on the CLI page is a --filters argument value, not an output value."},{"field":"describe-security-group-rules.SecurityGroupRules[?ReferencedGroupInfo && !Description]","op":"count_eq","value":0,"controls":["ca-9"],"description":"No security-group rule that reaches another group by identity rather than by address is left without a description. A documentation-completeness check, not evidence about the connection: Description is free text and AWS validates none of it — and in GovCloud it is a field AWS asks you to keep export-controlled data out of."}],"references":[{"title":"AWS CLI: ec2 describe-vpc-peering-connections (VpcPeeringConnections[] with Tags, ExpirationTime, RequesterVpcInfo/AccepterVpcInfo carrying OwnerId, VpcId, Region; Status.Code including pending-acceptance, active, deleted, rejected, expired)","url":"https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-vpc-peering-connections.html"},{"title":"AWS CLI: ec2 describe-transit-gateway-attachments (ResourceType and State enums, ResourceOwnerId, TransitGatewayOwnerId, Association, CreationTime, Tags)","url":"https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-transit-gateway-attachments.html"},{"title":"AWS CLI: ec2 describe-vpc-endpoints (VpcEndpointType and State enums, ServiceName, PolicyDocument, OwnerId, Groups, SubnetIds, RouteTableIds)","url":"https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-vpc-endpoints.html"},{"title":"AWS CLI: ec2 describe-vpc-endpoint-connections — the service-provider view of consumer connections, with VpcEndpointOwner and VpcEndpointState including pendingAcceptance","url":"https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-vpc-endpoint-connections.html"},{"title":"AWS CLI: ec2 describe-security-group-rules (SecurityGroupRuleId, IsEgress, Description, and the CidrIpv4 / CidrIpv6 / PrefixListId / ReferencedGroupInfo peer fields)","url":"https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-security-group-rules.html"},{"title":"AWS GovCloud (US) User Guide: Amazon VPC — security group rule IDs unavailable in the console, not all VPC endpoints support endpoint policies, and the free-text fields that must not carry export-controlled data","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-vpc.html"},{"title":"Amazon EC2 API: VpcEndpointConnection — vpcEndpointState valid values PendingAcceptance | Pending | Available | Deleting | Deleted | Rejected | Failed | Expired | Partial (PascalCase)","url":"https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_VpcEndpointConnection.html"},{"title":"Amazon EC2 API: TransitGatewayAttachment — state valid values including initiatingRequest, pendingAcceptance, rollingBack, modifying, rejecting and failing (camelCase; initiating deprecated), and resourceType including vpn-concentrator (tgw-peering deprecated)","url":"https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_TransitGatewayAttachment.html"}],"source":"aws","tools":["Amazon VPC","AWS Transit Gateway","AWS PrivateLink"]},{"id":"identifier-assignment-and-reuse-prevention","ksi_ids":["KSI-IAM-ELP","KSI-IAM-JIT"],"control_ids":["ia-4"],"aws_services":["AWS IAM","AWS IAM Identity Center","AWS CloudTrail"],"evidence":"Every identifier the account has issued, with the date it was assigned and the AWS-generated unique id behind it; the workforce identifiers issued through IAM Identity Center and the external issuer each one came from; and the CloudTrail record of identifiers being deleted, which is the only dated evidence of a name becoming free to reuse","collection":{"kind":"cli","commands":[{"name":"get-account-authorization-details","run":"aws iam get-account-authorization-details --filter User Role Group"},{"name":"list-instances","run":"aws sso-admin list-instances"},{"name":"list-users","run":"aws identitystore list-users --identity-store-id <IDENTITY_STORE_ID>"},{"name":"lookup-events-delete-user","run":"aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=DeleteUser --start-time <T0> --end-time <T1>"}]},"expected_output":"From get-account-authorization-details, UserDetailList[], RoleDetailList[] and GroupDetailList[] — a group name is an identifier IA-04 covers and GroupDetail carries GroupId, the stable and unique string identifying the group, though NOT Tags — with UserName/RoleName/GroupName, the AWS-generated UserId/RoleId, Arn, Path, CreateDate as an ISO-8601 timestamp, Tags, and the attached and inline policy lists. This call rather than iam list-users is deliberate: list-users returns UserId, UserName, Arn, Path and CreateDate but AWS documents that it does NOT return Tags or PermissionsBoundary, and the tag is where an authorization reference can live. From sso-admin list-instances, Instances[] — a list whose minimum length is ZERO — each with InstanceArn, IdentityStoreId, Name, OwnerAccountId, CreatedDate, PrimaryRegion, Regions and a Status of CREATE_IN_PROGRESS, CREATE_FAILED, DELETE_IN_PROGRESS or ACTIVE. IdentityStoreId from that response is the argument the third call needs. From identitystore list-users, Users[] with the required IdentityStoreId and UserId and optional UserName, ExternalIds (a list of issuer/id pairs), Name, Emails, UserStatus (ENABLED or DISABLED), CreatedAt/CreatedBy and UpdatedAt/UpdatedBy. From lookup-events, Events[] with EventName DeleteUser, EventTime, and Username — which AWS documents as the user or role name of the REQUESTER that called the API, meaning the administrator who ran the deletion and NOT the identifier that was deleted. The deleted name is inside the CloudTrailEvent JSON, at requestParameters.userName, and that is the field the reuse comparison reads — bounded, as every lookup-events call is, to management events within the last 90 days.","cadence":"continuous","govcloud":"IAM is global and partition-scoped: identifiers created in GovCloud carry arn:aws-us-gov and are entirely separate from the commercial partition's, so an identifier inventory taken in one partition says nothing about the other. IAM Identity Center operates in both AWS GovCloud (US) Regions over FIPS endpoints with NO multi-Region support, and its ARNs take the form arn:aws-us-gov:sso:::instance/<id> — so list-instances must be run in the Region the instance was enabled in, and PrimaryRegion in the response is what says which that is. CloudTrail management events are available in both GovCloud Regions, but note that CloudFront, IAM and STS events are delivered to us-gov-west-1 specifically: IAM is a global service, so the DeleteUser events this recipe reads land in US-West and a lookup-events call issued against US-East returns nothing while succeeding.","automatable":"partial","notes":"IA-04 is four verbs — receive authorization for an identifier, select it, assign it to the intended party, and prevent its reuse for a defined period. AWS proves the third completely, gives real but bounded evidence for the fourth, and returns nothing at all for the first two.\n\nAssignment is the easy half and it is genuinely complete: every identifier in the account is enumerable with the date it was assigned and the AWS-generated unique id behind it. Selection and authorization are records about a decision, not about a resource, and no call returns them.\n\nReuse is where this control is usually mis-evidenced, and AWS's own documentation is unusually direct about why. Within an account a friendly name for a user, group, role or policy must be unique — but only while it exists. AWS documents the exact failure IA-04's reuse clause exists to prevent: an employee named John leaves, the IAM user John is deleted, a new employee named John arrives, a new IAM user John is created, and a policy written against the friendly name grants the new John access to what the old John left behind. Nothing in AWS prevents that. There is no cooling-off period, no reserved-name list, no setting. What AWS guarantees instead is narrower and worth reading precisely: the unique id is never reused, so the old John's AIDA... and the new John's AIDA... differ, and a resource-based policy or an aws:userId condition written against the unique id cannot be inherited by a successor. The identifier that is protected from reuse is the one nobody uses in policy by default.\n\nSo the honest evidence for the reuse limb is a comparison, not a field: DeleteUser events dated against the CreateDate of a live identifier bearing the same name — UserDetailList[].CreateDate is collected here and is the right side of that comparison, which is why no CreateUser lookup is listed. Note what to read on the left side: Username on a lookup-events entry is the REQUESTER, the administrator who ran the deletion, so the deleted identifier has to come out of the CloudTrailEvent JSON at requestParameters.userName. Reading Username as the deleted name is the easy mistake and it produces a comparison against the wrong string entirely. Three limits on the comparison, all hard. lookup-events reads management events for the last 90 DAYS only, so any reuse period your SSP states beyond 90 days cannot be evidenced from this call at all — a CloudTrail Lake event data store or an S3 trail with a longer retention is what closes that, and it is a different collection. The call is also Region-bound in a way that has nothing to do with GovCloud: IAM is a global service, its events are recorded in one Region, and lookup-events shows them in the Region where they occurred — so the same command run anywhere else returns an empty Events[] while succeeding, which is indistinguishable from a window in which nobody was deleted. And the comparison itself is a join between two commands' outputs, which the assertion grammar cannot express: it compares a field to a constant, never one response to another. Both of those are why the reuse limb is written into the notes as a manual reconciliation rather than dressed up as a clause below.\n\nThe IAM users this recipe enumerates are also, on a well-run estate, the smallest part of the answer. Workforce identifiers should be issued in an external identity provider and reach AWS through IAM Identity Center, where the identifier's authority is the ExternalIds pair naming the issuer — which is what the third call reads. An account with an empty UserDetailList and a populated identity store is in better shape than the reverse, and an assertion that ranges over IAM users will say almost nothing about it. Read the two together or read neither.\n\nThe identity-store calls are collected as EVIDENCE and deliberately carry no assertion. An earlier draft asserted that an Identity Center instance exists and that every identity-store user carries ExternalIds; both were withdrawn, because they fail an account that federates through an IAM SAML provider instead — which is a different architecture, not an IA-04 defect — and because the second is vacuous anyway on an instance with zero users, so the first does not guard it. An assertion that encodes an architectural preference as a control failure is worse than a paragraph saying which architecture this evidence reads best.\n\nEmptiness traps to state rather than assert. list-instances returns a list whose documented minimum length is zero and whose maximum is ten, so an account with no instance returns an empty Instances[] rather than an error, and reading only the first entry misses up to nine. A DeleteUser lookup over a window in which nobody was deleted returns an empty Events[], indistinguishable from a window in which the trail was not recording. And GroupDetail carries no Tags, so the authorization-tag clause below cannot be extended to group identifiers at all; device and service identifiers are outside every call here.\n\nRoles and groups are collected alongside users because their names are identifiers the control covers, and because the same reuse behaviour applies: RoleId and GroupId are never reused, while RoleName and GroupName are free the moment the resource is deleted.","assertions":[{"field":"get-account-authorization-details.UserDetailList[?!not_null(Tags[?Key=='IdentifierAuthorization'] | [0])]","op":"count_eq","value":0,"controls":["ia-4"],"description":"No IAM user is missing the tag naming the record that authorized its identifier — the first of IA-04's four verbs, as far as a field can carry it. Written as an offender list because a field projection drops the untagged rows rather than reporting them false, which is exactly the population being asked about. Your own tagging convention, so it is only as complete as the tagging behind it; zero on an account with no IAM users, which is the target state, so read it beside the length of UserDetailList. It cannot extend to groups, which carry no Tags."}],"references":[{"title":"IAM User Guide: IAM identifiers — friendly names must be unique within an account, unique ids are never reused even when a deleted friendly name is re-created, and the worked example of a deleted user's name being re-created and inheriting policy access","url":"https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html"},{"title":"AWS CLI: iam list-users (Users[] always carries UserId, UserName, Arn, Path and CreateDate, and does NOT return Tags or PermissionsBoundary — use GetUser or GetAccountAuthorizationDetails for those)","url":"https://docs.aws.amazon.com/cli/latest/reference/iam/list-users.html"},{"title":"IAM API: RoleDetail — RoleId, RoleName, CreateDate and Tags as returned by GetAccountAuthorizationDetails","url":"https://docs.aws.amazon.com/IAM/latest/APIReference/API_RoleDetail.html"},{"title":"AWS CLI: sso-admin list-instances (Instances[] minimum length 0; InstanceArn, IdentityStoreId, OwnerAccountId, PrimaryRegion, Status ACTIVE | CREATE_IN_PROGRESS | CREATE_FAILED | DELETE_IN_PROGRESS)","url":"https://docs.aws.amazon.com/cli/latest/reference/sso-admin/list-instances.html"},{"title":"AWS CLI: identitystore list-users (requires --identity-store-id; Users[] with required IdentityStoreId and UserId, optional UserName, ExternalIds issuer/id pairs, UserStatus ENABLED | DISABLED, CreatedAt/CreatedBy)","url":"https://docs.aws.amazon.com/cli/latest/reference/identitystore/list-users.html"},{"title":"AWS CLI: iam get-account-authorization-details (--filter possible values User | Role | Group | LocalManagedPolicy | AWSManagedPolicy; the page describes the parameter as a comma-separated list while the CLI synopsis passes them space-separated, which is the form used here)","url":"https://docs.aws.amazon.com/cli/latest/reference/iam/get-account-authorization-details.html"},{"title":"AWS CLI: cloudtrail lookup-events — \"You can look up events that occurred in a Region within the last 90 days\"; Username is \"a user name or role name of the requester that called the API in the event returned\"; management events by default","url":"https://docs.aws.amazon.com/cli/latest/reference/cloudtrail/lookup-events.html"},{"title":"AWS GovCloud (US) User Guide: AWS CloudTrail — CloudFront, IAM and AWS STS events are recorded in us-gov-west-1, and Event history and lookup-events show these events in the Region where they occurred","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-ct.html"},{"title":"AWS GovCloud (US) User Guide: IAM Identity Center — \"Multi-Region support is presently not available\", FIPS endpoints for console/SDK/CLI, and the arn:aws-us-gov:sso:::instance/<SSOInstanceId> pattern","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-sso.html"},{"title":"IAM API: GroupDetail — GroupId is \"the stable and unique string identifying the group\"; the type carries no Tags member","url":"https://docs.aws.amazon.com/IAM/latest/APIReference/API_GroupDetail.html"}],"source":"aws","tools":["AWS IAM","AWS IAM Identity Center","AWS CloudTrail"]},{"id":"personnel-separation-access-revocation","ksi_ids":["KSI-IAM-ELP"],"control_ids":["ps-4"],"aws_services":["AWS IAM","AWS IAM Identity Center","AWS CloudTrail"],"evidence":"The state of every credential in the account as of a stated moment, the enabled-or-disabled status of every workforce identity in the identity store, and the CloudTrail record of the revocations themselves — the five API calls that actually revoke standing access, each with the time it happened and the administrator who did it","collection":{"kind":"cli","commands":[{"name":"generate-credential-report","run":"aws iam generate-credential-report"},{"name":"get-credential-report","run":"aws iam get-credential-report"},{"name":"list-users","run":"aws identitystore list-users --identity-store-id <IDENTITY_STORE_ID>"},{"name":"lookup-events-delete-user","run":"aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=DeleteUser --start-time <T0> --end-time <T1>"},{"name":"lookup-events-delete-login-profile","run":"aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=DeleteLoginProfile --start-time <T0> --end-time <T1>"},{"name":"lookup-events-delete-access-key","run":"aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=DeleteAccessKey --start-time <T0> --end-time <T1>"},{"name":"lookup-events-update-access-key","run":"aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=UpdateAccessKey --start-time <T0> --end-time <T1>"},{"name":"lookup-events-deactivate-mfadevice","run":"aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=DeactivateMFADevice --start-time <T0> --end-time <T1>"}]},"expected_output":"From generate-credential-report, a State of STARTED, INPROGRESS or COMPLETE — AWS stores ONE report per account and regenerates it at most once every four hours, so a request made inside that window silently returns the existing report rather than a fresh one. From get-credential-report, Content (the report, base64-encoded CSV), ReportFormat, and GeneratedTime — the timestamp that says what moment the evidence describes, and the field the freshness assertion reads. The CSV columns are: user, arn, user_creation_time, password_enabled, password_last_used, password_last_changed, password_next_rotation, mfa_active, access_key_1_active, access_key_1_last_rotated, access_key_1_last_used_date, access_key_1_last_used_region, access_key_1_last_used_service, the same five for access_key_2, cert_1_active, cert_1_last_rotated, cert_2_active, cert_2_last_rotated, and additional_credentials_info. Note what the report does NOT cover, because it bounds the whole recipe: AWS documents it as including only passwords, THE FIRST TWO access keys per user, MFA devices and X.509 signing certificates — service-specific credentials such as CodeCommit passwords are absent, as is any access key beyond the second, and additional_credentials_info is the only hint that more exist. From identitystore list-users, Users[] with UserId, UserName, ExternalIds and UserStatus, which is ENABLED or DISABLED. There are five lookup calls rather than one because lookup-events accepts exactly ONE lookup attribute per request, and revocation is five different API calls: deleting the user, deleting the console login profile, deleting an access key, setting an access key Inactive, and deactivating the MFA device. From each lookup-events call, Events[] with EventName, EventTime, Username — the REQUESTER who performed the revocation, not the identity revoked — and the CloudTrailEvent JSON, where requestParameters names the user or device that was acted on.","cadence":"continuous","govcloud":"IAM, the credential report and CloudTrail all operate in both AWS GovCloud (US) Regions and every call here is available; ARNs use partition arn:aws-us-gov. IAM Identity Center is available in both Regions but has NO multi-Region support, so list-users must be run against the Region its instance was enabled in. The CloudTrail calls carry the one real trap: IAM is a global service and its events are recorded in AWS GovCloud (US-West), us-gov-west-1, while Event history and lookup-events show these events in the Region where they occurred — so a DeleteUser lookup issued against us-gov-east-1 returns an empty Events[] and exits zero, which reads exactly like a period in which nobody was separated. The same shape applies in the commercial partition, where those events land in us-east-1.","automatable":"partial","notes":"PS-04 is a list of things that happen when someone leaves: disable system access within a defined period, revoke the authenticators, conduct an exit interview, retrieve organizational property, retain access to the information the person worked on, and notify named personnel within a period. AWS holds evidence for the first two and nothing whatever for the other four.\n\nAnd it holds those two only as state and history, never as subject. The credential report says every credential in the account and what shape it is in; CloudTrail says which revocation happened, when, and by which administrator. Five lookups rather than two, because lookup-events takes one lookup attribute per call and revocation is not one API: an estate that follows this recipe's own advice and DISABLES rather than deletes never emits DeleteUser at all, so a collection that watched only for deletions would return an empty Events[] on a correctly executed separation. UpdateAccessKey is in the list for the same reason — setting a key Inactive is a revocation that deletes nothing. What no call knows is that a PERSON was terminated. There is no roster in AWS, no employment status, no separation date — so the join between 'this human left on the 4th' and 'this identity was revoked on the 6th' is made by a human against an HR record, and the period the control defines is checked on that join and not here. This recipe produces the AWS-side column. It is partial for that reason and no assertion below pretends otherwise.\n\nThat is also why there is exactly one assertion. Any clause of the form 'no user has an active credential' is false on every working account and would be answering about the whole population when the control is about a named few; a clause over 'users your tagging marks as separated' would invent a third tagging convention to answer a question the HR record already answers better. So the single assertion is about the EVIDENCE rather than the estate: that the report describes a recent moment. A credential report is a snapshot with a timestamp, and a stale one is a true statement about a past that has already been superseded.\n\nRead the four-hour rule as a collection constraint, not a detail. AWS stores one credential report per account and regenerates it at most once every four hours; a generate call inside that window returns the report you already had. So generate-then-get does not guarantee freshness, GeneratedTime is what does, and a collector that runs the two calls back to back and assumes the second reflects the first is reporting on a state up to four hours old.\n\nThe report's coverage is narrower than its name suggests, and the gap is exactly where a separation goes wrong. AWS documents it as covering passwords, the first two access keys per user, MFA devices and X.509 signing certificates. A third access key is not in it. Service-specific credentials are not in it. A separated user whose remaining access is a CodeCommit password or a long-term service credential appears in this report as fully revoked. additional_credentials_info is the only signal that anything else exists, and closing that gap needs ListAccessKeys and ListServiceSpecificCredentials per user, which this recipe does not collect and an assessment should.\n\nOn a well-run estate the IAM half is the small half. Workforce identities live in the identity store and a separation there is UserStatus DISABLED rather than a deletion, which is why the third call is collected: an account whose credential report lists no IAM user for a departed employee, while the identity store still lists them as ENABLED, has not revoked anything — and the credential report, which sees only IAM, cannot show it. Disabling rather than deleting is also the better practice for this control, because it preserves the identifier — see the IA-04 recipe on why a freed name is a hazard.\n\nOne KSI was deliberately not claimed. KSI-IAM-SUS asks that privileged accounts be disabled or secured IN RESPONSE TO SUSPICIOUS ACTIVITY, and a routine separation is not suspicious activity; the output is identical in both cases and cannot tell an assessor which one it is looking at. KSI-IAM-JIT was dropped for a quieter reason. Its statement asks for a least-privileged, role and attribute-based, and JUST-IN-TIME authorization model, persistently reviewed. Nothing in five commands over STANDING credentials evaluates a just-in-time or attribute-based model; only the 'persistently reviewed' tail is touched, and that tail is already what KSI-IAM-ELP carries here. KSI-IAM-ELP is earned outright: an identity that outlives its holder is precisely a failure of the rule that each user can access only what they need.","assertions":[{"field":"get-credential-report.GeneratedTime","op":"max_age_days","value":1,"controls":["ps-4"],"description":"The credential report being read describes a moment within the last day. AWS regenerates at most once every four hours and returns the stored report inside that window, so calling generate before get does NOT make the result fresh — GeneratedTime is the only field that says how old the evidence is. Substitute the interval your SSP commits to for revocation review."}],"references":[{"title":"IAM User Guide: generate credential reports — the CSV columns, the four-hour regeneration rule, and the documented exclusion of service-specific credentials and any access key beyond the first two","url":"https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_getting-report.html"},{"title":"IAM API: GetCredentialReport — returns Content (the base64-encoded CSV), ReportFormat and GeneratedTime, the timestamp the freshness assertion reads","url":"https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetCredentialReport.html"},{"title":"AWS CLI: identitystore list-users (Users[] with UserId, UserName, ExternalIds and UserStatus ENABLED | DISABLED; requires --identity-store-id)","url":"https://docs.aws.amazon.com/cli/latest/reference/identitystore/list-users.html"},{"title":"AWS CLI: cloudtrail lookup-events — events within the last 90 days; Username is the user or role name of the REQUESTER that called the API","url":"https://docs.aws.amazon.com/cli/latest/reference/cloudtrail/lookup-events.html"},{"title":"AWS GovCloud (US) User Guide: AWS CloudTrail — CloudFront, IAM and AWS STS events are recorded in us-gov-west-1, and lookup-events shows them in the Region where they occurred","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-ct.html"},{"title":"AWS GovCloud (US) User Guide: IAM Identity Center — \"Multi-Region support is presently not available\"","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-sso.html"}],"source":"aws","tools":["AWS IAM","AWS IAM Identity Center","AWS CloudTrail"]},{"id":"name-resolution-role-separation","ksi_ids":["KSI-SVC-SIN"],"control_ids":["sc-22"],"aws_services":["Amazon Route 53","Amazon Route 53 Resolver"],"evidence":"The hosted zones that serve name resolution, each marked private or public so internal and external resolution can be shown to be served by separate zones, and the Resolver endpoints that carry queries across the VPC boundary with their direction and operational status","collection":{"kind":"cli","commands":[{"name":"list-hosted-zones","run":"aws route53 list-hosted-zones"},{"name":"list-resolver-endpoints","run":"aws route53resolver list-resolver-endpoints"}]},"expected_output":"From list-hosted-zones, HostedZones[] with Id, Name, CallerReference, ResourceRecordSetCount, an optional LinkedService naming the AWS service that created the zone, and Config carrying Comment and PrivateZone — a boolean, and the ONLY field that separates an internal zone from an external one. Read its absence as public: AWS documents PrivateZone true as private and false-or-absent as public, so a clause testing for equality with false misses the zones where Config never appears. From list-resolver-endpoints, ResolverEndpoints[] with Id, Arn, Name, CreatorRequestId, HostVPCId, SecurityGroupIds, IpAddressCount, a ResolverEndpointType of IPV4, IPV6 or DUALSTACK, a Direction of INBOUND, OUTBOUND or INBOUND_DELEGATION, and a Status of CREATING, OPERATIONAL, UPDATING, AUTO_RECOVERING, ACTION_NEEDED or DELETING.","cadence":"continuous","govcloud":"Route 53 is available in both AWS GovCloud (US) Regions with public and private DNS and health checking, and AWS states that public-zone DNS queries are answered from within the FedRAMP boundary — which is itself worth quoting in an assessment. The control plane for Route 53 in GovCloud is in AWS GovCloud (US-West), so these calls are issued there. Four documented differences bear on this recipe. Route 53 RESOLVER DELEGATION is not available for private hosted zones, so an architecture that separates roles by delegating a private subtree cannot be built there and the separation has to be zone-level. Alias targets may point at GovCloud Regions only, never at global AWS Regions. IP-based routing, the console DNS query checking tool and the TestDNSAnswer API are all unavailable, so there is no in-account way to verify what an external resolver actually receives. And the customer managed key for DNSSEC signing and the CloudWatch Logs group for query logging must both be in AWS GovCloud (US-West), where CloudWatch metrics such as DNSQueries can also be found. All Route 53 API actions there share a token bucket of capacity 40 refilling at 5 per second, which matters for a collector that walks many zones.","automatable":"partial","notes":"SC-22 asks two things of the systems that collectively provide name and address resolution: that they be FAULT-TOLERANT, and that they implement INTERNAL AND EXTERNAL ROLE SEPARATION. This output answers the second and cannot answer the first.\n\nRole separation is a field. Config.PrivateZone marks each hosted zone as serving internal or external resolution, and an architecture where internal names are served from private zones associated with VPCs while public names are served from public zones is exactly the separation the control describes — visible, enumerable, and reviewable on the cadence the indicator asks for. Resolver endpoints are collected beside the zones because they are where the two roles actually meet: an INBOUND endpoint lets on-premises resolvers query into the VPC, an OUTBOUND endpoint forwards VPC queries out, and each one is a crossing point that the zone list alone does not show.\n\nFault tolerance is not a field and is not measurable from your account. Route 53's resilience is a property of AWS's anycast name server fleet, not of your configuration, and no call against your account returns it. It is inherited under the shared responsibility model and evidenced from AWS's own authorization package — say so in the assessment rather than presenting a zone list as though it spoke to availability. The one adjacent thing this output does show is whether YOUR crossing points are healthy, which is what the Status clause below asserts, and that is endpoint health rather than service fault tolerance. Conflating them would be the whole control's failure in one line.\n\nA scope edge that decides whether this evidence means anything: name resolution is only separated if the two zone sets actually differ. Two hosted zones for the same domain, one private and one public, is split-horizon DNS and is a legitimate and common pattern — but so is a single public zone answering internal names, which satisfies neither role separation nor the control, and the assertions below cannot tell those apart because the grammar compares a field to a constant rather than one zone's Name to another's. Reconcile the two zone sets by name as part of the review.\n\nA private hosted zone also cannot be DNSSEC-signed, which is why signing is a separate recipe and not folded in here. And LinkedService is worth reading before anything is concluded from a zone's presence: a zone created by another AWS service on your behalf is inventory rather than architecture.\n\nOne KSI only. KSI-SVC-SIN — information secured from unwanted access — is what a private zone does, by keeping internal names off the public resolvers. The recipe does not claim the identity indicator that also reaches this control in the dataset: nothing in these two responses evaluates a permission.","assertions":[{"field":"list-hosted-zones.HostedZones[?Config.PrivateZone==`true`] | [0].Id","op":"exists","controls":["sc-22"],"description":"At least one private hosted zone exists — internal name resolution is served by a zone of its own rather than by the public one. The pipe is load-bearing: without it the [0] indexes each matched zone rather than the filtered list."},{"field":"list-hosted-zones.HostedZones[?Config.PrivateZone!=`true`] | [0].Id","op":"exists","controls":["sc-22"],"description":"At least one public hosted zone exists, so the separation asserted above is a separation of two live roles rather than an account that simply has no external presence. Written as != true rather than == false deliberately: AWS documents a public zone as PrivateZone false OR ABSENT, and an equality test against false misses every zone whose Config omits the field."},{"field":"list-resolver-endpoints.ResolverEndpoints[?Status!='OPERATIONAL']","op":"count_eq","value":0,"controls":["sc-22"],"description":"No Resolver endpoint is stuck in CREATING, UPDATING, AUTO_RECOVERING, ACTION_NEEDED or DELETING. This is the health of YOUR crossing points and is not evidence of fault tolerance in the resolution service itself, which is AWS's to demonstrate. Zero on an account with no Resolver endpoints, which is a normal architecture rather than a pass."}],"references":[{"title":"AWS CLI: route53 list-hosted-zones (HostedZones[] with Config.PrivateZone as the private/public discriminator, plus Id, Name, CallerReference, ResourceRecordSetCount and LinkedService)","url":"https://docs.aws.amazon.com/cli/latest/reference/route53/list-hosted-zones.html"},{"title":"AWS CLI: route53resolver list-resolver-endpoints (Direction INBOUND | OUTBOUND | INBOUND_DELEGATION; Status CREATING | OPERATIONAL | UPDATING | AUTO_RECOVERING | ACTION_NEEDED | DELETING; ResolverEndpointType IPV4 | IPV6 | DUALSTACK)","url":"https://docs.aws.amazon.com/cli/latest/reference/route53resolver/list-resolver-endpoints.html"},{"title":"AWS GovCloud (US) User Guide: Amazon Route 53 — public-zone queries answered from within the FedRAMP boundary, control plane in GovCloud (US-West), Resolver delegation unavailable for private hosted zones, TestDNSAnswer and IP-based routing unavailable, DNSSEC signing key must be in US-West","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-r53.html"}],"source":"aws","tools":["Amazon Route 53","Amazon Route 53 Resolver"]},{"id":"security-advisories-receipt-and-dissemination","ksi_ids":["KSI-SCR-MON"],"control_ids":["si-5"],"aws_services":["AWS Health","Amazon SNS"],"evidence":"The advisories AWS itself has issued against this account — operational issues, scheduled changes and account notifications, each dated and scoped — together with the subscribers on the topic those alerts are published to and the confirmation state of each subscription, which is the difference between an address that was entered and an address that receives","collection":{"kind":"cli","commands":[{"name":"describe-events","run":"aws health describe-events --filter eventTypeCategories=issue,accountNotification,scheduledChange"},{"name":"list-subscriptions-by-topic","run":"aws sns list-subscriptions-by-topic --topic-arn <SECURITY_ALERT_TOPIC_ARN>"},{"name":"get-subscription-attributes","run":"aws sns get-subscription-attributes --subscription-arn <SUBSCRIPTION_ARN>"}]},"expected_output":"From describe-events, events[] with arn, service, eventTypeCode, an eventTypeCategory of issue, accountNotification, scheduledChange or investigation, region, availabilityZone, startTime, endTime, lastUpdatedTime, a statusCode of open, closed or upcoming, and an eventScopeCode of PUBLIC, ACCOUNT_SPECIFIC or NONE — the field that says whether an advisory is a general service event or one raised against this account in particular. The API is served from a global endpoint, global.health.amazonaws.com, which resolves by CNAME to whichever of the us-east-1 (active) and us-east-2 (passive) endpoints is current, and AWS notes that only the active endpoint carries the latest data. From list-subscriptions-by-topic, Subscriptions[] with SubscriptionArn, Owner, Protocol, Endpoint and TopicArn, up to 100 per call with a NextToken beyond that. Read SubscriptionArn carefully: for a subscription that has not been confirmed it is not an ARN at all but the LITERAL STRING PendingConfirmation, which AWS's own published response for an unconfirmed email subscriber shows. That is the discriminator, and it is on this list. From get-subscription-attributes, an Attributes MAP whose values are all strings — including PendingConfirmation, documented as \"true if the subscription hasn't been confirmed\", and ConfirmationWasAuthenticated, \"true if the subscription confirmation request was authenticated\" — which is why any clause on them would compare against the string \"false\" and not a boolean. This call is collected as supplementary evidence — ConfirmationWasAuthenticated is not derivable from the list — and carries no assertion, because it takes --subscription-arn and the unconfirmed case has no ARN to pass it.","cadence":"continuous","govcloud":"Amazon SNS operates in both AWS GovCloud (US) Regions and topic and subscription ARNs use partition arn:aws-us-gov. The AWS Health call is the one to check before this recipe is planned rather than after, and its constraint is commercial as much as GovCloud: AWS states that a Business, Business+, Enterprise On-Ramp, Enterprise or Unified Operations plan is REQUIRED to use the AWS Health API, and that an account not enrolled in one receives a SubscriptionRequiredException. A provider on Basic or Developer support cannot run the first command at all, and for them the advisory-receipt half of this recipe is a Health Dashboard record — which every customer can see — rather than an API result. In GovCloud the endpoint is different in kind, not just in name: AWS documents the Health API there as a single regional endpoint in us-gov-west-1, as opposed to the commercial global endpoint with failover-capable Regions behind it, so the global.health.amazonaws.com DNS-lookup pattern does not apply and us-gov-west-1 is the target. Two further GovCloud behaviours worth writing into the assessment: some Health events are global rather than Regional — IAM's among them — and receiving those requires a rule in AWS GovCloud (US-West), the twin of the CloudTrail Region rule the PS-04 recipe names; and the EventBridge channel there does not deliver public Service Health View events at all, so an alerting pipeline built on EventBridge sees account-specific events only and the API or the RSS feed is what carries the rest.","automatable":"partial","notes":"SI-05 has four movements: receive security alerts, advisories and directives from external organizations on an ongoing basis; generate internal security alerts; disseminate them to the personnel your policy names; and implement directives or notify the issuing organization of the degree of nonconformance. This output touches the first and the third, and only for one external organization.\n\nThat organization is AWS. Health events are the vendor's own notifications to this account — operational issues, scheduled changes, account notifications — dated, scoped by eventScopeCode to this account or to the service generally, and enumerable. That is a real answer to 'are you receiving advisories from your suppliers, on an ongoing basis, and can you show it'. The supply-chain indicator is claimed on one limb of its statement and not the whole: KSI-SCR-MON allows for mechanisms that 'may include contractual notification requirements', and a vendor health feed is exactly such a mechanism. It is NOT the other limb — none of Health's four event categories is a vulnerability category, and this evidence monitors AWS's service health rather than the provider's third-party software inventory. Read the claim as the notification limb only.\n\nWhat it is not is the control. CISA emergency directives, US-CERT advisories, vendor bulletins for every other component in the stack, and the entire question of implementing a directive or reporting nonconformance to the issuer are correspondence and decisions. No API returns them, and a collection that presents Health events as SI-05 evidence without saying which limb they serve has answered a quarter of a control.\n\nThe dissemination half is where the honest telemetry is, and it is smaller than it looks. A topic with subscribers proves an alert has somewhere to go. It does not prove the subscribers are the personnel the policy names — that is a roster comparison — and it does not prove they receive anything unless the subscription is CONFIRMED. AWS requires confirmation for HTTP(S) endpoints, email addresses and cross-account resources.\n\nThe unconfirmed case is visible, and it is visible in the place you would not look for it. An unconfirmed subscription appears in the topic's subscription list with SubscriptionArn set to the literal string PendingConfirmation instead of an ARN — not a missing field, not a flag, a sentinel value in the identifier column. So the completeness clause below is written against the LIST. An earlier draft asserted the PendingConfirmation attribute from get-subscription-attributes instead, which cannot work: that call takes --subscription-arn, the unconfirmed subscription has no ARN to pass, and the failing case therefore never reaches the assertion at all. The third call stays as supplementary evidence for ConfirmationWasAuthenticated, which the list genuinely does not carry, and it carries no clause.\n\nIf a clause is ever written on those attributes, note that they come back as a map of STRINGS — PendingConfirmation is \"true\" or \"false\", not a boolean, and a comparison against a boolean matches nothing while returning green.\n\nOne gap the assertions cannot see, named here because a partial rating owes it: nothing collected joins the Health feed to the topic. SECURITY_ALERT_TOPIC_ARN is supplied by the operator, and no output here shows an EventBridge rule on the aws.health source targeting it. The two halves of this recipe are adjacent in the assessment's head and unjoined in the data; collect the EventBridge rule and its targets if that join needs to be evidence rather than assertion.\n\nTwo emptiness traps. describe-events as written sets no time bound — the filter selects categories, not a window — so what comes back is what AWS Health currently holds, and an empty events[] means the same thing on an account receiving advisories perfectly as on one whose Health access was never wired up. Add startTimes to the filter if the assessment needs the evidence pinned to a period. And the third call must be repeated per subscription: one confirmed subscription proves nothing about the other nine, and the assertion below is written against a single subscription ARN because that is what the API takes — walk the list from the second call and collect one response each, or the evidence covers one address.\n\nKSI-SVC-ACM was deliberately not claimed. It asks that configuration be managed by automation and reviewed for drift; an advisory feed and a subscriber list say nothing about configuration, and claiming it would put this recipe on an indicator page as evidence for a question it never asks.","assertions":[{"field":"list-subscriptions-by-topic.Subscriptions | [0].SubscriptionArn","op":"exists","controls":["si-5"],"description":"The security alert topic has at least one subscriber. A topic with none is an alerting path that terminates in nothing. Says nothing about WHO the subscribers are — that is a comparison against the personnel your policy names, and no call returns that roster — and on its own it does not even prove deliverability, since an unconfirmed subscription satisfies it too. Read it with the clause below, never alone."},{"field":"list-subscriptions-by-topic.Subscriptions[?SubscriptionArn=='PendingConfirmation']","op":"count_eq","value":0,"controls":["si-5"],"description":"No subscriber on the alert topic is still unconfirmed. AWS returns the literal string PendingConfirmation in the SubscriptionArn column for a subscription nobody has confirmed, so the failing case is a value in the list rather than an absence — which is why this clause reads the list and not the per-subscription attributes, whose call cannot even be made for an unconfirmed subscriber."}],"references":[{"title":"AWS CLI: health describe-events (events[] with eventTypeCategory issue | accountNotification | scheduledChange | investigation, statusCode open | closed | upcoming, eventScopeCode PUBLIC | ACCOUNT_SPECIFIC | NONE)","url":"https://docs.aws.amazon.com/cli/latest/reference/health/describe-events.html"},{"title":"AWS Health User Guide: using the AWS Health API — a Business+, Enterprise, Unified Operations, Business, Enterprise On-Ramp or Enterprise Support plan is required or the call returns SubscriptionRequiredException; global endpoint global.health.amazonaws.com with active us-east-1 and passive us-east-2","url":"https://docs.aws.amazon.com/health/latest/ug/health-api.html"},{"title":"Amazon SNS API: ListSubscriptionsByTopic (up to 100 subscriptions per call with NextToken; throttled at 30 TPS)","url":"https://docs.aws.amazon.com/sns/latest/api/API_ListSubscriptionsByTopic.html"},{"title":"AWS CLI: sns get-subscription-attributes — PendingConfirmation \"true if the subscription hasn't been confirmed\" and ConfirmationWasAuthenticated, returned in an Attributes map of strings","url":"https://docs.aws.amazon.com/cli/latest/reference/sns/get-subscription-attributes.html"},{"title":"Amazon SNS Developer Guide: creating a subscription — \"HTTP(S) endpoints, email addresses, and AWS resources in other AWS accounts require confirmation of the subscription before they can receive messages\"","url":"https://docs.aws.amazon.com/sns/latest/dg/sns-create-subscribe-endpoint-to-topic.html"},{"title":"AWS SDK code library: ListSubscriptions — the published response for an unconfirmed email subscriber carries SubscriptionArn: 'PendingConfirmation' rather than an ARN","url":"https://docs.aws.amazon.com/code-library/latest/ug/sns_example_sns_ListSubscriptions_section.html"},{"title":"AWS GovCloud (US) User Guide: AWS Health — the API is a single regional endpoint in us-gov-west-1 rather than a global endpoint; Business or Enterprise support required; global events including IAM need a rule in GovCloud (US-West); the EventBridge channel does not send public Service Health View events","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-health.html"}],"source":"aws","tools":["AWS Health","Amazon SNS"]},{"id":"personnel-transfer-access-reassignment","ksi_ids":["KSI-IAM-ELP"],"control_ids":["ps-5"],"aws_services":["AWS IAM","AWS IAM Identity Center","AWS CloudTrail"],"evidence":"What a transferred individual can still reach, and what they have actually used: the reassignment events themselves from CloudTrail (group membership, attached policy, and Identity Center account-assignment changes, each with its time and the administrator who made it), the current Identity Center assignments per permission set, and IAM's service-last-accessed report for the identities involved — a per-principal view of which services the identity is permitted to reach and which of those it has never authenticated to. `iam-access-analyzer-unused-access` answers the same question estate-wide from findings; this answers it for the named principal a transfer is about.","collection":{"kind":"cli","commands":[{"name":"lookup-events-add-user-to-group","run":"aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=AddUserToGroup --start-time <T0> --end-time <T1>"},{"name":"lookup-events-remove-user-from-group","run":"aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=RemoveUserFromGroup --start-time <T0> --end-time <T1>"},{"name":"lookup-events-attach-user-policy","run":"aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=AttachUserPolicy --start-time <T0> --end-time <T1>"},{"name":"lookup-events-detach-user-policy","run":"aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=DetachUserPolicy --start-time <T0> --end-time <T1>"},{"name":"lookup-events-create-account-assignment","run":"aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=CreateAccountAssignment --start-time <T0> --end-time <T1>"},{"name":"lookup-events-delete-account-assignment","run":"aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=DeleteAccountAssignment --start-time <T0> --end-time <T1>"},{"name":"list-permission-sets","run":"aws sso-admin list-permission-sets --instance-arn <INSTANCE_ARN>"},{"name":"list-account-assignments","run":"aws sso-admin list-account-assignments --instance-arn <INSTANCE_ARN> --account-id <ACCOUNT_ID> --permission-set-arn <PERMISSION_SET_ARN>"},{"name":"generate-service-last-accessed-details","run":"aws iam generate-service-last-accessed-details --arn <PRINCIPAL_ARN> --granularity ACTION_LEVEL"},{"name":"get-service-last-accessed-details","run":"aws iam get-service-last-accessed-details --job-id <JOB_ID>"}]},"expected_output":"From each lookup-events call, Events[] with EventName, EventTime, Username — the REQUESTER who made the change, never the identity changed — and CloudTrailEvent, whose requestParameters names the user, group, policy or account assignment acted on. Six calls rather than one because lookup-events accepts exactly ONE lookup attribute per request and a reassignment is six different APIs across two services; the window is bounded at 90 days by the API. From list-permission-sets, PermissionSets[] of ARNs. From list-account-assignments, AccountAssignments[] with AccountId, PermissionSetArn, PrincipalId and PrincipalType (USER or GROUP) — for ONE permission set in ONE account, which is why list-permission-sets precedes it: the assignment set of an account is the union over its permission sets and no single call returns it. From generate-service-last-accessed-details, a JobId (36 characters); from get-service-last-accessed-details, JobStatus (IN_PROGRESS | COMPLETED | FAILED) and ServicesLastAccessed[] with ServiceName, ServiceNamespace, LastAuthenticated, LastAuthenticatedEntity and TotalAuthenticatedEntities, plus TrackedActionsLastAccessed[] when the request asked for ACTION_LEVEL. A service the principal is permitted to call and has never called comes back with the entry PRESENT and LastAuthenticated ABSENT, which is the shape that makes unused-but-granted access visible at all.","cadence":"continuous","govcloud":"IAM, CloudTrail and IAM Identity Center all operate in both AWS GovCloud (US) Regions; ARNs use partition arn:aws-us-gov, and the sso-admin ARN patterns accept it. Two partition traps, both real here. IAM is global and its events are recorded in AWS GovCloud (US-West), us-gov-west-1, while lookup-events shows events in the Region where they occurred — so the four IAM lookups issued against us-gov-east-1 return an empty Events[] and exit zero, which reads exactly like a period in which nobody transferred. The equivalent commercial Region is us-east-1. Second, IAM Identity Center has no multi-Region support in GovCloud, so the sso-admin calls must be issued against the Region its instance was enabled in. What AWS documents is the absence of multi-Region support; what a call issued against the other Region actually does is not documented on that page and this recipe does not guess. Establish which Region holds the instance before collecting, rather than inferring it from a response.","automatable":"partial","notes":"PS-05 is about a person moving inside the organization: the access that fitted the old role is reviewed, what is no longer needed is removed, and the transfer is completed within a period the provider defines. AWS holds two-thirds of that and cannot hold the rest.\n\nWhat it holds is the CHANGE and the STATE. CloudTrail records the six APIs a reassignment actually travels through — two for group membership, two for attached user policies, two for Identity Center account assignments — each with a timestamp and the administrator who made it. Identity Center holds the assignments as they stand today. What no call knows is that a person MOVED. There is no roster in AWS, no job title, no transfer date, so the join between 'this human changed teams on the 4th' and 'this principal lost the finance permission set on the 9th' is made by a human against an HR record, and the period the control defines is checked on that join. This recipe produces the AWS-side column and the rating says so.\n\nThe third call set is the one that earns its place. A separation is visible as an absence; a transfer is visible as a RESIDUE — the old permission that nobody removed because nothing failed when it stayed. Service-last-accessed is the signal that names it per principal: for a principal it lists every service the identity could reach under its permissions policies, with the last time it authenticated, and an entry with no LastAuthenticated is a permission granted and never exercised. Run against the transferred principal it turns 'review the access that no longer fits' from an interview into a list. It is not the only route to that question — `iam-access-analyzer-unused-access` answers it estate-wide from findings — but it is the one that answers it about a NAMED identity, which is the unit a transfer is measured in.\n\nRead its four documented limits before quoting it. It reports for at least the last 400 days, and less in a Region that began supporting the feature within the last year — so a young Region reports a shorter history than the reviewer assumes. Recent activity usually appears within four hours, so a report pulled immediately after a transfer describes the state before it. It records ATTEMPTS and not successes: a denied call still marks a service as accessed, and AWS names CloudTrail as the authoritative source for whether a call succeeded. And it applies permissions-policy logic ONLY — resource-based policies, ACLs, Organizations policies, permissions boundaries and STS assume-role trust are all excluded, so an identity that reaches a bucket purely through a bucket policy is invisible to it. That last one bounds the whole claim: 'no unused permissions' from this report is a statement about identity policies, not about reach.\n\nlist-account-assignments takes an account id AND a permission set ARN, and returns the assignees of that one pair. There is no call that returns an account's assignments whole, which is why list-permission-sets runs first and the collection is a loop. A single unlooped call would be a slice of one permission set presented as an account's access, and an assertion over it would be green on an estate where every other permission set is wrong.\n\nThe authentication indicators are untouched here — nothing collected reads a factor. KSI-IAM-ELP is earned outright: a permission that fits a role the holder has left is exactly the failure of 'each user can access only what they need'. KSI-IAM-JIT is deliberately NOT claimed, on this overlay's own precedent: the PS-04 recipe dropped it because nothing evaluated over STANDING credentials examines a just-in-time or attribute-based model, and only the 'persistently reviewed' tail is touched — a tail KSI-IAM-ELP already carries. Everything collected here is standing too: standing credentials, standing group membership, standing account assignments. The just-in-time half is the Identity Center elevation recipe's to answer, and claiming it here would have had two sibling recipes taking opposite positions on one indicator over one class of telemetry.","assertions":[{"field":"get-service-last-accessed-details.ServicesLastAccessed[?!not_null(LastAuthenticated)]","op":"count_eq","value":0,"controls":["ps-5"],"description":"After the reassignment review, the transferred principal holds no permission it has never used. The offender form — the services with no LastAuthenticated at all — because a service the principal has never called is listed WITHOUT the most-recent-attempt details rather than with a null timestamp, so a projection over LastAuthenticated drops exactly the rows this clause exists to find. Read a non-zero count as the review's work list, not as a failure: the control is satisfied by acting on it within the period the SSP defines, and whether the individual still needs each one is the HR judgement no command makes."},{"field":"get-service-last-accessed-details.JobStatus","op":"eq","value":"COMPLETED","controls":["ps-5"],"description":"The last-accessed report actually finished. The generate call returns a JobId immediately and the get call answers IN_PROGRESS until the report exists — a collector that reads ServicesLastAccessed[] from an unfinished job reads an empty array, which would make the clause above vacuously true at the exact moment it has learned nothing."}],"references":[{"title":"AWS CLI: cloudtrail lookup-events — one lookup attribute per request, events within the last 90 days, Username is the REQUESTER that called the API","url":"https://docs.aws.amazon.com/cli/latest/reference/cloudtrail/lookup-events.html"},{"title":"IAM API: GenerateServiceLastAccessedDetails — Granularity SERVICE_LEVEL | ACTION_LEVEL, at least 400 days of history, activity appears within about four hours, records ATTEMPTS including denied ones, and applies permissions-policy logic only (resource-based policies, ACLs, Organizations policies, permissions boundaries and assume-role trust excluded)","url":"https://docs.aws.amazon.com/IAM/latest/APIReference/API_GenerateServiceLastAccessedDetails.html"},{"title":"IAM API: GetServiceLastAccessedDetails — JobStatus is IN_PROGRESS | COMPLETED | FAILED, and a service with no access attempt is listed WITHOUT details about the most recent attempt (the shape both assertions read)","url":"https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetServiceLastAccessedDetails.html"},{"title":"IAM Identity Center API: ListAccountAssignments — requires both AccountId and PermissionSetArn, returns AccountAssignments[] with PrincipalType and PrincipalId for that one pair","url":"https://docs.aws.amazon.com/singlesignon/latest/APIReference/API_ListAccountAssignments.html"},{"title":"AWS GovCloud (US) User Guide: AWS CloudTrail — IAM and AWS STS events are recorded in us-gov-west-1 and lookup-events shows them in the Region where they occurred","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-ct.html"},{"title":"AWS GovCloud (US) User Guide: IAM Identity Center — multi-Region support is not available, so the instance answers only in the Region it was enabled in","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-sso.html"}],"source":"aws","tools":["AWS IAM","AWS IAM Identity Center","AWS CloudTrail"]},{"id":"acquisition-scanning-and-sbom-inventory","ksi_ids":["KSI-SCR-MIT","KSI-SCR-MON"],"control_ids":["sr-5"],"aws_services":["Amazon Inspector","Amazon ECR"],"evidence":"Whether the tooling that examines acquired software is switched on and covering the estate, and what it found: Inspector's per-account enablement state for each scanned resource type, the registry-wide ECR scanning configuration (scan type and frequency, and the repository filters that decide which repositories it applies to), Inspector's own coverage statistics, and a CycloneDX 1.4 or SPDX 2.3 SBOM exported per monitored resource — the component-level inventory of what was actually acquired.","collection":{"kind":"cli","commands":[{"name":"batch-get-account-status","run":"aws inspector2 batch-get-account-status"},{"name":"get-registry-scanning-configuration","run":"aws ecr get-registry-scanning-configuration"},{"name":"list-coverage-statistics","run":"aws inspector2 list-coverage-statistics --group-by SCAN_STATUS_REASON"},{"name":"create-sbom-export","run":"aws inspector2 create-sbom-export --report-format CYCLONEDX_1_4 --s3-destination bucketName=<SBOM_BUCKET>,keyPrefix=<SBOM_PREFIX>,kmsKeyArn=<KMS_KEY_ARN>"},{"name":"get-sbom-export","run":"aws inspector2 get-sbom-export --report-id <REPORT_ID>"}]},"expected_output":"From batch-get-account-status, accounts[] with accountId, state.status, and resourceState broken out per scanned resource type — ec2, ecr, lambda, lambdaCode and codeRepository — each with its own status and an errorCode when the service could not enable it. From get-registry-scanning-configuration, registryId and scanningConfiguration with scanType (BASIC or ENHANCED) and rules[], each rule carrying scanFrequency (SCAN_ON_PUSH, CONTINUOUS_SCAN or MANUAL) and repositoryFilters[] of {filter, filterType}; ENHANCED supports CONTINUOUS_SCAN and SCAN_ON_PUSH, BASIC supports SCAN_ON_PUSH only, and where scan-on-push is not specified the frequency defaults to MANUAL. From list-coverage-statistics, countsByGroup[] and totalCounts — the number of resources Inspector is actually covering, which is the number that decides whether an empty findings list means clean or means unscanned. From create-sbom-export, a reportId; from get-sbom-export, status (IN_PROGRESS | SUCCEEDED | FAILED | CANCELLED) with the s3Destination the JSON documents were written to. Each exported document is one resource's component inventory: CycloneDX 1.4 with components[] carrying purl and bom-ref, or SPDX 2.3 with packages[] carrying versionInfo and externalRefs. Unresolved hashes — components whose package manager used a version range or dynamic reference and which therefore cannot be scanned for vulnerabilities — are INCLUDED in the export as hashes, and are the part of the inventory a vulnerability count silently omits.","cadence":"continuous","govcloud":"Both services are available in AWS GovCloud (US-West) and AWS GovCloud (US-East). Two documented differences bear on this recipe and one bears on the control it proves. Inspector: Lambda CODE scanning is not available in the partition, so resourceState.lambdaCode reports its absence rather than a misconfiguration, and the Linux deep-inspection plugin is documented as not FIPS compliant — which matters to a provider whose SSP claims FIPS-validated modules end to end. ECR: pull-through cache rules work only WITHIN the same partition, ECR public registries are not available, and the ECR Public Gallery is not hosted in GovCloud though it may be reachable if the estate has external internet access. Those three are acquisition facts, not availability trivia: they change where a GovCloud estate's images can legitimately come from, which is the strategy half of SR-05. S3 and KMS ARNs in the export destination use partition arn:aws-us-gov.","automatable":"partial","notes":"SR-05 asks for acquisition strategies, contract tools and procurement methods that reduce supply-chain risk. Two halves, and AWS holds one of them completely and the other not at all.\n\nThe half it holds is the TOOLING at the moment of acquisition. ECR's registry scanning configuration is the acquisition gate: scan-on-push examines an image as it enters the registry, and the rules[] with their repository filters say which repositories that applies to. Inspector monitors what has already landed and exports an SBOM per resource, which is the component-level inventory of what was actually acquired rather than what a supplier said they shipped. Both are continuous, both are machine-readable, and together they answer 'what did we take in, and what was wrong with it'.\n\nThe half it does not hold is the STRATEGY the control names — the contract clauses, the approved-supplier list, the delivery method, the decision to buy this component rather than that one. No scanner stands in for procurement, and none of these five commands reads a contract. That is the whole reason for the partial rating, and the assessment reads it out of the acquisition documentation the control asks for.\n\nThe enablement and coverage calls exist because of the empty-list trap, and here it is unusually sharp. A findings query against an estate with Inspector switched off returns an empty array and exits zero, and so does a clean estate. batch-get-account-status is what separates them: it reports status per resource type, so ECR ENABLED with EC2 DISABLED is visible as the partial coverage it is rather than as silence. Read BOTH of its arrays. An account that could not be enabled — access denied, or blocked by an Organizations policy — is reported in failedAccounts[] and does not appear in accounts[] at all, so a clause over accounts[] alone is green on the one account whose scanning never began. The enumeration has an edge worth stating too: accounts[] is capped at one hundred entries with no continuation token, and what the call returns when no account ids are passed is undocumented — so run it as the delegated administrator with the ids you mean, and treat the response as evidence about the accounts it names rather than about the organization. list-coverage-statistics closes the second half of the same gap — Inspector can be enabled and still not be covering a resource, and a count of covered resources is the denominator every finding count needs. Neither is decoration; without them the SBOM evidence is a claim about an unknown population.\n\nThe registry configuration has its own version of the trap, and it is the reason no assertion here asserts on rules[] alone. Scan frequency defaults to MANUAL where scan-on-push was not configured, and a registry with NO rules is a registry that scans nothing on push — but it answers with a well-formed scanningConfiguration all the same, so a clause reading 'every rule has SCAN_ON_PUSH or CONTINUOUS_SCAN' is vacuously true on exactly the estate that has configured nothing. The offender form is the population of repositories no rule's filter matches, and that needs a join against describe-repositories this assertion grammar cannot express — which is why this is partial and not full even though the enablement half could be written down.\n\nOn the SBOM export: it needs an S3 bucket whose policy allows Inspector to write and a KMS key whose policy allows Inspector to encrypt, both configured before the call, and it is asynchronous — create returns a reportId and get reports status. Read the unresolved-hash note in the output as evidence rather than noise. A component whose package manager used a version range cannot be mapped to a name and version, and therefore cannot be scanned for vulnerabilities at all; Inspector now includes those hashes in the export. They are the components a vulnerability count does not cover, and an SBOM read without them looks cleaner than the estate is.\n\nKSI-SCR-MON is earned directly: third-party software resources ARE automatically monitored for upstream vulnerabilities, and these commands prove the mechanism is on and covering. KSI-SCR-MIT is claimed for identify-and-review and not for mitigate — nothing here shows a risk being closed, only found.","assertions":[{"field":"batch-get-account-status.accounts[?resourceState.ecr.status!='ENABLED']","op":"count_eq","value":0,"controls":["sr-5"],"description":"Every account the call REPORTED ON has ECR scanning ENABLED. `resourceState.ecr` and its `status` are both required fields, so a never-enabled account answers DISABLED rather than omitting the key — the offender form is used here not to catch a missing key but because it names the failing rows directly, which is the shape that survives an estate this clause is later widened over. It is only half the check; the clause below is the other half."},{"field":"batch-get-account-status.failedAccounts","op":"count_eq","value":0,"controls":["sr-5"],"description":"No account failed to enable Inspector at all. This is the sibling array, and without it the clause above is the ListResolverDnssecConfigs trap in a new costume: an account that could not be enabled — ACCESS_DENIED, BLOCKED_BY_ORGANIZATION_POLICY — is reported in failedAccounts[] and is ABSENT from accounts[], so a clause reading only accounts[] is green on precisely the account whose scanning never started."},{"field":"get-sbom-export.status","op":"eq","value":"SUCCEEDED","controls":["sr-5"],"description":"The SBOM export finished and wrote its documents. Poll until the status is terminal — SUCCEEDED, FAILED or CANCELLED — and assert on that. IN_PROGRESS is not a pass: it is exactly the state in which a report id exists and no inventory has been written, which is the condition this clause exists to catch, so accepting it would be a check that cannot fail."}],"references":[{"title":"Amazon Inspector User Guide: Exporting SBOMs — CycloneDX 1.4 and SPDX 2.3, the create-sbom-export CLI form with its S3 and KMS prerequisites, and the inclusion of unresolved hashes for components that cannot be mapped to a name and version","url":"https://docs.aws.amazon.com/inspector/latest/user/sbom-export.html"},{"title":"Amazon ECR API: GetRegistryScanningConfiguration — returns registryId and scanningConfiguration with scanType and rules[] of {repositoryFilters, scanFrequency}","url":"https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_GetRegistryScanningConfiguration.html"},{"title":"Amazon ECR API: RegistryScanningRule — scanFrequency is SCAN_ON_PUSH | CONTINUOUS_SCAN | MANUAL; ENHANCED supports the first two, BASIC supports SCAN_ON_PUSH, and MANUAL is the default where scan-on-push is not specified","url":"https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_RegistryScanningRule.html"},{"title":"Amazon Inspector API: BatchGetAccountStatus — accounts[] with state.status and resourceState per ec2, ecr, lambda, lambdaCode and codeRepository, and failedAccounts[] as a SIBLING array carrying its own resourceStatus and the errorCodes for accounts that could not be enabled","url":"https://docs.aws.amazon.com/inspector/v2/APIReference/API_BatchGetAccountStatus.html"},{"title":"Amazon Inspector API: ResourceState — `ecr` is a required member and its `status` is required, so a never-enabled account reports DISABLED rather than omitting the key","url":"https://docs.aws.amazon.com/inspector/v2/APIReference/API_ResourceState.html"},{"title":"AWS CLI: inspector2 get-sbom-export — status is SUCCEEDED | IN_PROGRESS | CANCELLED | FAILED; IN_PROGRESS is the state in which a report id exists and no document has been written","url":"https://docs.aws.amazon.com/cli/latest/reference/inspector2/get-sbom-export.html"},{"title":"AWS GovCloud (US) User Guide: Amazon Inspector — available in both GovCloud Regions; Lambda code scanning is not available and the Linux deep-inspection plugin is not FIPS compliant","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-inspector2.html"},{"title":"AWS GovCloud (US) User Guide: Amazon ECR — pull through cache rules are available only within the same partition, public registries are not available, and the ECR Public Gallery is not hosted in the partition","url":"https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-ecr.html"}],"source":"aws","tools":["Amazon Inspector","Amazon ECR"]}],"counts":{"full":0,"partial":50,"narrative":1},"recipesByRequirement":{"MAS-CSO-IIR":["config-asset-inventory","information-location-and-classification"],"MAS-CSO-FLO":["boundary-access-points-and-default-deny","remote-access-authorization-and-monitoring"],"MAS-CSO-MDI":["information-location-and-classification"]}}}