agentleFS
AWSKnown publisherawslabs/service-onboarding-accelerator-on-aws/skills/map-generate-controls/SKILL.md

map-generate-controls

Skill0 starsChanged 18 days agoApache-2.0View on GitHub

Generate security controls from mitigations x capabilities, organized by Category x Scope matrix. Produces map-controls-generated.json with rich metadata for downstream framework mapping. Consumes validated.json.

We split this skill into the rules it gives an agent

39 rules. About 0 are common, 10 are distinctive, and 29 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈10

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 29

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Every string this skill emits (control name, description, security_rationale, mapped-objective rationale) MUST be defensible from the input …

    line 17
  2. Distinctive:

    If you are not 100% certain a specific claim holds, omit it or hedge to the broad objective level — describe the *category* of mechanism …

    line 31
  3. Very specific:

    If assessment_gaps[] exists in validated.json:

    line 102
  4. Distinctive:

    Check if any existing mitigation addresses this gap

    line 104
  5. Very specific:

    If not, flag as unaddressed_gap — must generate controls for it

    line 105
  6. Very specific:

    Read the mitigation_goal — defines WHAT needs to be controlled

    line 116
  7. Very specific:

    Read request_parameters[] — the API parameters that enforce the control

    line 117
  8. Very specific:

    Look up the corresponding capability in capabilities{} — defines HOW the service supports it

    line 118
  9. Distinctive:

    Write the security_rationale — 1-2 short sentences naming the mitigation goal and the mechanism family chosen to enforce it.

    line 120
  10. Very specific:

    When assigning condition_keys and actions to a control, verify that each condition key actually applies to the specified actions.

    line 131
  11. Distinctive:

    Before assigning a condition key to a control:

    line 136
  12. Very specific:

    Look up the key in capabilities.iam.condition_keys[]

    line 137
  13. Very specific:

    Read its applies_to_actions[] list

    line 138
  14. Very specific:

    If the action is NOT in applies_to_actions[], do NOT use that condition key for that action.

    line 140
  15. Distinctive:

    Instead, find an alternative enforcement mechanism (resource policy, Config rule, etc.)

    line 141
  16. Distinctive:

    Before assigning mechanisms, classify each mitigation's posture and verify mechanism

    line 161
  17. Very specific:

    Persist the fallback log: Every entry appended to classification_fallback_log[] MUST be written into map-controls-generated.json as a …

    line 239
  18. Very specific:

    Look up P in validated.json.api_surface.operations[].parameters[] and read gateable_by[], sar_condition_key, enum, mutability:

    line 247
  19. Very specific:

    Do NOT pick VPCE Policy as the mechanism for a control whose parameters_controlled[] references a request-body field of a Create/Update …

    line 269
  20. Very specific:

    If feasibility holds, emit a control in map-controls-generated.json.controls[] with:

    line 287
  21. Very specific:

    If feasibility FAILS, append to map-controls-generated.json.coverage_gaps[]:

    line 292
  22. Very specific:

    Use the spelling "preventative" in prose throughout (the posture vocabulary already does).

    line 335
  23. Distinctive:

    If a new mechanism is proposed that is not in this table, map-generate-controls MUST add a row explicitly — never infer scope ad-hoc.

    line 385
  24. Distinctive:

    Every control MUST include these fields:

    line 393
  25. Very specific:

    Do NOT: use service-prefixed variants (S3-RES-PRO-01, ATH-ORG-PRV-01, LAMBDA-ACC-DET-03).

    line 421
  26. Very specific:

    Use mechanism types: SSM Automation, Lambda Remediation, EventBridge + Step Functions.

    line 469
  27. Very specific:

    Check if any of its actions[] appear in a technique's api_surface_evidence[]

    line 481
  28. Very specific:

    If so, add that technique's ID to attack_techniques[] on the control

    line 482
  29. Very specific:

    Every control MUST have a non-empty attack_techniques[] — if no direct action match exists, use mechanism alignment to identify relevant …

    line 488
  30. Very specific:

    When two candidate controls would share that identity, emit one control and union the source mitigation_ids[] (and the other traceability …

    line 506
  31. Very specific:

    After generating all controls, verify that EVERY mitigation in existing_mitigations[] has at least one control referencing its ID in …

    line 563
  32. Very specific:

    Any other: Read mitigation_goal and request_parameters[] to determine domain, then generate.

    line 587
  33. Very specific:

    Keep the resolver's mitigation_references[{alias,uuid}] as canonical provenance.

    line 599
  34. Very specific:

    Run python3 tools/validate/audit_threat_model.py --slug <slug> after writing outputs.

    line 603
  35. Very specific:

    After generating all controls and verifying mitigation coverage, tag each control with a canonical mitigation_intent and security_domain …

    line 609
  36. Very specific:

    If no pattern matches, set mitigation_intent = "unclassified" and security_domain = null — the mapping skill will flag these for manual …

    line 623
  37. Very specific:

    When a control traces to multiple mitigations with different intents (e.g., a consolidated Config rule), use the intent of the primary …

    line 635
  38. Distinctive:

    After intent tagging, thread the source mitigation's AWS Threat Model identity and MITRE ATT&CK techniques onto each control.

    line 642
  39. Distinctive:

    Run the resolver over the AWS Threat Model input and thread onto each control (additive fields):

    line 660

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Service Approval — Map: Generate Controls

Using the mitigation inventory and verified service capabilities, generate security controls organized by the Category x Scope matrix. Each control traces back to mitigation(s) and includes the metadata downstream skills (framework-mapping, generate) need without re-deriving the reasoning chain.

Authoring rule — state only what is verified

Every string this skill emits (control name, description, security_rationale, mapped-objective rationale) MUST be defensible from the input data:

  • API operations and parameters MUST appear in validated.json.api_surface.operations[].
  • IAM action strings MUST come from the AWS Service Authorization Reference IAM action list (resolved via the operation→action map at data/botocore-iam-alias.json) or validated.json.capabilities.iam.permission_only_actions[].
  • Condition keys MUST appear in validated.json.capabilities.iam.condition_keys[] or be cited from a specific public AWS doc URL.

If you are not 100% certain a specific claim holds, omit it or hedge to the broad objective level — describe the category of mechanism (SCP, AWS Config rule, KMS key policy, etc.) and what objective the control serves, and stop there. Do NOT assert that an action list is complete, that a particular condition key fires under specific runtime context not present in the input data, or that the control causally prevents a named attack without an explicit ATT&CK technique cross-reference. Causal- filler and absolute-overreach phrases ("ensures", "guarantees", "prevents all", "fully mitigates", "completely blocks", bare "prevents" / "blocks" / "blocking", "cannot", absolute "only", "denies … untrusted", "confused-deputy" as a claim) are banned in description and security_rationale — prefer "supports", "enforces at request time", "constrains to the listed actions", or similar. The tools/validate/lint_control_prose.py lint flags these phrases plus an over-length budget on each prose field, and it also scans the rendered .md files so a render-time re-wording cannot slip a stronger claim past the source JSON.

Input: .service-approval/<slug>/03-validate/validated.json Output: .service-approval/<slug>/04-map/map-controls-generated.json Schema: schemas/map-controls-generated.schema.json — encodes shape, id pattern ^CTRL-(ORG|ACC|RES)-(PRO|PRV|DET|COR)-\d{3}$, scope/category enums, mitigation_ids[] pattern ^M-.+, per-control required fields.

This is one of 4 focused map sub-skills. Each writes to a separate intermediate state file.


Prerequisites

test -f .service-approval/<slug>/03-validate/validated.json && echo "validated: OK" || echo "ERROR: run validator first"
mkdir -p .service-approval/<slug>/04-map

Load validated.json and extract:

python3 -c "
import json
vj = json.load(open('.service-approval/<slug>/03-validate/validated.json'))
print(f'Assets: {len(vj[\"assets\"])}')
print(f'Operations: {len(vj[\"api_surface\"][\"operations\"])}')
print(f'Mitigations: {len(vj[\"existing_mitigations\"])}')
print(f'Condition keys: {len(vj[\"capabilities\"][\"iam\"][\"condition_keys\"])}')
gaps = vj.get('assessment_gaps', [])
print(f'Assessment gaps: {len(gaps)}')
print(f'SCP: {vj[\"capabilities\"][\"organization_policies\"][\"scp\"][\"supported\"]}')
print(f'RCP: {vj[\"capabilities\"][\"organization_policies\"].get(\"rcp\", {}).get(\"supported\", False)}')
"

Phase 1: Build Mitigation Index

For each mitigation in existing_mitigations[], build a rich index:

{
  "id": "M-<Resource>.0",
  "statement": "...",
  "mitigation_goal": "...",
  "request_parameters": ["$.encryptionKeyArn", "$.executionRoleArn"],
  "resource_type": "AWS::<Service>::<Resource>",
  "verified": true,
  "verification_note": "..."
}

Group mitigations by resource_type for efficient lookup during control generation.

Cross-reference with assessment gaps

If assessment_gaps[] exists in validated.json:

  • For each gap with status = REQUIREMENT GAP or CUSTOMER ACTION REQUIRED:
    • Check if any existing mitigation addresses this gap
    • If not, flag as unaddressed_gap — must generate controls for it

Phase 2: Generate Controls

For each mitigation, generate controls using the Category x Scope matrix.

2.1 Control generation logic

For each mitigation:

  1. Read the mitigation_goal — defines WHAT needs to be controlled
  2. Read request_parameters[] — the API parameters that enforce the control
  3. Look up the corresponding capability in capabilities{} — defines HOW the service supports it
  4. Generate one or more controls that enforce the mitigation goal using available capabilities
  5. Write the security_rationale — 1-2 short sentences naming the mitigation goal and the mechanism family chosen to enforce it. Stay at the objective level: name the control category (SCP, Config rule, KMS key policy) and the parameter/action it constrains. Do NOT claim the control prevents specific attacks unless an ATT&CK technique is cross-referenced in the same control. The rationale is read by map-framework-mapping; broad, verified statements are more useful than confident-sounding speculation.

CRITICAL — Condition key action applicability:

When assigning condition_keys and actions to a control, verify that each condition key actually applies to the specified actions. The SAR page defines which condition keys work with which actions — a condition key used in an SCP/IAM Deny on an action it doesn't apply to will be silently ignored by IAM, making the control ineffective.

Before assigning a condition key to a control:

  1. Look up the key in capabilities.iam.condition_keys[]
  2. Read its applies_to_actions[] list
  3. Confirm that EVERY action in the control's actions[] appears in applies_to_actions[]
  4. If the action is NOT in applies_to_actions[], do NOT use that condition key for that action. Instead, find an alternative enforcement mechanism (resource policy, Config rule, etc.)

Common error: using a resource-type-specific condition key (e.g., one that applies to Gateway operations) on a different resource type's actions (e.g., Runtime). Each resource type may have different applicable condition keys.

2.2 Control sources to enumerate

  • IAM controls — from capabilities.iam (condition keys, resource policies, permission boundaries, SCPs)
  • Encryption controls — from capabilities.encryption (KMS CMK, SSE, TLS, key rotation)
  • Network controls — from capabilities.network (VPC, endpoints, PrivateLink, security groups)
  • Logging controls — from capabilities.logging (CloudTrail, CloudWatch, data events)
  • Configuration controls — from capabilities.config (Config rules, conformance packs)
  • Organization policy controls — from capabilities.organization_policies (SCPs, RCPs, declarative policies)
  • Tagging controls — tag policies, required tags
  • Availability controls — backup, multi-AZ, lifecycle
  • AI-specific controls — guardrails, prompt validation (if applicable)

2.2.1 Rule C1 — Posture × feasibility filter (REQUIRED)

Before assigning mechanisms, classify each mitigation's posture and verify mechanism feasibility against the parameter's gateable_by and mutability fields in validated.json.api_surface.operations[].parameters[]. A critical mitigation produces controls at multiple postures (defense-in-depth) — one control per feasible posture.

Posture values:

  • preventative-request — AWS IAM evaluates the condition at request time (SCP, RCP, IAM policy, trust policy, permission boundary, KMS key policy, resource-based policy, VPCE policy). Requires "sar" in the parameter's gateable_by[].
  • preventative-proactive — IaC/CI pipeline evaluates the template before any AWS call (CFN Guard, CFN Hooks, Checkov, OPA, Terraform validation, CDK runtime validation). Requires "api" in gateable_by[].
  • reactive-detective — AWS Config or Access Analyzer flags existing state (Config rule Lambda, managed Config rule, Access Analyzer). Requires "api" in gateable_by[].
  • reactive-corrective — remediation after a detective finding (SSM Automation, Step Functions, Lambda remediator). Requires "api" in gateable_by[] AND mutability != "create_only".

Posture classification (three-pass derivation from mitigation_goal, theme nouns, and title/statement fallback):

Mitigation goals vary in style across sources. Some are verb-led ("Enforce KMS encryption"); others are noun-phrase themes ("Encryption at Rest — CMK", "Network Perimeter — Approved Networks"). Run all three passes and take the union before falling back.

posture_set = []
haystack_goal = mitigation.mitigation_goal

# ── Pass 1 — Verb pass against mitigation_goal ──────────────────────────
if haystack_goal matches /prevent|block|disallow|deny|mandate/:                posture_set += [preventative-proactive, preventative-request]
if haystack_goal matches /enforce|require|ensure.*(?:at creation|configured|set)/:
                                                                                posture_set += [preventative-proactive, preventative-request]
if haystack_goal matches /encrypt|cmk|kms key|customer-managed key/:           posture_set += [preventative-proactive, preventative-request]
if haystack_goal matches /protect|safeguard|guard against/:                    posture_set += [preventative-proactive, preventative-request]
if haystack_goal matches /enable.*(?:logging|telemetry|monitoring)|configure.*(?:at creation|upfront)|ensure.*(?:available|enabled|active)/:
                                                                                posture_set += [preventative-proactive]
if haystack_goal matches /detect|monitor|audit|flag|observe|alert|visibility|anomaly|forensic/:
                                                                                posture_set += [reactive-detective]
if haystack_goal matches /remediat|rollback|restore|auto-repair|revert/:       posture_set += [reactive-corrective]
if haystack_goal matches /\bcorrect\b.*(?:state|configuration|drift|violation)/
   AND NOT                /correct (?:classification|tagging|labeling)/:       posture_set += [reactive-corrective]

# ── Pass 2 — Noun-phrase themes against mitigation_goal ─────────────────
# Noun-phrase goals like "Identity Perimeter" or "Encryption at Rest" have
# no verb to match; their posture is implied by the theme.
if haystack_goal matches /perimeter|boundary|allow.?list|deny.?list/:          posture_set += [preventative-proactive, preventative-request]
if haystack_goal matches /at rest|in transit|encryption/:                      posture_set += [preventative-proactive, preventative-request]
if haystack_goal matches /logging|telemetry|observability|audit trail/:        posture_set += [preventative-proactive, reactive-detective]
if haystack_goal matches /tagging|governance/:                                 posture_set += [preventative-proactive]
if haystack_goal matches /remediation|recovery|rollback/:                      posture_set += [reactive-corrective]

# ── Pass 3 — Title + statement fallback (only if passes 1 & 2 empty) ────
# Some mitigations compress the actionable signal into the title or the
# statement text rather than the goal field. Re-run BOTH pass 1 and pass 2
# regexes against (title + " " + statement) as a last resort.
if posture_set is empty AND (mitigation.title or mitigation.statement):
    haystack_fallback = (mitigation.title or "") + " " + (mitigation.statement or "")
    <re-run pass 1 AND pass 2 patterns against haystack_fallback>

# Critical mitigations get ALL FOUR postures where feasible (defense-in-depth)
if criticality == "critical":
    posture_set = [preventative-request, preventative-proactive, reactive-detective, reactive-corrective]

# Final fallback when all three passes return empty
if posture_set is empty:
    posture_set = [preventative-proactive]
    append to `classification_fallback_log` (emit in map-controls-generated.json):
        {"mitigation_id": M.id, "reason": "no keyword match in goal/title/statement"}

Over-generation is safe. The feasibility filter (next section) drops postures that have no gateable parameter — so a noun-phrase match that fires multiple postures does not pollute the output. A mitigation goal like "Audit data at rest" legitimately warrants BOTH reactive-detective (from audit) and preventative-proactive|request (from at rest); the filter resolves which layer(s) are actually buildable.

Persist the fallback log. Every entry appended to classification_fallback_log[] MUST be written into map-controls-generated.json as a top-level key (not just printed to stdout). Downstream consumers and validators rely on the JSON record to verify that the fallback only fires on genuinely ambiguous goals.

Feasibility filter per posture (per parameter path P in mitigation.request_parameters):

Look up P in validated.json.api_surface.operations[].parameters[] and read gateable_by[], sar_condition_key, enum, mutability:

PostureFeasibility requirementMechanism family
preventative-request"sar" in gateable_by (requires sar_condition_key)SCP, RCP, IAM identity policy, trust policy, permission boundary, resource-based policy, KMS key policy, VPCE policy
preventative-proactive"api" in gateable_byCFN Guard, CFN Hook, Checkov, OPA, Terraform validation, CDK runtime validation
reactive-detective"api" in gateable_byAWS Config rule (managed or custom Lambda), Access Analyzer
reactive-corrective"api" in gateable_by AND mutability != "create_only"SSM Automation runbook, Step Functions workflow, Lambda remediator

When VPCE Policy is NOT a valid preventative-request mechanism:

VPC endpoint policies appear in the preventative-request mechanism family above, but they have a narrower preventive scope than the other policies in that row. A VPCE policy gates who can use the VPC endpoint to reach a service (Action × Principal × ResourceArn at the endpoint boundary). It does not see request-body parameters such as lambda:CreateFunction.VpcConfig.SubnetIds, s3:PutObject.ServerSideEncryption, or any nested config field of the caller's request payload. The endpoint-policy evaluation context is the endpoint itself, not the API request body.

Do NOT pick VPCE Policy as the mechanism for a control whose parameters_controlled[] references a request-body field of a Create/Update operation (subnet selection, encryption-key parameter, KMS key ARN on create, etc.). A VPCE policy cannot enforce those at request time. The correct mechanism family for "<service> must be created with parameter X set to Y" is SCP / IAM identity policy / IAM permission boundary (when a SAR condition key for the parameter exists) and CFN Guard / CFN Hook / OPA (template-time, preventative-proactive).

VPCE Policy is appropriate when the control gates endpoint use: restricting which principals/actions can flow through the endpoint, or enforcing aws:PrincipalOrgID on traffic that egresses the VPC. If the mitigation request_parameters[] is an endpoint-level concern (e.g., "only org members may invoke any action via this VPCE"), VPCE Policy is correct and preventative-request applies; otherwise, route the control to the mechanisms above.

Emit one control per feasible posture. For each posture in posture_set:

  • If feasibility holds, emit a control in map-controls-generated.json.controls[] with:
    • posture: one of the four values above
    • mechanism: selected from the mechanism family (canonical snake_case via the mechanism→scope table in §2.3)
    • source: derived from the parameter's gateable_by ("sar", "api", or "both")
  • If feasibility FAILS, append to map-controls-generated.json.coverage_gaps[]:
    {"mitigation_id": "M-X.0", "posture": "reactive-corrective",
     "reason": "parameter is create_only — cannot remediate post-create"}

A single critical mitigation can produce up to 4 controls (one per posture). A low-priority non-critical mitigation typically produces 1-2.

Downstream contract: The posture field is propagated through map-assemble into mapping-results.json.controls[].posture, then baked into each generated control file's _metadata.posture. Validator CHECK-15 enforces posture ↔ directory consistency at write time — a reactive-detective control that lands in preventive/scp-policy.json will fail the hook.

2.3 Control Matrix — Category x Scope

Assign each control a primary category (X-axis) and a scope level (Y-axis):

Categories (X-axis) — canonical definitions. Each category is defined by what it does (purpose) and the AWS implementation primitives that realise it. This is the single authoritative definition of the four categories; the customer-facing controls-catalog.md renders the same purpose text.

CategoryCodePurposeImplementation primitives
ProactivePROValidate resource configuration before creation and block non-compliant resources from being createdCloudFormation Hooks (via Control Tower), CloudFormation Guard (CI/CD), OPA rules (CI/CD)
PreventativePRVStop non-compliant actions — hard guardrails that deny API calls regardless of the caller's IAM permissionsSCPs (org), RCPs (resource), Declarative Policies (service), IAM permissions boundaries (entity), VPC endpoint policies
DetectiveDETIdentify non-compliance after deploymentAWS Config rules, Security Hub controls, GuardDuty findings, CloudTrail, Amazon Inspector
Corrective (Responsive)CORRemediate after detectionSSM Automation runbooks, Config auto-remediation (→ SSM), Lambda remediation, Security Hub automated response, EventBridge → remediation workflows

Category ↔ posture correspondence. This pipeline carries two vocabularies for the same distinction: the four control categories above (PRO/PRV/DET/COR, the schema enum on controls[].category) and the four postures on controls[].posture (§2.2.1). They map one-to-one:

CategoryPosture
PRO (Proactive)preventative-proactive
PRV (Preventative)preventative-request
DET (Detective)reactive-detective
COR (Corrective/Responsive)reactive-corrective

Use the spelling "preventative" in prose throughout (the posture vocabulary already does); the schema code stays PRV. "Responsive" is the short label used for the COR column and the scope-matrix axis; "Corrective" is its category name.

The Examples-oriented restatement below keeps the previous phrasing for quick reference — the purpose + primitives table above is authoritative:

CategoryCodeDefinitionExamples
ProactivePROPrevents non-compliant resources from being createdCFN Guard, SCP deny, Config proactive rules, OPA
PreventativePRVBlocks unauthorized actions at runtimeIAM policies, resource policies, SCPs, VPC SGs, KMS key policies
DetectiveDETIdentifies non-compliant or suspicious state after the factConfig rules, CloudTrail analysis, CloudWatch alarms
Corrective (Responsive)CORAutomatically remediates non-compliant stateSSM runbooks, Lambda remediation, EventBridge workflows

Scope Levels (Y-axis):

ScopeCodeDefinitionExamples
OrganisationORGApplied across the AWS OrganizationSCPs, tag policies, Config org rules
AccountACCApplied within a single AWS accountIAM policies, permission boundaries, Config rules
ResourceRESApplied directly to an individual resourceResource-based policies, KMS key policies

Deterministic mechanism → scope mapping (REQUIRED for stability across runs):

To prevent iteration-to-iteration drift, every control's scope MUST be derived from its mechanism using this table. Do NOT choose scope by thematic heading (e.g., "Resource × Proactive" in generate-preventive) — the mechanism alone determines scope.

MechanismScopeRationale
SCP (Service Control Policy)ORGAttached at the Organization / OU level
RCP (Resource Control Policy)ORGOrganization policy type
Tag policyORGOrganizations-native
Config org rule / conformance pack (org)ORGDeployed via Organizations
CloudFormation Guard ruleACCEnforced at account's CFN deploy (or per-account Hook)
CloudFormation HookACCRegistered per-account
OPA / Rego policyACCEvaluated per-account at CI / deploy
Checkov custom policyACCEvaluated per-account at CI / deploy
IAM identity-based policy (user/role)ACCPrincipals live at the account level
IAM permission boundaryACCAttached to an account-level principal
AWS Config rule (account)ACCDeployed per-account
EventBridge rule (default bus)ACCDefault event bus is account-scoped
CloudWatch alarm / metric filterACCPer-account resources
SSM runbook / automation documentACCDocument is account-scoped
Lambda remediatorACCFunction lives in an account
Step Functions workflowACCState machine lives in an account
Resource-based policy (S3 bucket policy, SNS topic policy, Lambda resource policy, SQS queue policy)RESAttached to a specific resource ARN
KMS key policyRESAttached to a specific key
VPC endpoint policyRESAttached to a specific endpoint

If a new mechanism is proposed that is not in this table, map-generate-controls MUST add a row explicitly — never infer scope ad-hoc. This makes control ID prefixes stable across runs, which is required for downstream generate skills to produce consistent artifact IDs.

Each control MUST have exactly one category AND one scope level.

2.4 Control output schema

Every control MUST include these fields:

{
  "id": "CTRL-ORG-PRV-001",
  "name": "Enforce CMK encryption on <Resource> creation via SCP",
  "description": "Constrains <Resource> creation to require encryption at request time via SCP",
  "posture": "preventative-request",
  "source": "sar",
  "scope": "ORG",
  "category": "PRV",
  "mechanism": "SCP",
  "mitigation_ids": ["M-<Resource>.0", "M-<Resource>.3"],
  "parameters_controlled": ["$.encryptionKeyArn"],
  "condition_keys": ["<prefix>:encryptionKeyArn"],
  "actions": ["<prefix>:Create<Resource>", "<prefix>:Update<Resource>"],
  "resource_types": ["AWS::<Service>::<Resource>"],
  "assessment_gap_ids": [],
  "security_rationale": "Supports encryption-at-rest by gating the create call on the customer-managed key parameter at request time.",
  "attack_techniques": ["T1530", "T1486"],
  "framework_keywords": ["encryption", "key management", "data protection", "policy enforcement"]
}

Field requirements:

  • id: Format MUST match the regex ^CTRL-(ORG|ACC|RES)-(PRO|PRV|DET|COR)-\d{3}$ — literal CTRL- prefix, scope token, category token, three-digit sequence. Example: CTRL-ORG-PRV-001, CTRL-ACC-DET-012, CTRL-RES-PRO-003. Do NOT use service-prefixed variants (S3-RES-PRO-01, ATH-ORG-PRV-01, LAMBDA-ACC-DET-03). The service identity lives in _metadata.service and validated.service — duplicating it inside the control ID creates drift that fails validate_state.py CHECK-S11 and validate_controls.py CHECK-13 body scans. Service-agnostic uniformity lets every downstream check be a literal string match rather than "strip prefix, then compare". Validator CHECK-S11 enforces this regex enum; non-conforming IDs fail state validation.
  • posture: One of preventative-request, preventative-proactive, reactive-detective, reactive-corrective. Derived from the classification in §2.2.1. Propagated downstream to mapping-results.json.controls[].posture by map-assemble, then to each generated artifact's _metadata.posture so CHECK-15 can enforce posture ↔ directory consistency.
  • source: One of sar, api, both — derived from the parameter's gateable_by[]. Indicates which data source authoritatively drives the mechanism's contents.
  • scope: Abbreviated code: ORG, ACC, RES
  • category: Abbreviated code: PRV, PRO, DET, COR
  • mechanism: AWS enforcement mechanism (SCP, CloudFormation Hook, AWS Config Rule, VPC Endpoint Policy, IAM Permission Boundary, Resource Policy, IAM Trust Policy, CloudTrail, CloudWatch Alarm, CloudWatch Logs, Kinesis Data Streams, SSM Automation, EventBridge + Step Functions, Tag Policy, OPA Policy, CloudFormation Guard, Compensating Control Documentation)
  • mitigation_ids: Non-empty array — every control MUST trace to at least one mitigation
  • parameters_controlled: API parameters this control enforces (from mitigation request_parameters[])
  • condition_keys: IAM condition keys used (empty array for non-IAM controls)
  • actions: IAM actions restricted/monitored (empty array for non-IAM controls)
  • resource_types: AWS resource types affected (CFN format)
  • assessment_gap_ids: Assessment gaps addressed (empty array if none)
  • security_rationale: ONE broad sentence stating the control's intent at objective level — name the kind of setting it governs and the objective it supports. Do not assert a causal guarantee, enumerate verbatim actions, or list condition keys here (those live in the structured actions[] / condition_keys[] fields the generate phase consumes). State only what is verified; a broad true sentence beats a precise uncertain one. Banned causal-filler words (ensures, guarantees, prevents all, bare prevents/blocks, cannot, absolute only, …) are rejected by lint_control_prose.py. Prefer "supports / constrains / enforces at request time". This field is the PRIMARY context for downstream framework-mapping — write it broad but accurate.
  • attack_techniques: ATT&CK technique IDs this control mitigates (empty array if attack_surface not present in validated.json). Populated by cross-referencing the control's actions[], condition_keys[], and mechanism against attack_surface.applicable_techniques[].api_surface_evidence[]. A control mitigates a technique if it restricts/monitors any action listed in that technique's evidence.
  • framework_keywords: 4-8 keywords for framework objective matching. Include BOTH domain keywords (encryption, access, network) AND mechanism keywords (deny, monitor, audit, enforce, restrict).

2.5 Responsive control generation

For every detective control, evaluate whether a responsive control is appropriate. Generate responsive controls for at minimum:

  • Encryption drift: auto-remediate resources created without CMK encryption
  • Authentication drift: alert + optional auto-remediate gateways with AuthorizerType=NONE
  • Tag compliance: auto-tag resources missing mandatory tags
  • VPC configuration drift: alert on resources created without VPC networking

Link each responsive control to the same mitigation IDs as its corresponding detective control. Use mechanism types: SSM Automation, Lambda Remediation, EventBridge + Step Functions.

2.6 Assessment gap coverage

For every REQUIREMENT GAP and CUSTOMER ACTION REQUIRED from assessment_gaps[]:

  • Generate at least one control that addresses the gap
  • Link via assessment_gap_ids[] on the control

2.7 ATT&CK technique cross-reference (mandatory)

validated.json always contains attack_surface.applicable_techniques[] (MITRE ATT&CK analysis is mandatory for every service onboarding). For each control:

  1. Check if any of its actions[] appear in a technique's api_surface_evidence[]
  2. If so, add that technique's ID to attack_techniques[] on the control
  3. Also check mechanism alignment:
    • SCP/IAM controls → relevant to T1078, T1098, T1548 (identity techniques)
    • Encryption controls → relevant to T1530, T1486 (data techniques)
    • Network controls → relevant to T1190, T1133 (access techniques)
    • Logging controls → relevant to T1562, T1070 (evasion techniques)
  4. Every control MUST have a non-empty attack_techniques[] — if no direct action match exists, use mechanism alignment to identify relevant techniques

2.7.1 Canonical control identity — avoid emitting structural duplicates

Per-resource mitigation variants (e.g. CapacityProvider / EventSourceMapping / Layer / CodeSigningConfig twins of a Function control) often produce structurally identical controls that differ only by ID — same name, scope, category, mechanism, resource_types, actions, and posture. These inflate the control count and the coverage numbers without adding coverage.

Two controls have the same canonical identity when they share:

(normalized name, scope, category, mechanism,
 sorted(resource_types), sorted(actions), posture)

When two candidate controls would share that identity, emit one control and union the source mitigation_ids[] (and the other traceability lists) onto it — do not emit a second ID. If two controls differ only by which resource type they target but enforce the same action via the same mechanism, collapse them to one control and keep resource_types[] as a list so traceability survives; controls that gate different actions or use a different mechanism stay distinct.

A deterministic safety net runs in map-assemble Phase 0 (dedup_controls()): it collapses any remaining duplicates by this same canonical key before the rest of the pipeline consumes the controls, and tools/validate/validate_control_dedup.py fails the map gate if two controls in mapping-results.json still share the key. Generating cleanly here keeps the catalog honest before the safety net even runs.

2.8 Compensating control documentation

Some capabilities are NOT available for the service (e.g., RCP not supported, no delegated admin, ABAC partial). For each unavailable capability referenced in assessment_gaps or where controls cannot be implemented:

  • Generate a control with mechanism: "Compensating Control Documentation"
  • Describe what compensating measures should be taken
  • Include in the controls array like any other control

2.8.1 Doc reference for non-AWS Threat Model controls (doc_reference)

Controls that are NOT sourced from an AWS Threat Model mitigation (trace_source: "assessment_gap" — the compensating / assessment-gap controls from §2.6 and §2.8) do not carry an AWS Threat Model citation, so the framework-mapping report cites an AWS documentation source for them instead. Attach a doc_reference to every such control:

"doc_reference": {
  "url": "https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_rcps.html#rcp-supported-services",
  "title": "AWS Organizations — List of AWS services that support RCPs"
}

Rules:

  • The url MUST be a real https://docs.aws.amazon.com/... page, MCP-verified (awsknowledge / aws-documentation) and logged to mcp-calls.log — never a training-data guess (see "Verify, never hardcode").
  • Make it as specific as possible so a reviewer can cross-check it: deep-link to the exact page and, where the page has one, the #section anchor for the relevant list or section (e.g. the RCP supported-services list, the declarative-policy supported-services list) rather than a service landing page.
  • Pick the page that documents why the compensating control exists — typically the AWS doc that establishes the capability gap the control compensates for (e.g. the RCP or declarative-policy supported-services list when the service is absent from it).
  • AWS Threat Model-sourced controls (trace_source: "threat_model") do NOT get a doc_reference — they cite their AWS Threat Model mitigation. doc_reference is optional in the schema and present only on non-AWS Threat Model controls.

Phase 3: Mitigation Coverage Completeness Check (MANDATORY)

CRITICAL — Do NOT write output until this check passes.

After generating all controls, verify that EVERY mitigation in existing_mitigations[] has at least one control referencing its ID in mitigation_ids[]:

all_mit_ids = set(m['id'] for m in existing_mitigations)
covered_mit_ids = set()
for ctrl in controls:
    covered_mit_ids.update(ctrl.get('mitigation_ids', []))
uncovered = all_mit_ids - covered_mit_ids
if uncovered:
    print(f"UNCOVERED MITIGATIONS: {sorted(uncovered)}")
    # MUST generate controls for each — see guidance below

For each uncovered mitigation, generate feasible controls using this guidance or record the explicit source-specific disposition described below:

  • Availability mitigations (multi-AZ, redundancy): CloudFormation Guard rule + Config rule checking subnet AZ diversity. Distinct from "has VPC subnets."
  • Authentication configuration mitigations (JWT, OIDC): Guard rule validating full auth config (discovery URL, audiences, clients, scopes). Distinct from "authorizer type != NONE."
  • Network perimeter mitigations (approved networks): VPC endpoint policies + IAM conditions restricting by source VPC/endpoint.
  • Data lifecycle mitigations (retention, deletion): Config rules checking resource config.
  • Any other: Read mitigation_goal and request_parameters[] to determine domain, then generate.

Re-run accounting after gap filling. Every source mitigation needs controls or an explicit disposition; do not invent automated controls to reach zero gaps.

For manual guidance, not-applicable decisions, or unresolved coverage, write mitigation_dispositions[] in map-controls-generated.json. Each entry requires mitigation_id (internal M-* ID), mitigation_alias, mitigation_uuid (source), status (manual_guidance, not_applicable, or unresolved), and a specific rationale. manual_guidance also requires concrete guidance. The source pair must match the saved model. Unresolved entries remain visible gaps.

Keep the resolver's mitigation_references[{alias,uuid}] as canonical provenance. Legacy alias/UUID arrays must have matching positions; primary UUID must belong to primary alias. Preserve repair metadata. Assembly repairs unique known pairs and records what changed; ambiguous source identities are reported, not guessed. Run python3 tools/validate/audit_threat_model.py --slug <slug> after writing outputs.


Phase 3b: Mitigation Intent Tagging (MANDATORY)

After generating all controls and verifying mitigation coverage, tag each control with a canonical mitigation_intent and security_domain derived from its source mitigation statement. This step is deterministic — no LLM reasoning needed.

Input: data/mitigation-intent-patterns.json — controlled vocabulary of ~20 intent patterns with match_statements[] (case-insensitive substring/regex patterns matched against the mitigation statement field).

Algorithm:

  1. For each control, find its source mitigation(s) via mitigation_ids[]
  2. For each source mitigation, get the statement text from validated.json.existing_mitigations[]
  3. Match the statement against data/mitigation-intent-patterns.json patterns (first match wins)
  4. Set mitigation_intent = matched pattern's intent_id
  5. Set security_domain = matched pattern's security_domain
  6. If no pattern matches, set mitigation_intent = "unclassified" and security_domain = null — the mapping skill will flag these for manual review

Example:

  • Mitigation statement: "An AWS Lambda function should be encrypted with an AWS KMS Customer Managed Key (CMK)"
  • Matches pattern: "encrypted with an AWS KMS Customer Managed Key"
  • Result: mitigation_intent: "cmk_encryption", security_domain: "data_protection_at_rest"

Why this matters: The mitigation_intent field constrains downstream framework mapping. A control tagged cmk_encryption can only map to objectives in the encryption/data-protection domains — it CANNOT be mapped to IAM, logging, or network objectives regardless of keyword overlap.

When a control traces to multiple mitigations with different intents (e.g., a consolidated Config rule), use the intent of the primary mitigation (first in mitigation_ids[] array).


Phase 3c: AWS Threat Model Traceability Threading (MANDATORY)

After intent tagging, thread the source mitigation's AWS Threat Model identity and MITRE ATT&CK techniques onto each control. This step is deterministic — the MITRE techniques are recovered from the AWS Threat Model source, never invented or fetched from an external service.

Why. An AWS Threat Model mitigation carries an alias (e.g. Mitigation.LAMBDA.Function.1) and a UUID; the MITRE ATT&CK technique IDs live on the threat objects the mitigation addresses. Threading these onto controls lets the final framework-mapping.md cite the exact AWS Threat Model mitigation and MITRE technique behind every mapped row.

Resolver: scripts/threat_model_trace.py walks the golden-source chain mitigation.mappings[type=="THREAT"].id → threats[].id → threat.mappings[type=="FRAMEWORK", framework.name=="MITRE ATT&CK"].id (also accepting legacy frameworkName) to build, per mitigation, {alias, statement, mitre_techniques[], threat_aliases[]}.

Algorithm:

  1. For each control, collect the alias of each source mitigation (from validated.json.existing_mitigations[], matched by mitigation_ids[]) and stamp it as mitigation_aliases[] on the control.
  2. Run the resolver over the AWS Threat Model input and thread onto each control (additive fields):
    import importlib.util, json
    spec = importlib.util.spec_from_file_location("threat_model_trace", "scripts/threat_model_trace.py")
    ct = importlib.util.module_from_spec(spec); spec.loader.exec_module(ct)
    threat_model = json.load(open("<the AWS Threat Model input json>"))   # the intake threat-model file
    ct.thread_controls(controls, threat_model)
    Each control gains mitigation_alias, mitigation_aliases[], mitigation_uuid, mitigation_uuids[], mitre_techniques[], and trace_source: "threat_model".
  3. No-AWS Threat Model / synthetic controls. A control derived from an assessment gap (no real AWS Threat Model mitigation behind it) has no resolvable alias — the resolver marks it trace_source: "assessment_gap" with an empty mitre_techniques[]. This is expected, not an error.

Provenance note: mitre_techniques[] here is resolved deterministically from the AWS Threat Model source and is authoritative for downstream citation. It is distinct from the heuristic attack_techniques[] (§2.7), which is derived by action/mechanism alignment; keep both.


Phase 4: Write Output

Write .service-approval/<slug>/04-map/map-controls-generated.json:

{
  "schema_version": "1.0",
  "service": "<service-name>",
  "controls": [
    {
      "id": "CTRL-ORG-PRV-001",
      "name": "...",
      "description": "...",
      "posture": "preventative-request",
      "source": "sar",
      "scope": "ORG",
      "category": "PRV",
      "mechanism": "SCP",
      "mitigation_ids": ["M-<Resource>.0"],
      "parameters_controlled": ["$.encryptionKeyArn"],
      "condition_keys": ["<prefix>:encryptionKeyArn"],
      "actions": ["<prefix>:Create<Resource>"],
      "resource_types": ["AWS::<Service>::<Resource>"],
      "assessment_gap_ids": [],
      "security_rationale": "...",
      "framework_keywords": ["encryption", "key management"],
      "mitigation_intent": "cmk_encryption",
      "security_domain": "data_protection_at_rest"
    }
  ],
  "control_summary": {
    "total_controls": 60,
    "by_scope": { "ORG": 14, "ACC": 27, "RES": 19 },
    "by_category": { "PRV": 20, "PRO": 10, "DET": 18, "COR": 12 },
    "by_mechanism": { "SCP": 10, "AWS Config Rule": 12 }
  },
  "mitigation_coverage": {
    "total_mitigations": 37,
    "covered": 37,
    "uncovered": 0,
    "uncovered_ids": []
  },
  "assessment_gap_coverage": {
    "total_gaps": 13,
    "covered": 10,
    "uncovered": 3,
    "uncovered_ids": ["GAP-3", "GAP-7", "GAP-11"]
  },
  "coverage_gaps": [
    {
      "mitigation_id": "M-<Resource>.2",
      "posture": "reactive-corrective",
      "reason": "Parameter $.encryptionKey is create_only — cannot remediate post-create"
    }
  ]
}

The coverage_gaps[] array is produced by Rule C1's feasibility filter — one entry per (mitigation, posture) pair where no feasible mechanism exists. map-assemble forwards these into mapping-results.json.coverage_gaps[].


Validate Output

Output validation runs automatically via PostToolUse hook. The JSON Schema (see Schema above) hard-fails on shape / id-pattern / scope-category enum / required-field drift. validate_state.py::check_controls_generated adds the cross-file and cross-field checks the schema cannot express: duplicate control IDs, scope/category mismatch between id and fields, mitigation coverage against validated.json, and control_summary.total_controls vs actual length.

Print Summary

Map (Generate Controls) complete:
  Controls: {total} ({by_scope}, {by_category})
  Mitigations covered: {covered}/{total} (100%)
  Assessment gaps covered: {covered}/{total}
  Output: .service-approval/<slug>/04-map/map-controls-generated.json

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.