| name | generate-preventive |
| description | Generate preventive and proactive security control artifacts — SCPs, resource policies, KMS key policies, permission boundaries, tag policies, OPA policies, and CloudFormation Guard rules. Reads from mapping-results.json and validated.json. |
| disable-model-invocation | false |
| argument-hint | [--service=<name>] [--include-unverified] |
Service Approval — Generator: Preventive & Proactive Controls
Generate all preventive and proactive security control artifacts from the Controls Matrix.
This covers Organisation×Proactive, Organisation×Preventive, Account×Proactive,
Account×Preventive, Resource×Proactive, and Resource×Preventive cells.
Output: .service-approval/<slug>/05-generate/preventive/ and .service-approval/<slug>/05-generate/proactive/
This is one of 3 focused generate sub-skills. Each writes to separate directories.
Prerequisites
test -f .service-approval/<slug>/04-map/mapping-results.json && echo "mapping-results: OK" || echo "ERROR"
test -f .service-approval/<slug>/03-validate/validated.json && echo "validated: OK" || echo "ERROR"
mkdir -p .service-approval/<slug>/05-generate/{preventive,proactive/opa-policies}
Load controls from mapping-results.json and API surface from validated.json:
python3 -c "
import json
mr = json.load(open('.service-approval/<slug>/04-map/mapping-results.json'))
vj = json.load(open('.service-approval/<slug>/03-validate/validated.json'))
prv = [c for c in mr['controls'] if c['category'] == 'PRV']
pro = [c for c in mr['controls'] if c['category'] == 'PRO']
print(f'Preventive controls: {len(prv)}')
print(f'Proactive controls: {len(pro)}')
print(f'API operations: {len(vj[\"api_surface\"][\"operations\"])}')
"
Unless --include-unverified is set, skip controls where verified: false.
Artifact Header Template
Every generated artifact MUST include these comment lines at the top:
# SCOPE: <org|account|resource>
# LAYER: <proactive|preventive>
# POSTURE: <preventative-request|preventative-proactive>
# CONTROLS: <comma-separated control IDs>
# FRAMEWORK: <framework-name> — <FULL sorted union of ALL MAPPED objective IDs>
# MITIGATIONS: <comma-separated mitigation IDs>
# GENERATED: service-approval v3.0.0
# SERVICE: <service-name>
For JSON files, add as a "_metadata" key AND set _metadata.posture to the exact
posture value from mapping-results.json.controls[].posture for the control this
artifact implements. For HCL/TypeScript, use comment blocks.
CHECK-15 enforcement: Every generated artifact's _metadata.posture MUST match its
directory family:
preventative-request → preventive/{scp,rcp,iam-policies,permission-boundary,resource-policy,vpce-policy,kms-key-policy}*.json
preventative-proactive → proactive/** or iac/**
An SCP tagged with posture: "reactive-detective" would fail the hook — the upstream
map-generate-controls Rule C1 filter should never have produced it. If the hook
fires, trace back to the control's posture in mapping-results.json and fix the root
cause (either the posture classification or the mechanism selection).
The FRAMEWORK line MUST be copied VERBATIM from
mapping-results.json.framework_header_canonical — scripts/map-assemble.py precomputes
this sorted-union string once so every artifact emits the identical header. Do NOT re-derive
the list from _metadata.controls[] or from per-control framework_objectives[] — that
produces per-artifact subsets and fails CHECK-X1 (validate_cross.py:120), which asserts
every artifact shares one framework header. Read framework_header_canonical, paste it
after # FRAMEWORK: (and into _metadata.framework for JSON files), done.
Rule C2 — SAR-driven mechanism contents (REQUIRED)
All mechanisms in this skill that operate at AWS IAM request-time evaluation are
SAR-driven. That includes: SCPs, RCPs, IAM identity-based policies, IAM trust policies,
permission boundaries, resource-based policies, VPCE policies, KMS key policies.
For every such artifact:
-
Every Condition-block key MUST come from validated.json.capabilities.iam.condition_keys[],
the AWS global key set (e.g., aws:PrincipalARN, aws:SourceAccount, aws:RequestTag/*),
or — for KMS key policy files only — the KMS service-owned key set
(kms:ViaService, kms:EncryptionContext:*, kms:GrantOperations, etc.).
-
API parameter paths are NEVER valid condition keys. A literal like
"$.networkConfiguration.awsvpcConfiguration.assignPublicIp" in a Condition block
is silently ignored by AWS IAM — the intended guardrail does not fire. Instead, find
the SAR condition key that gates the parameter (look up the parameter in
validated.json.api_surface.operations[].parameters[] and read its sar_condition_key
field, populated by Phase B enrichment).
-
Every Action in the statement MUST come from validated.json.api_surface.operations[].operation
OR capabilities.iam.permission_only_actions[] (this was already required; restated
here for completeness — SAR-driven mechanisms are grounded in both the action list
AND the condition-key list).
-
Action-name verification (HARD RULE). Every IAM Action string in every policy
artifact (SCP, resource policy, IAM policy, permission boundary, KMS key policy,
VPCE policy) MUST appear verbatim (case-sensitive) in one of:
sar-facts.json action list or permission_only_actions[]
research-api-surface.json.api_surface.operations[].operation
validated.json.api_surface.operations[].operation (post-verification authoritative)
Do NOT generate action names by analogy or extrapolation. If a mitigation statement
references an action in English prose (e.g., "prevent memory searches"), look up the
actual SAR action that implements the concept — do not translate the English verb
into an action suffix. If the service does not implement an action the control
requires, document it as a compensating control in
compensating-controls-documentation.json rather than inventing an action name.
Example of the failure mode (real regression observed in production runs):
- WRONG:
<service-prefix>:SearchRecords — invented by the LLM; does not
exist in SAR or botocore. IAM AccessAnalyzer rejects with "The action X does
not exist."
Validator CHECK-14a enforces rule 1 at hook time. A condition-block key that is not a
SAR/global/KMS key fails the hook with a remediation message. The #1 failure mode this
prevents is an agent treating an API parameter as if it were a condition key — a class
of silent-ignore bug that was endemic before Phase B enrichment.
Zero-service-condition-keys branch
Some services (examples: DataSync, Athena, Step Functions in some configurations)
publish NO service-specific condition keys — only the global tag keys
(aws:RequestTag/${TagKey}, aws:ResourceTag/${TagKey}, aws:TagKeys,
aws:PrincipalTag/${TagKey}) apply. You can tell deterministically by inspecting
validated.json.capabilities.iam.condition_keys[]: if every entry's key starts
with aws:, the service has zero service-specific keys.
In that branch, SCPs and permission boundaries can ONLY gate tag-based controls
(creation-time tagging, tag-mandated resource categories) — they CANNOT gate
service-specific properties like encryption mode, network mode, result location,
workgroup enforcement flags, etc. Do NOT fabricate a service-specific condition
key to make an SCP statement work. CHECK-14a rejects invented keys, and even if
it didn't, AWS IAM would silently ignore the condition and the guardrail would
be a no-op.
Instead, route non-tag gating to mechanisms that can see the property values
directly:
- Preventative-proactive → CloudFormation Guard rules (
proactive/cfn-guard-rules.guard)
and Checkov custom policies (proactive/custom-policies/). These evaluate
template bodies before deploy and have full visibility into every property.
- Reactive-detective → AWS Config Rules with Lambda handlers
(
detective/config-rule-lambdas/**/handler.py). These evaluate deployed
resources via the Config configuration item.
- Reactive-corrective → SSM Automation runbooks (
responsive/ssm-runbook.yaml)
or Step Functions workflows (responsive/stepfunctions-workflow.json) invoked
by EventBridge on the relevant AWS API events.
The map-generate-controls Rule C1 feasibility filter already routes around
SCP/IAM infeasibility at control-selection time — this rule ensures the
generate-preventive skill doesn't produce SCP artifacts whose statements would
be silently ignored at runtime.
Organisation × Proactive
Tag Policy (preventive/tag-policy.json)
- Enforce: Owner, Environment, Purpose, CostCenter + service-specific tags from controls
- Use
awsknowledge MCP to confirm valid tag key formats
OPA Org Policy (proactive/opa-policies/org-policy.rego)
- OPA
.rego rules for org-wide provisioning rules not expressible as tag policies
SCP Provisioning Deny (preventive/scp-provisioning.json)
- Only for controls where
scp_expressible: true with provisioning-time intent
- Follow SCP authoring rules from Organisation × Preventive below
Organisation × Preventive
SCP Runtime Deny (preventive/scp-policy.json)
Only generate if validated.json capabilities.organization_policies.scp_expressible: true.
If false, write _metadata.scp_gap note and skip.
SCP authoring rules (strict):
-
Always "Effect": "Deny" — never Allow-only
-
Always include relevant condition key(s) — never bare action deny without conditions
-
Use real condition keys from awsknowledge MCP — confirm each key is valid for the action
-
No custom placeholders in policy documents: SCPs are IAM policies. Custom placeholders
like ${trusted:account-id} or ${ORGANIZATION_ID} cause MalformedPolicyDocument errors.
IAM policy variables like ${aws:PrincipalAccount} work in real SCPs evaluated by Organizations
but fail when the policy is tested as a standalone IAM managed policy (which a downstream policy tester typically does).
Use literal placeholder values like 123456789012 with a _metadata._deployment_notes field
explaining what values to substitute at deployment time.
-
ARN region and account placeholders (HARD RULE): In ARN region and account
segments, NEVER use shell-style placeholders like ${AWS_REGION}, ${AWS_ACCOUNT_ID},
${ACCOUNT_ID}, ${REGION}. IAM AccessAnalyzer treats them as literal values and
rejects the policy at deploy time with "The Region X is not valid for this resource"
or "The account X is not valid." Valid forms:
- Any region / any account: use
* wildcard.
- Scoped via IAM policy variables:
${aws:RequestedRegion} (region),
${aws:PrincipalAccount} / ${aws:ResourceAccount} (account).
- Literal substitution:
us-east-1 and 123456789012 with
_metadata._deployment_notes explaining the substitution.
Examples:
WRONG:
"Resource": "arn:aws:<service>:${AWS_REGION}:${AWS_ACCOUNT_ID}:<type>/*"
RIGHT (any region, any account):
"Resource": "arn:aws:<service>:*:*:<type>/*"
RIGHT (scoped via IAM policy variables):
"Resource": "arn:aws:<service>:${aws:RequestedRegion}:${aws:PrincipalAccount}:<type>/*"
RIGHT (literal substitution with deployment note):
"Resource": "arn:aws:<service>:us-east-1:123456789012:<type>/*"
"_metadata": {"_deployment_notes": "Replace us-east-1 and 123456789012 with your account's region and ID."}
The shell-style ${AWS_REGION} pattern is a harness-substitution convention only —
it belongs in prose (instructional text), never in Resource ARN
values, Condition key values, or any string IAM parses as policy content.
No non-standard fields in Statements: IAM policy Statements only accept standard
keys (Sid, Effect, Principal, NotPrincipal, Action, NotAction, Resource,
NotResource, Condition). Do NOT add _description, _comment, _control_ids, or
any other underscore-prefixed metadata inside Statement objects — IAM Access Analyzer
and the SCP API will reject them. Put per-statement documentation in the Sid field
(e.g., "Sid": "DenyUnencryptedCreation") or as _metadata at the top level only.
Format:
{ "_metadata": { ... }, "Version": "2012-10-17", "Statement": [ ... ] }
SCP validation (JSON syntax and 5120-byte minified policy body limit, excluding
_metadata) runs automatically via PostToolUse hook (validate_controls.py
Checks 7, 12).
SCP simulation metadata (REQUIRED for every SCP): Each SCP JSON MUST include a
_simulation_tests field in the _metadata object. This tells a downstream test
harness what actions to simulate as denied and what actions should remain allowed.
{
"_metadata": {
"framework": "...",
"controls": ["CTRL-..."],
"_simulation_tests": {
"should_deny": [
{"action": "lambda:CreateFunction", "context": {"lambda:CodeSigningConfigArn": ""}}
],
"should_allow": [
{"action": "lambda:CreateFunction", "context": {"lambda:CodeSigningConfigArn": "arn:aws:lambda:us-east-1:123456789012:code-signing-config:csc-abc"}}
]
}
},
"Version": "2012-10-17",
"Statement": [...]
}
The should_deny list includes actions + context values that the SCP should block.
The should_allow list includes actions + context values that should pass.
A test harness exercising SCPs typically creates an ephemeral test role, attaches
the SCP as an IAM policy, and calls iam:SimulatePrincipalPolicy to verify the
deny/allow behavior matches the test vectors in _simulation_tests.
AI Opt-out Policy (preventive/ai-optout-policy.json)
Only if ai_optout_applicable: true.
Account × Proactive
Rule C3 — API-driven mechanism contents (REQUIRED for every artifact below)
CFN Guard rules, CFN Hooks, Checkov policies, OPA/Rego rules, Terraform variable
validation blocks, and CDK runtime validation are API-driven mechanisms — they
evaluate IaC/template contents before any AWS API call. Their contents MUST be grounded
in validated.json.api_surface.operations[].parameters[]:
-
Every property path referenced by a rule MUST correspond to a parameter in
api_surface.operations[].parameters[]. Do not invent property paths; copy the
path field verbatim (strip the $. prefix if the target DSL uses dotted notation).
CFN Guard caveat — API path ≠ CFN property path. When the rule targets a
CloudFormation resource, the API path from parameters[] is NOT authoritative for
the rule. The CFN template schema is a separate surface: AWS can (and does) expose
the same setting at a different resource type and under a different property name
than the underlying API operation. Example: Fargate ephemeral-storage KMS appears in
the RunTask API, but the CFN schema places it at
AWS::ECS::Cluster.Properties.Configuration.ManagedStorageConfiguration.FargateEphemeralStorageKmsKeyId
— AWS::ECS::TaskDefinition.EphemeralStorage only accepts SizeInGiB. Before
asserting <Path>.<Prop> exists in a Guard rule, verify the property is actually
declared on that resource type in the CFN Template Reference
(https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/AWS_{cfn_prefix}.html)
or via awsiac MCP. An assertion on a non-existent property is doubly broken: the
rule can never pass a real template, AND a "compliant" template written to satisfy
the rule fails cfn-lint E3002 (additional properties not allowed).
-
Every enum-literal list (in [...], AllowedValues: [...], validation {...},
contains([...], var.x)) MUST use exact values from parameters[].enum. Do NOT
re-scrape AWS API reference docs at generate time — the enum values in
validated.json are the single source of truth. Copy them verbatim. Common failure:
substituting IAM for IAM_AUTH, CustomJWT for CUSTOM_JWT_AUTHORIZER, or
inventing camelCase/PascalCase variants — all cause the rule to silently not fire.
-
Every min/max/pattern constraint (TF validation, CFN MinValue/MaxValue/
AllowedPattern) MUST come from parameters[].min / parameters[].max /
parameters[].pattern.
Validator CHECK-14b enforces rule 2 at hook time by scanning the artifact body for
enum-style literal lists and verifying every uppercase-literal value exists in
parameters[].enum. A literal value that is not in any parameter's enum fails the
hook. The check is scoped to API-driven files only — preventive/ SCP-style files are
not evaluated by CHECK-14b (they are evaluated by CHECK-14a).
CloudFormation Guard Rules (proactive/cfn-guard-rules.guard)
Cross-validation constraint: Every Guard rule MUST pass when evaluated against the
compliant CloudFormation template generated by generate-iac. The consistency check is:
cfn-guard validate --rules proactive/CTRL-*.guard --data iac/cloudformation/*.cfn.yaml
If a guard rule flags a violation in the compliant template, either the guard rule
uses an incorrect property path or the CFN template is missing a required property.
Both must be fixed together — trace to the source of the discrepancy.
-
Use awsiac MCP for valid cfn-guard syntax per resource type
-
Every rule MUST have a control ID: rule name includes control ID.
Example: rule enforce_encryption_ctrl_acc_pro_001 { ... } (format example — NOT a literal ID).
Do NOT generate "bonus" rules that are not mapped to any CTRL-* control. If a check is
needed but has no control, either assign it to an existing control or omit it. Unmapped
rules create audit confusion — every rule must trace back to the controls catalog.
-
Mapping-driven ID lookup (REQUIRED — do not invent IDs): Before writing ANY artifact,
load .service-approval/<slug>/04-map/mapping-results.json and find the control whose mechanism
matches the artifact type you are generating. Use the EXACT id field from that control,
verbatim:
- Rule/policy names (Guard, OPA, Checkov): lowercase the ID, convert hyphens to underscores
(e.g.,
CTRL-ACC-PRO-001 → ctrl_acc_pro_001)
- JSON/YAML
_metadata.controls, CFN headers, comments: uppercase form verbatim (CTRL-ACC-PRO-001)
- The ID prefix (
ACC/ORG/RES) is determined by mapping's mechanism→scope table — do
NOT override based on a section heading in this skill.
- If no control in mapping-results.json has a matching mechanism, do NOT generate the artifact.
Pattern-matching an ID from a previous service's run is a bug.
-
One artifact per mechanism per control: Each mapped control represents a distinct
enforcement mechanism. If mapping has N controls whose mechanism field differs
(e.g., one KMS Key Policy, one IAM Trust Policy, one VPC Endpoint Policy, one S3 Bucket
Policy), generate N separate artifacts — do NOT collapse them into a single file just
because they share a category/scope prefix. Collapsing loses the control-to-file
traceability the control-to-file validator enforces (see validate_controls.py CHECK-13).
-
Valid Guard 2.x syntax only: Do NOT use %size() function — it is not standard CFN Guard
2.x syntax. For collection size checks, use some clauses, [N] EXISTS checks, or iterate
with when blocks. Example: check subnet count ≥ 2 with Properties.Subnets[1] EXISTS.
-
Unique let variable names per resource type: Each
block MUST use a UNIQUE variable name across the whole .guard file. cfn-guard
does NOT overwrite a previously declared — redeclaring the same name
parses but produces unpredictable rule dispatch across resource types. For a
service with N CFN resource types, emit N uniquely-named variables:
CloudFormation Hooks Policy (proactive/cfn-hooks-policy.json)
- Use
awsiac MCP to confirm resource type supports CFN Hooks
- Header completeness: The
CONTROLS header MUST list ALL proactive controls, not just a
subset. The hook executes the Guard file which implements all proactive controls, so the hook
header must claim all of them.
Checkov Config (proactive/checkov-config.yaml)
OPA Account Policies (proactive/opa-policies/<ResourceType>.rego)
- One
.rego file per resource type
- Authorizer field completeness: When validating authorizer controls, OPA deny rules
MUST check ALL accepted configuration fields. Verify the exact set via
aws <service> <command> --generate-cli-skeleton. Generate separate deny blocks for
each required field — do not generate checks for fields the API does not accept.
Access-log exemption tag contract (S3)
The CTRL-ACC-PRO-002 rule (require aws_s3_bucket_logging companion on every
aws_s3_bucket) MUST provide an exemption for log-target buckets. Log sink buckets
cannot log to themselves — the companion requirement would force a recursion. The
canonical exemption is a tag with key "bucket-role" and value "log-target",
checked by a predicate named is_log_target(tags):
deny contains msg if {
some rc in input.resource_changes
rc.type == "aws_s3_bucket"
not has_logging
not is_log_target(rc.change.after.tags)
msg := sprintf("CTRL-ACC-PRO-002: aws_s3_bucket %v requires logging (aws_s3_bucket_logging) unless tagged bucket-role=log-target", [rc.address])
}
is_log_target(tags) if {
tags["bucket-role"] == "log-target"
}
generate-iac emits this exact tag on any generated access-log sink bucket (see
generate-iac/SKILL.md "Logging target buckets (S3)"). If you change the tag key,
the tag value, or the predicate name here, update generate-iac/SKILL.md in lockstep —
both skills must agree.
Plan-time unknown attributes (Terraform after_unknown)
When an OPA rego deny rule checks a Terraform resource attribute value that may
reference a sibling resource being created in the same plan (e.g.,
kms_master_key_id = aws_kms_key.bucket.arn where the KMS key is net-new), the rule
MUST accept both forms Terraform emits:
- Known at plan time — attribute value present in
change.after.
- Known after apply — attribute value absent from
change.after, recorded in
change.after_unknown as true. This happens whenever an attribute references a
sibling resource being created in the same plan — typically: KMS keys, SNS topics,
CloudWatch log groups, IAM roles, security groups, VPC endpoints, S3 buckets.
Without the after_unknown branch, the rego incorrectly denies every fresh
terraform plan where the referenced resource is created inline. The rule is
service-agnostic: apply the two-branch pattern to ANY rego rule that checks a
cross-resource reference attribute.
Helper pattern (pass the whole rc.change, not just rc.change.after, so both
branches are reachable):
# Example: CTRL-ACC-PRO-001 SSE-KMS rule
deny contains msg if {
some rc in input.resource_changes
rc.type == "aws_s3_bucket_server_side_encryption_configuration"
not has_kms(rc.change)
msg := sprintf("CTRL-...: %v must use aws:kms with a CMK", [rc.address])
}
# Branch 1: concrete kms_master_key_id in `after`
has_kms(change) if {
some rule in change.after.rule
some sse in rule.apply_server_side_encryption_by_default
sse.sse_algorithm in {"aws:kms", "aws:kms:dsse"}
sse.kms_master_key_id != ""
}
# Branch 2: plan-time-unknown marker in `after_unknown`
has_kms(change) if {
some rule in change.after.rule
some sse in rule.apply_server_side_encryption_by_default
sse.sse_algorithm in {"aws:kms", "aws:kms:dsse"}
some unknown_rule in change.after_unknown.rule
some unknown_sse in unknown_rule.apply_server_side_encryption_by_default
unknown_sse.kms_master_key_id == true
}
Helper signatures MUST take rc.change (not rc.change.after) when they cover
attributes that could be cross-referenced. Existing rego rules using the old
has_X(after) signature continue to work for attributes never referenced across
resources; update signatures when migrating a rule to support cross-referenced
attributes.
Account × Preventive
Permission Boundary (preventive/permission-boundary.json)
Generate ONLY if mapping-results.json contains at least one control with
mechanism == "IAM Permission Boundary". If no such control exists, SKIP
this file entirely — do NOT emit a placeholder and do NOT invent a control ID
(e.g., CTRL-ACC-PRV-BOUNDARY). The resulting artifact must reference ONLY
control IDs present in mapping-results.
- Limit role permissions to needed service actions
- Use
awsknowledge MCP for real action names
- Globally-scoped IAM actions MUST be in their own Statement with
Resource: "*":
Some IAM actions (notably ecr:GetAuthorizationToken, sts:GetCallerIdentity,
sts:AssumeRole against account-wide scope, cloudtrail:DescribeTrails when
service-wide) do NOT accept resource-level permissions and must be granted
Resource: "*". Grouping them in a Statement whose Resource lists ARNs
(e.g., arn:aws:ecr:{region}:{account}:repository/*) produces a boundary that
silently denies them because AWS evaluates per-action resource compatibility.
Split such actions into their own Allow Statement with Resource: "*" and
document the reason in a Sid. Verify against the SAR page: actions whose
Resource types column is empty are global-only and cannot be scoped.
IAM action validation (CRITICAL): Before generating any IAM policy artifact (SCP,
permission boundary, IAM policies, resource policies, VPCE policies), verify that every
Action value exists in validated.json api_surface.operations[].operation OR is listed
as a permission-only action in capabilities.iam.permission_only_actions[]. Permission-only
actions (e.g., InvokeGateway) are valid IAM-evaluated actions without API endpoints — they
CAN be used in policies. Common error: inventing action patterns (e.g., <prefix>:Describe*)
when the service only supports Get* and List* for read operations. Check the service's
IAM actions via awsknowledge MCP or
the IAM Actions Reference. If the service prefix does not support a particular action verb,
do not generate it. This applies to ALL policy files: scp-policy.json, iam-policies.json,
permission-boundary.json, resource-policy.json, vpce-policy.json.
IAM Policies and Trust Policies (preventive/iam-policies.json)
Generate ONLY if mapping-results.json contains at least one control with
mechanism ∈ {"IAM Policy", "IAM Trust Policy"}. If no such control exists,
SKIP this file entirely — do NOT emit a placeholder and do NOT invent a control
ID (e.g., CTRL-ACC-PRV-EXEC-ROLE). The controls field of _metadata must
reference ONLY IDs present in mapping-results.
- Generate standalone IAM policy and trust policy documents for ACC-PRV controls with
mechanism
of "IAM Policy" or "IAM Trust Policy"
- Each control gets a named policy document in the JSON file
- Trust policies: include
Condition block for confused deputy protection
- IAM policies: scope to specific resource ARN patterns, not
*
- Do NOT skip IAM Policy / Trust Policy controls that ARE in mapping-results
— they are distinct from the permission boundary and implement specific scoped
access patterns (execution role policies, trust relationships). A mapped
IAM-Policy-mechanism control with no standalone artifact is a generation gap.
(This directive catches mapper omissions of mapped controls; it does NOT
sanction inventing control IDs when mapping is empty — see the gate above.)
Resource × Proactive
Scope is determined by mapping-results.json, NOT by this heading. CloudFormation Guard
rules map to scope=ACC by default per the map-generate-controls mechanism→scope table (they
run at the account's CFN deploy or as account-scoped Hooks, not attached to an individual
resource ARN). A RES-PRO control exists only if the mechanism is genuinely resource-attached
(rare for Guard — more typical for mechanisms like resource-based policies that also enforce
at create time).
Before generating any artifact in this section, look up the control in
mapping-results.json. If its scope is ACC, emit the rule under the Account × Proactive
section instead — do NOT force-write a CTRL-RES-PRO-* ID just because this section is
titled "Resource × Proactive".
If resource-scoped proactive controls DO exist in mapping, append them to
proactive/cfn-guard-rules.guard with a comment header separating them from account-level
rules and use the control's exact mapping ID for the rule name.
Resource × Preventive
Resource Policy (preventive/resource-policy.json)
Inclusion is data-driven, not a hardcoded service list. Three branches based
on validated.json.capabilities.iam.resource_policies_supported AND
mapping-results.json:
Branch 1 — Service supports RBP AND mapping has a control:
Generate the full policy. Required when
resource_policies_supported: true AND mapping-results.json contains at least
one control with mechanism ∈ {"Resource Policy", "Resource-Based Policy"}.
Branch 2 — Service supports RBP but no control is mapped:
SKIP the file entirely. Do NOT emit a placeholder; do NOT invent a control ID
(e.g., CTRL-RES-PRV-RESPOL). The mapping is the authoritative source of what
to generate.
Branch 3 — Service does NOT support RBP:
When resource_policies_supported: false, emit a NOT_APPLICABLE stub so that
(a) CHECK-X2 control-coverage passes for any RBP-intended control that couldn't
be rendered, (b) CHECK-X1 framework-header consistency is satisfied, and (c)
reviewers see the absence is intentional rather than a generation miss:
{
"_metadata": {
"status": "NOT_APPLICABLE",
"reason": "Service does not expose resource-based policies (validated.json.capabilities.iam.resource_policies_supported: false)",
"posture": "preventative-request",
"controls": [],
"framework": "<copy from mapping-results.framework_header_canonical>",
"mitigations": [],
"generated": "service-approval v3.0.0",
"service": "<service-name>"
},
"Statement": []
}
The stub keeps the framework header consistent across artifacts without
fabricating policy content that AWS IAM would silently ignore. Do NOT use this
stub as a shortcut when Branch 1 applies — only when RBP is genuinely
unsupported.
Determine support from validated.json, NOT a hardcoded service list. RBP
support changes over time (e.g., DynamoDB gained resource-based policies in
late 2024; Lambda gained function URL resource policies earlier; EventBridge
gained bus resource policies). Read
validated.json.capabilities.iam.resource_policies_supported — that flag is
populated by Phase 0 Assess from the AWS documentation and Phase 2 Validate
from MCP verification.
When generating (Branch 1):
- Use
awsknowledge MCP for real ARN patterns and action names
- MANDATORY confused deputy protection on resource-based policies: EVERY resource-based
policy Statement (policies attached TO a resource like Runtime or Gateway) MUST include
"Condition": { "StringEquals": { "aws:SourceAccount": "${AWS::AccountId}" } } at minimum.
For service-to-service calls, also add "ArnLike": { "aws:SourceArn": "arn:aws:<service>:<region>:<account>:*" }.
A resource-based policy without confused deputy conditions is a security gap.
- VPC endpoint policies are different: VPCE endpoint policies control who can use the
endpoint, not who can access the resource. Use
aws:PrincipalOrgID for org scoping, NOT
aws:SourceAccount/aws:SourceArn (confused deputy conditions do not apply to endpoint policies).
KMS Key Policy (preventive/kms-key-policy.json)
Generate ONLY if mapping-results.json contains at least one control with
mechanism == "KMS Key Policy". If no such control exists, SKIP this file
entirely — do NOT emit a placeholder and do NOT invent a control ID (e.g.,
CTRL-RES-PRV-KMS). "Encryption capability exists in validated.json" is not a
sufficient trigger; mapping decides whether a KMS key policy is in scope.
- Include
kms:ViaService condition keyed to the caller service
RCP (preventive/rcp-policy.json)
Generate ONLY if mapping-results.json contains at least one control with
mechanism == "RCP" AND rcp_expressible: true in validated.json. If no
mapped RCP control exists, SKIP this file entirely — do NOT emit a placeholder
and do NOT invent a control ID (e.g., CTRL-RES-PRV-RCP). Only when a control
is mapped to RCP but the service is not RCP-expressible (rcp_expressible: false) do you write _metadata.rcp_gap and skip the policy body.
- Gap placeholder metadata: Even gap/skip files MUST include complete
_metadata with ALL
required fields (scope, layer, controls, framework, mitigations, generated, service). The
controls field can be [] for gaps, but framework and mitigations MUST be populated
with the full union lists. A gap file with missing metadata fields breaks downstream validation.
- Always
"Effect": "Deny" and "Principal": "*"
"BoolIfExists": {"aws:PrincipalIsAWSService": "false"}
"StringNotEquals": {"aws:PrincipalOrgID": "${ORG_ID}"}
Declarative Policy (preventive/declarative-policy.json)
Only if declarative_policy_expressible: true.
Backup Policy (preventive/backup-policy.json)
Only if backup_policy_applicable: true.
Apply Enrichment Opportunities
For each enrichment_opportunity in validated.json with verified: true:
- Find the referenced control in already-generated artifacts
- Add the enhancement (inject condition key, add
kms:ViaService, etc.)
- Generate concrete IAM policy example inline
Validate Generated Artifacts
Artifact validation runs automatically via PostToolUse hook (validate_controls.py) and Stop hook (validate_cross.py Check X9).
Completion checklist (REQUIRED — run before returning)
Before declaring the run complete, iterate this checklist. Each mandatory artifact
is in one of three terminal states:
- EMITTED — file written to disk, references the expected control IDs via header comment
- N/A — explicit gate evaluated false per the skill rule (document which gate)
- SKIPPED-ERROR — run aborted before this artifact could be generated (STOP and report)
Check every row. Do NOT return success if any row is SKIPPED-ERROR or if a row is
both expected (mapping-results has a matching control) and not EMITTED.
| Artifact | Gate (emit only if…) | Expected when |
|---|
preventive/scp-policy.json | control has mechanism == "SCP" | always for SAR-eligible services |
preventive/scp-provisioning.json | control has mechanism == "SCP" with provisioning-time intent | platform services with admin/delegation controls |
preventive/rcp-policy.json | rcp_expressible: true AND RCP-mechanism control | rare, service-dependent |
preventive/declarative-policy.json | declarative_policy_expressible: true | only EC2/VPC/EBS today |
preventive/ai-optout-policy.json | ai_optout_applicable: true | Bedrock/Comprehend/etc. |
preventive/backup-policy.json | backup_policy_applicable: true | service has AWS Backup integration |
preventive/permission-boundary.json | control has mechanism == "IAM Permission Boundary" | any service with Acc×Preventive |
preventive/iam-policies.json | control has mechanism == "IAM Policy" (trust policy, etc.) | SLR/trust-policy controls |
preventive/resource-policy.json | control has mechanism == "Resource Policy" on primary service | services with RBP |
preventive/vpce-policy.json | control has mechanism == "VPCE Policy" | services with VPC endpoints + endpoint-policy support |
preventive/kms-key-policy.json | control has mechanism == "KMS Key Policy" | services with CMK consumption |
proactive/tag-policy.json | any control requires tag enforcement | always (most services) |
proactive/cfn-guard-rules.guard | any resource-scope control exists |
Before returning, emit a concise report:
Preventive completion: N EMITTED, K N/A (gated), 0 SKIPPED-ERROR
Emitted: scp-policy.json, permission-boundary.json, kms-key-policy.json, ...
N/A (gate false):
- declarative-policy.json (declarative_policy_expressible=false)
- ai-optout-policy.json (ai_optout_applicable=false)
- rcp-policy.json (rcp_expressible=false)
If K SKIPPED-ERROR > 0, do NOT claim success — report the missing artifacts and
the reason (context exhaustion, subagent timeout, upstream data missing, etc.)
so the orchestrator can retry or adjust.
Print Summary
Generate (preventive/proactive) complete:
Org×Proactive: tag-policy, OPA org policy [, scp-provisioning]
Org×Preventive: scp-policy [, ai-optout-policy]
Acc×Proactive: cfn-guard-rules, cfn-hooks-policy, checkov-config, OPA policies
Acc×Preventive: permission-boundary
Res×Proactive: cfn-guard resource rules
Res×Preventive: resource-policy [, kms-key-policy] [, rcp-policy]
Output: .service-approval/<slug>/05-generate/preventive/, .service-approval/<slug>/05-generate/proactive/