From d801c9972c7ab4b533f17622cc2b59dc71fe8e91 Mon Sep 17 00:00:00 2001 From: "arunim2@gmail.com" Date: Thu, 22 Oct 2026 10:34:24 +0000 Subject: [PATCH] chore: sync skills from sg-clickhouse-mcp@ed30e3c --- skills/sg-manage-roles/SKILL.md | 148 +++++ .../SKILL.md | 178 ++++++ .../examples/README.md | 23 + .../examples/approvals.tf | 63 ++ .../examples/config_rules.tf | 109 ++++ .../examples/network_guardrails.tf | 64 ++ .../examples/no_public_s3.tf | 68 ++ .../examples/opa_and_marketplace.tf | 70 +++ .../examples/required_tags.tf | 61 ++ .../examples/versions.tf | 17 + .../references/schema.md | 57 ++ .../references/sources-and-lifecycle.md | 212 +++++++ .../references/tirith.md | 197 ++++++ .../SKILL.md | 124 ++++ .../examples/groups.tf | 54 ++ .../examples/versions.tf | 17 + .../SKILL.md | 217 +++++++ .../examples/README.md | 21 + .../examples/automation_opentofu.tf | 98 +++ .../examples/custom_steps.tf | 148 +++++ .../examples/git_terraform.tf | 94 +++ .../examples/git_vcs_triggers.tf | 51 ++ .../examples/outputs_and_references.tf | 75 +++ .../examples/own_template.tf | 130 ++++ .../examples/versions.tf | 17 + .../references/automation-and-outputs.md | 189 ++++++ .../references/discovery.md | 59 ++ .../references/execution.md | 244 ++++++++ .../references/lifecycle.md | 163 +++++ .../references/schema.md | 588 ++++++++++++++++++ .../references/sources-and-templates.md | 234 +++++++ 31 files changed, 3790 insertions(+) create mode 100644 skills/sg-manage-roles/SKILL.md create mode 100644 skills/sg-writing-stackguardian-policy-terraform/SKILL.md create mode 100644 skills/sg-writing-stackguardian-policy-terraform/examples/README.md create mode 100644 skills/sg-writing-stackguardian-policy-terraform/examples/approvals.tf create mode 100644 skills/sg-writing-stackguardian-policy-terraform/examples/config_rules.tf create mode 100644 skills/sg-writing-stackguardian-policy-terraform/examples/network_guardrails.tf create mode 100644 skills/sg-writing-stackguardian-policy-terraform/examples/no_public_s3.tf create mode 100644 skills/sg-writing-stackguardian-policy-terraform/examples/opa_and_marketplace.tf create mode 100644 skills/sg-writing-stackguardian-policy-terraform/examples/required_tags.tf create mode 100644 skills/sg-writing-stackguardian-policy-terraform/examples/versions.tf create mode 100644 skills/sg-writing-stackguardian-policy-terraform/references/schema.md create mode 100644 skills/sg-writing-stackguardian-policy-terraform/references/sources-and-lifecycle.md create mode 100644 skills/sg-writing-stackguardian-policy-terraform/references/tirith.md create mode 100644 skills/sg-writing-stackguardian-workflow-group-terraform/SKILL.md create mode 100644 skills/sg-writing-stackguardian-workflow-group-terraform/examples/groups.tf create mode 100644 skills/sg-writing-stackguardian-workflow-group-terraform/examples/versions.tf create mode 100644 skills/sg-writing-stackguardian-workflow-terraform/SKILL.md create mode 100644 skills/sg-writing-stackguardian-workflow-terraform/examples/README.md create mode 100644 skills/sg-writing-stackguardian-workflow-terraform/examples/automation_opentofu.tf create mode 100644 skills/sg-writing-stackguardian-workflow-terraform/examples/custom_steps.tf create mode 100644 skills/sg-writing-stackguardian-workflow-terraform/examples/git_terraform.tf create mode 100644 skills/sg-writing-stackguardian-workflow-terraform/examples/git_vcs_triggers.tf create mode 100644 skills/sg-writing-stackguardian-workflow-terraform/examples/outputs_and_references.tf create mode 100644 skills/sg-writing-stackguardian-workflow-terraform/examples/own_template.tf create mode 100644 skills/sg-writing-stackguardian-workflow-terraform/examples/versions.tf create mode 100644 skills/sg-writing-stackguardian-workflow-terraform/references/automation-and-outputs.md create mode 100644 skills/sg-writing-stackguardian-workflow-terraform/references/discovery.md create mode 100644 skills/sg-writing-stackguardian-workflow-terraform/references/execution.md create mode 100644 skills/sg-writing-stackguardian-workflow-terraform/references/lifecycle.md create mode 100644 skills/sg-writing-stackguardian-workflow-terraform/references/schema.md create mode 100644 skills/sg-writing-stackguardian-workflow-terraform/references/sources-and-templates.md diff --git a/skills/sg-manage-roles/SKILL.md b/skills/sg-manage-roles/SKILL.md new file mode 100644 index 0000000..8b0fe43 --- /dev/null +++ b/skills/sg-manage-roles/SKILL.md @@ -0,0 +1,148 @@ +--- +name: sg-manage-roles +description: Guided flow for creating, changing and deleting StackGuardian custom roles with the create_role / update_role / delete_role tools — turning "let team X read/run workflows in group Y" into the right permission names and scopes. Use when the user wants a new role, wants to widen or narrow what a role can do, asks why a role grants nothing, or wants to retire a role. +--- + +# Manage Roles — Guided Flow + +A StackGuardian **role** is a named bundle of **permissions**. Each permission +is one platform capability (`GetWorkflow`, `CreateWorkflowRun`, ...) plus a +**scope**: for every placeholder the permission carries, a list of full-match +regex **patterns** over resource IDS. Five **predefined roles** (ADMIN, DEV, +SEC, OPS, READ_ONLY) are code and cannot be changed; everything else is a +**custom role**. Roles are allow-only and additive — a user with several +roles gets the union. Only ADMIN can manage roles. Follow the steps IN ORDER. +Never surface parameter names like `resourceName` or `` to the user; +say "the role's name", "workflow group". + +**What these tools can't do — say so up front, before building anything:** +they can't list users, look up a person, assign a role to anyone, or show a +role's current permissions. "Give Alice read access to group X" ends with +the user assigning the role in the dashboard's Access Management, and any +change to an existing role needs its current patterns from the user. + +## The rules that make a role silently wrong + +1. **Patterns match IDS, not display names.** The scope is matched against + the URL segment, and an id can differ from the name: a group created in + the dashboard as `gitops` may have the id `gitops-y8pf2mb1`. A pattern + `gitops` would then save fine and grant nothing. Look every named group, + workflow, connector, policy or secret up with + `search_orchestrator_resources` and use the last segment of its + `ResourceId`. A NESTED group is its full id-path: the ids after each + `/wfgrps/` in its `ParentId`, then its own id, joined by `/` + (`company-x1/team-y2`). The bare leaf grants nothing on it, and would + match an unrelated top-level group of that id. Roles created by + `create_role` have id = name. +2. **Patterns are full-match, anchors implicit.** `team-a` grants exactly + `team-a`; write `team-a-.*` for a prefix. Never `^` or `$` — the platform + strips them from anywhere, so `[^/]+` would invert. No inline flags + (`(?i)`), named groups, backreferences, nested repetition + (`(?:x+)+`) or big counted repeats (`a{5000}`): the tool refuses them. +3. **GET on a group path grants every ancestor, no descendants.** + `company/team` also grants `company`, never `company/team/project`. The + platform splits a group pattern at each `/`, so a `/` may only separate + whole segments: not inside `[...]` or `(...)`, and not with a top-level + `|` (use one row per path). +4. **Root-level Create* patterns match a NAME; CreateNestedWorkflowGroup + matches a PATH** (`parent/child-.*`). +5. **User management matches a principal** via `` (local|sso) + and `` (an email pattern like `.*@example\.com`, or an SSO + group name). `` must be `.*`: the platform ignores it, so + `group` would not keep email users out. It also needs `GetRole` scoped + to the roles the holder may assign — that scope IS the list of roles + they can hand out, so `.*` lets them invite admins. + +Scope lists are index-parallel rows: `{"": ["prod", "dev"], "": +[".*", "x-.*"]}` is two grants, (prod, all) and (dev, x-*). Use `a|b` inside +one pattern for alternatives; all lists must be the same length. Any ONE row +that is all `.*` makes the whole permission org-wide. + +## Step A — Translate the ask into permissions + +1. Check whether a role already does the job: + `search_orchestrator_resources(resourceTypes=['ROLE'])` (names and + descriptions only). Reusing one beats creating a near-duplicate. +2. Ask what the holder should be able to DO and WHERE, then resolve every + WHERE to an id (rule 1): + +| User says | Permissions (scope the placeholders they named, `.*` for the rest) | +| --- | --- | +| see workflows in group G | `GetWorkflowGroup` {G}, `GetWorkflow` {G, .*}, `GetWorkflowRun` {G, .*, .*} | +| run workflows in G | + `CreateWorkflowRun` {G, .*}, `GetWorkflowRunLogs` | +| create workflows in G | + `CreateWorkflow` {G, .*} (pattern = the new workflow's name) | +| manage a group's stacks | `GetStack`, `CreateStack`, `UpdateStack`, `CreateStackRun`, `GetStackRun` | +| use connector C | `GetIntegration` {C} ("connector" is `Integration` on the wire) | +| read secrets/policies | `GetSecret` {name}, `GetPolicy` {name} | +| invite / remove users | `InviteUser`/`UpdateUser`/`DeleteUser` + `GetRole` {assignable roles} — privileged | +| manage roles / API keys / billing | `CreateRole`… / `*APIAccess` (regenerating a key is `UpdateAPIAccess`) / `ManageBilling` — privileged | + +An unknown permission name returns the full catalog grouped by area +(`availablePermissions`) with `didYouMean`; a missing placeholder returns +`requiredPlaceholders`. There is no "everything" shorthand — an admin-like +role lists every permission explicitly. + +## Step B — Create + +1. Ask for a name: letters, digits, `_`, `-` (it becomes the permanent id; + no rename). Not a predefined name. +2. If any permission is privileged (roles, users, API access, billing, or + `GetRole` next to user management), tell the user plainly that holders + could change who can do what, and only on an explicit yes call with + `confirmPrivileged=true`. If a write permission (anything but Get*/List*) + is scoped to ALL resources, the tool asks for `confirmOrgWide=true`: + first try to narrow the scope to what the user actually said; confirm + only when "everything" is what they mean. +3. Call `create_role(resourceName, permissions=[{permission, scope}, ...], + description?, tags?)`. +4. Walk the user through the returned `permissions` table row by row, in + words ("workflow group prod, any workflow") — it is the only way they + catch a wrong pairing — and relay `warnings`. +5. Assignment has no tool here: the user assigns the role to people or SSO + groups in the dashboard's Access Management. Changes apply on the + holder's next request. + +## Step C — Change + +1. Find the id: `search_orchestrator_resources(resourceTypes=['ROLE'])` or + the `availableRoles` list a tool error returns. Ids are case-sensitive. +2. `setPermissions` REPLACES a stored entry of the same permission name in + full, and no tool shows the stored patterns. Before replacing one, ask + the user for its current patterns (the role's page in the dashboard's + Access Management) and resend them along with the change — otherwise + widening `prod` to `prod|dev` silently drops whatever else was there. + Adding a permission the role doesn't hold, `removePermissions`, and + `description`/`tags` need no read. Never use `delete_role` to read a role. +3. `update_role(roleId, setPermissions=[...], removePermissions=[names], + description?, tags?)`. Untouched permissions are kept. Writing a + privileged permission (new or rewritten) needs `confirmPrivileged`; an + org-wide write scope needs `confirmOrgWide`. The response's + `permissions` is the full table after the write — show it. +4. A V3.BETA role is rewritten as equivalent V4 rows on its first permission + edit (`migratedFromV3`); say so. A role stored with NO version grants + nothing today: ANY update turns all of its permissions on, so the tool + gates them all — tell the user that before confirming. An update that + would leave zero permissions is refused — use Step D. +5. A stored permission with an EMPTY scope is an unrestricted grant; the tool + warns — offer to scope it. + +## Step D — Delete + +1. Call `delete_role(roleId)` WITHOUT `confirmDelete`. The error carries + `holders` (users and SSO groups bound to it, with their other roles), + `permissionsSnapshot` and the `cascade` note. +2. Tell the user: deletion strips the role from every holder atomically, a + holder with no other role loses all access, API-key bindings are not + cleaned, and there is no undo (keep the snapshot to recreate). +3. Only after an explicit yes, call `delete_role(roleId, confirmDelete=true)`. + Predefined names are refused outright. + +## Why does my role grant nothing? + +Ask the user to open the role in the dashboard and read you its permissions. +Then check: a pattern is a display name where the id differs (rule 1); an +exact name where a prefix was meant (rule 2); a GET on a child path was +expected to cover descendants (rule 3); user management without `GetRole` +(rule 5); the role was stored without a version (the gateway ignores it +until an update rewrites it); the holder's token predates an SSO group +change (needs a fresh login). diff --git a/skills/sg-writing-stackguardian-policy-terraform/SKILL.md b/skills/sg-writing-stackguardian-policy-terraform/SKILL.md new file mode 100644 index 0000000..d9bcbb5 --- /dev/null +++ b/skills/sg-writing-stackguardian-policy-terraform/SKILL.md @@ -0,0 +1,178 @@ +--- +name: sg-writing-stackguardian-policy-terraform +description: Use when writing, reviewing or debugging Terraform/OpenTofu HCL for StackGuardian policies (stackguardian_policy, policies_config, enforced_on, Tirith JSON, OPA/Rego, marketplace policy templates, cost limits, approval gates, config-time workflow rules), attaching a policy to a workflow, group or connector — or when a policy applies but never blocks, blocks every plan, never asks for approval, or its scope silently disappears. Also covers resolving scopes and dependencies via the StackGuardian MCP server. +--- + +# Writing StackGuardian policy Terraform + +## Overview + +A `stackguardian_policy` is a list of rules (`policies_config`) plus a scope (`enforced_on`). +**Attachment lives only on the policy** — workflows, groups and templates have no policy +attribute. Rules are evaluated at two different moments: + +| `meta.required_provider` | Evaluated | Verdict effect | +|---|---|---| +| `stackguardian/terraform_plan`, `stackguardian/infracost`, OPA (`OPA_REGO`) | in the plan step of TERRAFORM/OPENTOFU runs (also CloudFormation/Pulumi steps), before `apply`; not on drift or `plan-without-policy` runs, not in Ansible/Kubernetes steps | FAIL errors the run; APPROVAL_REQUIRED pauses before the next step | +| `stackguardian/json` (config rule) | when a workflow in scope is **created/updated** (also via Terraform) and when a run is **built** | FAIL = `403 Workflow policy (:) failed …` on create/update (live); at run build the run is refused or gated at step 0 | + +The provider validates nothing and the Tirith engine fails *open* on many mistakes, so a wrong +rule usually applies cleanly and then passes everything. Provider **1.12.3**, Tirith **1.0.5** +(core; the terraform step on `main` pins 1.0.7 with the same semantics). "(live)" = reproduced against a real org. Setup (source address, +`api_uri`): **sg-writing-stackguardian-workflow-terraform**. + +## Discover scope and dependencies first (StackGuardian MCP) + +`enforced_on` entries are never validated, so a typo silently attaches nothing. **If the +StackGuardian MCP server is connected, take scope strings and ids from it** (rows must carry +`OrgId = "/orgs/"`; read tools only): + +| Need | MCP call | Value to use | +|---|---|---| +| group scope | `search_orchestrator_resources(resourceTypes=["WORKFLOW_GROUP"], searchQuery=…)` | `SubResourceId` as-is (`/wfgrps/PROD/GCP-4jc2fd3k`) — not `ResourceId` (leaf only) | +| workflow / stack scope | `search_workflows(searchQuery=…)`, `search_stacks` | `SubResourceId` as-is (`/wfgrps//wfs/`, `/wfgrps//stacks/`) | +| connector scope | `search_orchestrator_resources(resourceTypes=["INTEGRATION.CLOUD"])` | `ResourceId` (`/integrations/`) | +| blast radius before applying | `search_workflows(parentId="/orgs//wfgrps/", count=true)`, `search_workflows(cloudIntegrationIds=[…])`, `cross_reference(targetKind="cloud_integration", targetId=…, count=true)` | how many workflows the policy will judge | +| existing policies | `search_policies(searchQuery=…)`, `get_orchestrator_resource_by_id(parentId="/orgs/", resourceId="/policies/")` | name collisions, current rules and `Scope` | +| marketplace policy template | `get_subscription(subscriptionTypes=["IAC_POLICY"])` | `//` + a revision that exists | +| git repo auth (OPA / git Tirith) | `search_orchestrator_resources(resourceTypes=["INTEGRATION.VCS"])`, `["SECRET"]` | `auth` = `ResourceId` / `/secrets/` | +| config-rule key paths | `get_orchestrator_resource_by_id` on a real workflow | exact field names (`TerraformConfig`, `DeploymentPlatformConfig`, `RunnerConstraints`, …) | + +Without MCP: literal paths from the user plus data-source existence checks — never guessed. + +## Core template (runtime rule on the Terraform plan) + +```terraform +resource "stackguardian_policy" "no_public_s3" { + resource_name = "no-public-s3" # slug: it is the id, and approvals match on it + policy_type = "GENERAL" # the only value the provider can write + description = "Block public S3 ACLs" + tags = ["security"] + enforced_on = ["/wfgrps/${stackguardian_workflow_group.payments.id}"] # ALWAYS declare (see rule 1) + approvers = [] # declare, even when empty + number_of_approvals_required = 0 + + policies_config = [{ + name = "s3_acl_not_public" # slug, unique in the policy + skip = false + on_fail = "FAIL" # FAIL | WARN | PASS | APPROVAL_REQUIRED + on_pass = "PASS" # REQUIRED too: omitted -> 400 `onPass: "" is not a valid choice` (live) + + policy_vcs_config = { + use_marketplace_template = false + custom_source = { source_config_kind = "SG_POLICY_FRAMEWORK", source_config_dest_kind = "INLINE" } + } + + policy_input_data = { + schema_type = "TIRITH_JSON" + data = jsonencode({ # only meta, evaluators, eval_expression at the top level + meta = { version = "v1", required_provider = "stackguardian/terraform_plan" } + evaluators = [{ + id = "acl_not_public" # snake_case; never a prefix of another id + provider_args = { + operation_type = "attribute" + terraform_resource_type = "aws_s3_bucket_acl" + terraform_resource_attribute = "acl" + } + condition = { + type = "NotContainedIn" # found value must NOT be in this list + value = ["public-read", "public-read-write", "authenticated-read"] + error_tolerance = 1 # skip plans without aws_s3_bucket_acl (0 = FAIL them) + } + }] + eval_expression = "acl_not_public" # && || ! ( ) only + }) + } + }] +} +``` + +## Rules that bite + +1. **Declare `enforced_on`, `tags`, `approvers`, `number_of_approvals_required`, `description` and + `policies_config` every time.** They are Optional+Computed without state carry-over: omit one + while changing anything else and the provider sends `null` (the live test wiped `enforced_on` to + `[]`, silently detaching the policy); an omitted `number_of_approvals_required` is sent as `0` — + *all* approvers. `enforced_on = []` or no attribute = attached to **nothing**; org-wide is `["*"]`. +2. **Scope paths** are exact strings, never validated: `"*"`, `/wfgrps/` (includes nested + groups, stacks, workflows), `/wfgrps//wfs/`, `/wfgrps//stacks/[/wfs/]`, + `/integrations/` (every workflow deploying through it), + `/integrationgroups/[/integrations/]`. Nested group: `/wfgrps/parent/child`. No trailing `/`. +3. **Condition direction:** `Contains` = found value contains yours; `ContainedIn` = found value is + in your list (allow-list); `NotContainedIn` = deny-list. Types are case-sensitive; an unknown + type (`In`, `Exists`, `StartsWith`, `NotRegexMatch`) fails every input. +4. **`error_tolerance`** (inside `condition`): 0 fails plans that lack the resource type, 1 skips + them, 2 also skips resources missing the attribute. A fully skipped rule **passes**. +5. **Terraform plan actions are `create`, `update`, `delete`, `no-op`, `read`** — never `apply` or + `destroy`. The provider docs' `NotEquals "apply"` approval example never fires. +6. **APPROVAL_REQUIRED** needs `approvers` + `number_of_approvals_required` on the policy (`0` = + all approvers). A runtime approval pauses before the *next* step, so plan-only runs never pause. + Approvals match on the policy's id: keep `resource_name` a slug, don't set a different `id`, and + never rename a policy that uses APPROVAL_REQUIRED (the id stays, the match breaks). +7. **Config rules** must be `stackguardian/json` + `operation_type = "get_value"` + a `key_path` + rooted at a workflow field (`TerraformConfig.managedTerraformState`, `Tags`, `WfType`, + `DeploymentPlatformConfig.*.config.integrationId`, …). Any other `required_provider` + (`sg_workflow`, `sg_stack`, `kubernetes`, none) ends in "Tirith run errored" → `on_fail` in the + terraform step — it **blocks every Terraform/OpenTofu run in scope**. `sg_workflow`/`sg_stack` + with a workflow `key_path` are also picked up at config time and always fail there (Tirith has no + `sg_stack` provider; `sg_workflow` ignores `key_path`): with `FAIL`, every workflow create/update gets 403. +8. **OPA** needs `additional_config = jsonencode({ opaQuery = "." })` (no `data.` + prefix) — without it every Rego policy passes. The provider cannot read it back: the first apply + ends in *inconsistent result* (tainted); `terraform untaint`, then use + `lifecycle { ignore_changes = all }` and change the policy only with `-replace`. + **Not** `ignore_changes = [policies_config]`: any later update then re-sends the rules from state + and wipes `opaQuery` server-side (live). +9. **Only three top-level keys** in a Tirith document (`meta`, `evaluators`, `eval_expression`), + integers rather than floats, `jsonencode` for `data`, `schema_type = "TIRITH_JSON"` for inline Tirith. +10. **A policy in scope also blocks Terraform:** a failing config rule makes `terraform apply` of + the affected `stackguardian_workflow_*` resources fail with 403. + +## Quick reference + +| Need | Use | +|---|---| +| Block a resource setting | `terraform_plan` / `attribute` + `NotContainedIn` / `NotEquals`, tolerance 1 | +| Required tags | `attribute` on `tags` (or `tags_all`) + `Contains ""` (key) or `Contains { K = "v" }` (pair) | +| Restrict region | `provider_config`, `attribute = "region"`, `ContainedIn [...]` — one evaluator per registry (`registry.terraform.io/hashicorp/aws` for Terraform, `registry.opentofu.org/hashicorp/aws` for OpenTofu), tolerance 2, joined with `&&`; only literal regions are checked | +| Count cap | `count` + `LessThanEqualTo N` | +| Monthly cost cap | `stackguardian/infracost`, `total_monthly_cost`, `resource_type = ["*"]`, `LessThanEqualTo 500` | +| Approval on deletes | `action`, type `"*"`, `NotContainedIn ["delete"]`, `on_fail = "APPROVAL_REQUIRED"` | +| Config guardrail | `stackguardian/json`, `get_value`, `key_path`; e.g. managed state = `is_custom \|\| uses_managed_state` (tolerance 1 on the managed key — see `examples/config_rules.tf`) | +| Scope an existing group/workflow | a literal/local path (`local.net = "platform/networking"` → `"/wfgrps/${local.net}"`), with `data "stackguardian_workflow_group" { id = local.net }` as a plan-time existence check. Don't use `data..id` in the path: it is only the leaf for nested groups (live) | +| Marketplace rule | `policy_vcs_config = { use_marketplace_template = true, policy_template_id = "//:" }` | +| Git Tirith | `SG_POLICY_FRAMEWORK` + git kind; the file must be `/tirith.json`; runtime only | +| Warn-only rollout / disable | `on_fail = "WARN"` / `skip = true` (there is no active flag) | +| Import | `terraform import stackguardian_policy.x ` | + +## Common mistakes + +| Mistake | What happens | +|---|---| +| `schema_type = "TIRITH_FORMAT"` / other invented enums | Apply error: `… is not a valid …` | +| Org-wide by listing groups | Misses new groups; use `["*"]` | +| `eval_expression = "a and b"` | Becomes an undefined symbol → **silent pass** | +| Ids `acl-ok` and `acl-ok-2` | Prefix collision → `final_result = -1`, treated as a pass: the longer id's evaluator is silently ignored | +| `error_tolerance = 0` on a type-specific rule, group-wide | Every plan without that type FAILS | +| `on_fail = "APPROVAL_REQUIRED"` without approvers | The run pauses and anyone with approve permission can release it | +| `required_provider = "stackguardian/sg_workflow"` (or `sg_stack`, `kubernetes`) | FAILs the terraform step of every run in scope; with a workflow `key_path` also 403s every workflow create/update (rule 7) | +| Required tag as `terraform_resource_attribute = "tags.CostCenter"` with tolerance 2, or with type `"*"` | A missing key is "attribute not found" (severity 2): tolerance 2 skips it, so the rule never fails; with `"*"` and tolerance 1 every untaggable resource fails. Use one type per evaluator with tolerance 1, or `tags` + `Contains "CostCenter"` | +| `policies_config { ... }` block, or a raw HCL object as `data` | Syntax / type error: `policies_config = [{ ... }]`, `data = jsonencode({ ... })` | +| `stackguardian_policy { data = jsonencode({...}) }` (pre-1.x form, still in the provider's onboarding guide asset) | Unsupported argument; use the typed attributes | +| `severity`, a stage (`PRE_PLAN`, `PRE_APPLY`), per-rule approvers, nested `all_of`/`any_of` | Not part of a policy: severity/stage keys are ignored, nested shapes are not read. Enforcement is only `on_fail`/`on_pass`; approvers are policy-level | +| `{{ var.x }}` placeholders in the document | Never filled; the plan step crashes (even with `on_fail = "WARN"`) | +| OPA `opaQuery = "data.main.allow"` or `main.deny` | KeyError / non-boolean → on_fail | +| `policy_template_id = "/stackguardian/x:latest"` | Resolved per run; pin a number | +| `policy_type = "FILTER.INSIGHT"` | Provider always writes GENERAL → inconsistent result | +| `policies_config = []` (a policy without rules) | Inconsistent result, tainted (live) — a policy needs at least one rule; disable with `skip = true` | +| `resource_name` of a policy that already exists | `409 Policy ID not unique` (from code) — `terraform import` it instead | +| Floats like `value = 99.5` in the document | The create stores raw floats and can 409 (unverified); use integers | + +## References + +- `references/tirith.md` — Tirith 1.0.5: providers, operation types, conditions, tolerance and skip + semantics (incl. the skip-erasure bug), `eval_expression`, config-time key paths, verified recipes. +- `references/sources-and-lifecycle.md` — inline / git / OPA / marketplace sources, scope + resolution, approvals, provider round-trip behaviour, import, destroy, error catalog. +- `references/schema.md` — exact `stackguardian_policy` schema (provider 1.12.3). +- `examples/*.tf` — validated with `terraform`/`tofu validate`, applied live, and every Tirith + document executed against Tirith 1.0.5 with passing and failing sample plans (`examples/README.md`). diff --git a/skills/sg-writing-stackguardian-policy-terraform/examples/README.md b/skills/sg-writing-stackguardian-policy-terraform/examples/README.md new file mode 100644 index 0000000..389f8db --- /dev/null +++ b/skills/sg-writing-stackguardian-policy-terraform/examples/README.md @@ -0,0 +1,23 @@ +# Examples + +Every file pairs with `versions.tf` as a root module and passes `terraform validate` and +`tofu validate` (provider 1.12.3). All were also applied against a real org with names prefixed +and `enforced_on` pointed at an empty scope, re-planned with no changes (the OPA policy after the +documented untaint), and destroyed. Every +inline Tirith document was extracted from the Terraform plan and executed on Tirith 1.0.5 with +passing, failing and skip inputs. + +| File | Rules | Where it runs | +|---|---|---| +| `no_public_s3.tf` | S3 ACL deny-list + public-access-block check; group + single-workflow scope | runtime (plan) | +| `required_tags.tf` | CostCenter key and Environment=production on EC2/RDS | runtime (plan) | +| `network_guardrails.tf` | no 0.0.0.0/0 ingress, EU regions via `provider_config`, instance-type allow-list | runtime (plan) | +| `approvals.tf` | approval on any delete/replace, approval when monthly cost > 500; org-wide `["*"]`, shared approvers | runtime (plan / Infracost) | +| `config_rules.tf` | managed state required (warn), approved connectors + private runners (fail), pinned Terraform version (warn) | config time (workflow create/update, run build) | +| `opa_and_marketplace.tf` | Rego from private GitHub with `opaQuery` (+ the untaint / `ignore_changes = all` handling), connector scope; marketplace checkov bundle warn-only | runtime | + +The `terraform_version` rule in `config_rules.tf` was added later: it passes both validators and its +document was run on Tirith 1.0.5 with passing and failing workflow payloads, but it was not applied +live. + +Replace the placeholder ids (`acme`, connector names, repo URLs, approver emails) with yours. diff --git a/skills/sg-writing-stackguardian-policy-terraform/examples/approvals.tf b/skills/sg-writing-stackguardian-policy-terraform/examples/approvals.tf new file mode 100644 index 0000000..9f79f0e --- /dev/null +++ b/skills/sg-writing-stackguardian-policy-terraform/examples/approvals.tf @@ -0,0 +1,63 @@ +# Two approval gates in one policy, org-wide: plans that delete anything, and plans whose +# estimated monthly cost exceeds $500. Approvers are policy-level and shared by both rules. +# Runtime approvals pause before apply; plan-only runs never pause. + +locals { + inline = { + use_marketplace_template = false + custom_source = { source_config_kind = "SG_POLICY_FRAMEWORK", source_config_dest_kind = "INLINE" } + } +} + +resource "stackguardian_policy" "change_approvals" { + resource_name = "change-approvals" # slug: runtime approvals match approvers on this name + policy_type = "GENERAL" + description = "Human approval for deletions and expensive plans" + tags = ["governance"] + enforced_on = ["*"] # whole organization — on its own, never combined + + approvers = ["finops@acme.com", "cto@acme.com"] # lowercase emails or SSO groups + number_of_approvals_required = 1 # 0 would mean ALL of them + + policies_config = [ + { + name = "approve_deletes" + skip = false + on_fail = "APPROVAL_REQUIRED" + on_pass = "PASS" + policy_vcs_config = local.inline + policy_input_data = { + schema_type = "TIRITH_JSON" + data = jsonencode({ + meta = { version = "v1", required_provider = "stackguardian/terraform_plan" } + evaluators = [{ + id = "no_deletes" + provider_args = { operation_type = "action", terraform_resource_type = "*" } + # plan actions are create/update/delete/no-op/read; a replace yields delete + create + condition = { type = "NotContainedIn", value = ["delete"], error_tolerance = 1 } + }] + eval_expression = "no_deletes" + }) + } + }, + { + name = "approve_cost_over_500" + skip = false + on_fail = "APPROVAL_REQUIRED" + on_pass = "PASS" + policy_vcs_config = local.inline + policy_input_data = { + schema_type = "TIRITH_JSON" + data = jsonencode({ + meta = { version = "v1", required_provider = "stackguardian/infracost" } + evaluators = [{ + id = "monthly_cost_ok" + provider_args = { operation_type = "total_monthly_cost", resource_type = ["*"] } + condition = { type = "LessThanEqualTo", value = 500 } # integers only + }] + eval_expression = "monthly_cost_ok" + }) + } + }, + ] +} diff --git a/skills/sg-writing-stackguardian-policy-terraform/examples/config_rules.tf b/skills/sg-writing-stackguardian-policy-terraform/examples/config_rules.tf new file mode 100644 index 0000000..0405c29 --- /dev/null +++ b/skills/sg-writing-stackguardian-policy-terraform/examples/config_rules.tf @@ -0,0 +1,109 @@ +# Config-time rules (stackguardian/json): evaluated against the WORKFLOW DEFINITION when a workflow +# in scope is created or updated (terraform apply of stackguardian_workflow_* gets a 403 on FAIL) +# and again when each run is built. + +resource "stackguardian_workflow_group" "platform" { + id = "platform" + resource_name = "platform" +} + +locals { + inline = { + use_marketplace_template = false + custom_source = { source_config_kind = "SG_POLICY_FRAMEWORK", source_config_dest_kind = "INLINE" } + } +} + +resource "stackguardian_policy" "workflow_standards" { + resource_name = "workflow-standards" + policy_type = "GENERAL" + description = "Managed state, approved connectors, private runners, pinned Terraform version" + tags = ["platform"] + enforced_on = ["/wfgrps/${stackguardian_workflow_group.platform.id}"] + approvers = [] + number_of_approvals_required = 0 + + policies_config = [ + { + name = "managed_state" + skip = false + on_fail = "WARN" # roll out as a warning first + on_pass = "PASS" + policy_vcs_config = local.inline + policy_input_data = { + schema_type = "TIRITH_JSON" + data = jsonencode({ + meta = { version = "v1", required_provider = "stackguardian/json" } + evaluators = [ + { + id = "is_custom" + provider_args = { operation_type = "get_value", key_path = "WfType" } + condition = { type = "Equals", value = "CUSTOM", error_tolerance = 2 } + }, + { + id = "uses_managed_state" + provider_args = { operation_type = "get_value", key_path = "TerraformConfig.managedTerraformState" } + # tolerance 1: a TERRAFORM/OPENTOFU workflow that omits the key (server default = unmanaged) fails too + condition = { type = "Equals", value = true, error_tolerance = 1 } + }, + ] + eval_expression = "is_custom || uses_managed_state" # CUSTOM workflows have no Terraform state + }) + } + }, + { + name = "approved_connectors" + skip = false + on_fail = "FAIL" + on_pass = "PASS" + policy_vcs_config = local.inline + policy_input_data = { + schema_type = "TIRITH_JSON" + data = jsonencode({ + meta = { version = "v1", required_provider = "stackguardian/json" } + evaluators = [ + { + id = "known_connector" + provider_args = { operation_type = "get_value", key_path = "DeploymentPlatformConfig.*.config.integrationId" } + condition = { type = "ContainedIn", value = ["/integrations/aws-prod-oidc", "/integrations/aws-shared-rbac"], error_tolerance = 2 } + }, + { + id = "private_runner" + provider_args = { operation_type = "get_value", key_path = "RunnerConstraints.type" } + # tolerance 1: an omitted runner type (server default = shared) fails too + condition = { type = "Equals", value = "private", error_tolerance = 1 } + }, + ] + eval_expression = "known_connector && private_runner" + }) + } + }, + { + name = "terraform_version" + skip = false + on_fail = "WARN" + on_pass = "PASS" + policy_vcs_config = local.inline + policy_input_data = { + schema_type = "TIRITH_JSON" + data = jsonencode({ + meta = { version = "v1", required_provider = "stackguardian/json" } + evaluators = [ + { + id = "is_custom" + provider_args = { operation_type = "get_value", key_path = "WfType" } + condition = { type = "Equals", value = "CUSTOM", error_tolerance = 2 } + }, + { + id = "tf_version_ok" + provider_args = { operation_type = "get_value", key_path = "TerraformConfig.terraformVersion" } + # HCL "\\." is a literal dot in the regex; tolerance 1: a workflow without a version fails + condition = { type = "RegexMatch", value = "^1\\.(5|9)\\.", error_tolerance = 1 } + }, + ] + eval_expression = "is_custom || tf_version_ok" + }) + } + }, + ] +} diff --git a/skills/sg-writing-stackguardian-policy-terraform/examples/network_guardrails.tf b/skills/sg-writing-stackguardian-policy-terraform/examples/network_guardrails.tf new file mode 100644 index 0000000..d653a35 --- /dev/null +++ b/skills/sg-writing-stackguardian-policy-terraform/examples/network_guardrails.tf @@ -0,0 +1,64 @@ +# Runtime rules: no world-open ingress (IPv4/IPv6), literal EU provider regions only (Terraform and +# OpenTofu registries), approved instance types. + +resource "stackguardian_workflow_group" "apps" { + id = "apps" + resource_name = "apps" +} + +resource "stackguardian_policy" "network_guardrails" { + resource_name = "network-guardrails" + policy_type = "GENERAL" + description = "Ingress, region and instance-type guardrails" + tags = ["security"] + enforced_on = ["/wfgrps/${stackguardian_workflow_group.apps.id}"] + approvers = [] + number_of_approvals_required = 0 + + policies_config = [{ + name = "network_guardrails" + skip = false + on_fail = "FAIL" + on_pass = "PASS" + policy_vcs_config = { + use_marketplace_template = false + custom_source = { source_config_kind = "SG_POLICY_FRAMEWORK", source_config_dest_kind = "INLINE" } + } + policy_input_data = { + schema_type = "TIRITH_JSON" + data = jsonencode({ + meta = { version = "v1", required_provider = "stackguardian/terraform_plan" } + evaluators = [ + { + id = "no_open_ipv4_ingress" + provider_args = { operation_type = "attribute", terraform_resource_type = "aws_vpc_security_group_ingress_rule", terraform_resource_attribute = "cidr_ipv4" } + # tolerance 1: a cidr unknown at plan time fails (tolerance 2 would let skips erase violations) + condition = { type = "NotEquals", value = "0.0.0.0/0", error_tolerance = 1 } + }, + { + id = "no_open_ipv6_ingress" + provider_args = { operation_type = "attribute", terraform_resource_type = "aws_vpc_security_group_ingress_rule", terraform_resource_attribute = "cidr_ipv6" } + condition = { type = "NotEquals", value = "::/0", error_tolerance = 1 } + }, + { + id = "eu_region_terraform" + provider_args = { operation_type = "provider_config", terraform_provider_full_name = "registry.terraform.io/hashicorp/aws", attribute = "region" } + # only a literal region in the first matching provider block resolves; tolerance 2 skips the rest + condition = { type = "ContainedIn", value = ["eu-central-1", "eu-west-1"], error_tolerance = 2 } + }, + { + id = "eu_region_opentofu" + provider_args = { operation_type = "provider_config", terraform_provider_full_name = "registry.opentofu.org/hashicorp/aws", attribute = "region" } + condition = { type = "ContainedIn", value = ["eu-central-1", "eu-west-1"], error_tolerance = 2 } + }, + { + id = "approved_instance_type" + provider_args = { operation_type = "attribute", terraform_resource_type = "aws_instance", terraform_resource_attribute = "instance_type" } + condition = { type = "ContainedIn", value = ["t3.micro", "t3.small", "m6i.large"], error_tolerance = 1 } + }, + ] + eval_expression = "no_open_ipv4_ingress && no_open_ipv6_ingress && eu_region_terraform && eu_region_opentofu && approved_instance_type" + }) + } + }] +} diff --git a/skills/sg-writing-stackguardian-policy-terraform/examples/no_public_s3.tf b/skills/sg-writing-stackguardian-policy-terraform/examples/no_public_s3.tf new file mode 100644 index 0000000..405aef1 --- /dev/null +++ b/skills/sg-writing-stackguardian-policy-terraform/examples/no_public_s3.tf @@ -0,0 +1,68 @@ +# Runtime rule: deny public canned ACLs on aws_s3_bucket_acl and require block_public_acls on declared +# aws_s3_bucket_public_access_block resources. Not covered: buckets without a public access block +# (a direct_references recipe exists, with limits: references/tirith.md §8), ACL grants via +# access_control_policy, bucket policies. +# Scope: a workflow group (incl. nested groups) plus one extra workflow elsewhere. + +resource "stackguardian_workflow_group" "payments" { + id = "payments" + resource_name = "payments" +} + +locals { + inline_tirith = { + use_marketplace_template = false + custom_source = { source_config_kind = "SG_POLICY_FRAMEWORK", source_config_dest_kind = "INLINE" } + } +} + +resource "stackguardian_policy" "no_public_s3" { + resource_name = "no-public-s3" + policy_type = "GENERAL" + description = "Deny public canned S3 ACLs; public access blocks must block public ACLs" + tags = ["security", "s3"] + enforced_on = [ + "/wfgrps/${stackguardian_workflow_group.payments.id}", # group + everything below it + "/wfgrps/platform/wfs/shared-dns", # one workflow: /wfgrps//wfs/ + ] + approvers = [] + number_of_approvals_required = 0 + + policies_config = [{ + name = "s3_not_public" + skip = false + on_fail = "FAIL" + on_pass = "PASS" + policy_vcs_config = local.inline_tirith + policy_input_data = { + schema_type = "TIRITH_JSON" + data = jsonencode({ + meta = { version = "v1", required_provider = "stackguardian/terraform_plan" } + evaluators = [ + { + id = "acl_not_public" + description = "aws_s3_bucket_acl.acl must not grant public access" + provider_args = { + operation_type = "attribute" + terraform_resource_type = "aws_s3_bucket_acl" + terraform_resource_attribute = "acl" + } + # tolerance 1: plans without aws_s3_bucket_acl are skipped, not failed + condition = { type = "NotContainedIn", value = ["public-read", "public-read-write", "authenticated-read"], error_tolerance = 1 } + }, + { + id = "public_access_blocked" + description = "public access blocks must block public ACLs" + provider_args = { + operation_type = "attribute" + terraform_resource_type = "aws_s3_bucket_public_access_block" + terraform_resource_attribute = "block_public_acls" + } + condition = { type = "Equals", value = true, error_tolerance = 1 } + }, + ] + eval_expression = "acl_not_public && public_access_blocked" # a skipped evaluator drops out of the expression + }) + } + }] +} diff --git a/skills/sg-writing-stackguardian-policy-terraform/examples/opa_and_marketplace.tf b/skills/sg-writing-stackguardian-policy-terraform/examples/opa_and_marketplace.tf new file mode 100644 index 0000000..037d946 --- /dev/null +++ b/skills/sg-writing-stackguardian-policy-terraform/examples/opa_and_marketplace.tf @@ -0,0 +1,70 @@ +# Rego from a private GitHub repo, scoped to every workflow deploying through one connector, +# plus a StackGuardian marketplace policy template org-wide as warn-only. + +data "stackguardian_connector" "github" { + id = "github_com" +} + +resource "stackguardian_policy" "opa_guardrails" { + resource_name = "opa-guardrails" + policy_type = "GENERAL" + description = "Rego guardrails for production AWS" + tags = ["opa"] + enforced_on = ["/integrations/aws-prod-oidc"] # workflows whose deployment_platform_config uses it + approvers = [] + number_of_approvals_required = 0 + + policies_config = [{ + name = "rego_allow" + skip = false + on_fail = "FAIL" + on_pass = "PASS" + policy_vcs_config = { + use_marketplace_template = false + custom_source = { + source_config_kind = "OPA_REGO" + source_config_dest_kind = "GITHUB_COM" # OPA is never INLINE + config = { + repo = "https://github.com/acme/opa-policies" + ref = "main" + working_dir = "terraform" # directory loaded with `opa eval --data` + is_private = true + auth = "/integrations/${data.stackguardian_connector.github.id}" + } + # package terraform.guardrails + `default allow = false` + allow rules + # (OPA 0.55: Rego v0 with `import future.keywords.if`; `import rego.v1` does not parse) + additional_config = jsonencode({ opaQuery = "terraform.guardrails.allow" }) # no "data." prefix + } + } + }] + + # The provider cannot read additional_config back: the first apply reports + # "Provider produced inconsistent result" although the policy is stored correctly. + # Then: terraform untaint stackguardian_policy.opa_guardrails + # ignore_changes = all (NOT [policies_config]): any update would re-send the rules from state + # without opaQuery and silently disable the Rego check. Change this policy with -replace. + lifecycle { + ignore_changes = all + } +} + +resource "stackguardian_policy" "aws_best_practices" { + resource_name = "aws-best-practices" + policy_type = "GENERAL" + description = "StackGuardian AWS best practices (checkov), report only" + tags = ["aws"] + enforced_on = ["*"] + approvers = [] + number_of_approvals_required = 0 + + policies_config = [{ + name = "aws_best_practices_all" + skip = false + on_fail = "WARN" + on_pass = "PASS" + policy_vcs_config = { + use_marketplace_template = true + policy_template_id = "/stackguardian/aws-best-practices-all:2" # numeric revision; not validated until a run + } + }] +} diff --git a/skills/sg-writing-stackguardian-policy-terraform/examples/required_tags.tf b/skills/sg-writing-stackguardian-policy-terraform/examples/required_tags.tf new file mode 100644 index 0000000..16e1487 --- /dev/null +++ b/skills/sg-writing-stackguardian-policy-terraform/examples/required_tags.tf @@ -0,0 +1,61 @@ +# Runtime rule: EC2 instances and RDS databases must carry a CostCenter tag and Environment=production. +# One evaluator per resource type and tag; tolerance 1 so plans without that type pass. + +resource "stackguardian_workflow_group" "prod" { + id = "prod" + resource_name = "prod" +} + +locals { + tag_attr = { operation_type = "attribute", terraform_resource_attribute = "tags" } # "tags_all" also counts default_tags +} + +resource "stackguardian_policy" "required_tags" { + resource_name = "required-tags" + policy_type = "GENERAL" + description = "Mandatory cost and environment tags" + tags = ["finops"] + enforced_on = ["/wfgrps/${stackguardian_workflow_group.prod.id}"] + approvers = [] + number_of_approvals_required = 0 + + policies_config = [{ + name = "mandatory_tags" + skip = false + on_fail = "FAIL" + on_pass = "PASS" + policy_vcs_config = { + use_marketplace_template = false + custom_source = { source_config_kind = "SG_POLICY_FRAMEWORK", source_config_dest_kind = "INLINE" } + } + policy_input_data = { + schema_type = "TIRITH_JSON" + data = jsonencode({ + meta = { version = "v1", required_provider = "stackguardian/terraform_plan" } + evaluators = [ + { + id = "ec2_cost_center" + provider_args = merge(local.tag_attr, { terraform_resource_type = "aws_instance" }) + condition = { type = "Contains", value = "CostCenter", error_tolerance = 1 } # tag key present + }, + { + id = "ec2_environment" + provider_args = merge(local.tag_attr, { terraform_resource_type = "aws_instance" }) + condition = { type = "Contains", value = { Environment = "production" }, error_tolerance = 1 } # key = value + }, + { + id = "rds_cost_center" + provider_args = merge(local.tag_attr, { terraform_resource_type = "aws_db_instance" }) + condition = { type = "Contains", value = "CostCenter", error_tolerance = 1 } + }, + { + id = "rds_environment" + provider_args = merge(local.tag_attr, { terraform_resource_type = "aws_db_instance" }) + condition = { type = "Contains", value = { Environment = "production" }, error_tolerance = 1 } + }, + ] + eval_expression = "ec2_cost_center && ec2_environment && rds_cost_center && rds_environment" + }) + } + }] +} diff --git a/skills/sg-writing-stackguardian-policy-terraform/examples/versions.tf b/skills/sg-writing-stackguardian-policy-terraform/examples/versions.tf new file mode 100644 index 0000000..e0b7bdd --- /dev/null +++ b/skills/sg-writing-stackguardian-policy-terraform/examples/versions.tf @@ -0,0 +1,17 @@ +terraform { + required_version = ">= 1.5" + + required_providers { + stackguardian = { + # Full address so the same code works under OpenTofu (its registry has no copy). + source = "registry.terraform.io/StackGuardian/stackguardian" + version = "~> 1.12.3" + } + } +} + +# Credentials from the environment: +# STACKGUARDIAN_API_KEY raw key (sgu_/sgo_...), no "apikey " prefix +# STACKGUARDIAN_ORG_NAME org slug +# STACKGUARDIAN_API_URI https://api.app.stackguardian.io (EU) or https://api.us.stackguardian.io (US) — there is no default +provider "stackguardian" {} diff --git a/skills/sg-writing-stackguardian-policy-terraform/references/schema.md b/skills/sg-writing-stackguardian-policy-terraform/references/schema.md new file mode 100644 index 0000000..5ae326e --- /dev/null +++ b/skills/sg-writing-stackguardian-policy-terraform/references/schema.md @@ -0,0 +1,57 @@ +# Provider 1.12.3 schema — `stackguardian_policy` + +Generated from `terraform providers schema -json` (StackGuardian/stackguardian 1.12.3). +`= {...}` / `= [{...}]` are nested **attributes** (`name = …`), never blocks. +`optional+computed` without state carry-over: omit it while changing anything else and it is +sent as `null` (cleared) — declare every top-level attribute. + +Accepted values (parsed at apply, not at validate): + +| Attribute | Values | +|---|---| +| `policy_type` | `GENERAL` (the provider cannot write `FILTER.INSIGHT`) | +| `policies_config[].on_fail`, `.on_pass` | `FAIL`, `WARN`, `PASS`, `APPROVAL_REQUIRED` — both required by the API | +| `policy_input_data.schema_type` | `TIRITH_JSON` (inline Tirith), `FORM_JSONSCHEMA` (marketplace template inputs), `RAW_JSON`, `NONE` | +| `custom_source.source_config_kind` | `SG_POLICY_FRAMEWORK` (Tirith), `OPA_REGO` | +| `custom_source.source_config_dest_kind` | `INLINE` (Tirith only), `GITHUB_COM`, `GITHUB_APP_CUSTOM`, `GITLAB_COM`, `BITBUCKET_ORG`, `AZURE_DEVOPS`, `GIT_OTHER` | +| `custom_source.additional_config` | JSON string, e.g. `jsonencode({ opaQuery = "pkg.allow" })` (not read back — see sources-and-lifecycle §1) | +| `policy_vcs_config.policy_template_id` | `//