diff --git a/.claude/skills/tirith-migrate/examples/azure-policy/allowed-locations/policy.json b/.claude/skills/tirith-migrate/examples/azure-policy/allowed-locations/policy.json index 65dd4e15..41b396bc 100644 --- a/.claude/skills/tirith-migrate/examples/azure-policy/allowed-locations/policy.json +++ b/.claude/skills/tirith-migrate/examples/azure-policy/allowed-locations/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "id": "e56962a6-4747-49cd-b67b-bf8b01975c4c", "name": "Allowed locations", "description": "Every resource that has a location is deployed in one of listOfAllowedLocations. Azure exempts location 'global'; keep it in the list.", diff --git a/.claude/skills/tirith-migrate/examples/azure-policy/allowed-resource-types/policy.json b/.claude/skills/tirith-migrate/examples/azure-policy/allowed-resource-types/policy.json index 94ecf7e0..eed6005b 100644 --- a/.claude/skills/tirith-migrate/examples/azure-policy/allowed-resource-types/policy.json +++ b/.claude/skills/tirith-migrate/examples/azure-policy/allowed-resource-types/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "id": "a08ec900-254a-4555-9bf5-e42af04b5c5c", "name": "Allowed resource types", "description": "Only the listed azurerm types appear in the plan. count over * with the allowed types excluded must be 0. Non-Azure resources (random_*, null_resource, data sources) count too; exclude them explicitly, Azure never sees them.", diff --git a/.claude/skills/tirith-migrate/examples/azure-policy/allowed-vm-skus/policy.json b/.claude/skills/tirith-migrate/examples/azure-policy/allowed-vm-skus/policy.json index dd41ce46..6f188e7f 100644 --- a/.claude/skills/tirith-migrate/examples/azure-policy/allowed-vm-skus/policy.json +++ b/.claude/skills/tirith-migrate/examples/azure-policy/allowed-vm-skus/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "id": "cccc23c7-8427-4f53-ad12-b6a63eb452b3", "name": "Allowed virtual machine size SKUs", "description": "Every virtual machine uses a size from listOfAllowedSKUs. sku.name is `size` on the linux and windows resources and `vm_size` on the legacy one.", diff --git a/.claude/skills/tirith-migrate/examples/azure-policy/not-allowed-resource-types/policy.json b/.claude/skills/tirith-migrate/examples/azure-policy/not-allowed-resource-types/policy.json index d28df568..0a3997fc 100644 --- a/.claude/skills/tirith-migrate/examples/azure-policy/not-allowed-resource-types/policy.json +++ b/.claude/skills/tirith-migrate/examples/azure-policy/not-allowed-resource-types/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "id": "6c112d4e-5bc7-47ae-a041-ea2d9dccd749", "name": "Not allowed resource types", "description": "No resource of a forbidden type is in the plan. Microsoft.Network/publicIPAddresses is azurerm_public_ip; Microsoft.Compute/virtualMachines is three azurerm types, one evaluator each.", diff --git a/.claude/skills/tirith-migrate/examples/azure-policy/nsg-rdp-from-internet/policy.json b/.claude/skills/tirith-migrate/examples/azure-policy/nsg-rdp-from-internet/policy.json index ba77d34d..f861a423 100644 --- a/.claude/skills/tirith-migrate/examples/azure-policy/nsg-rdp-from-internet/policy.json +++ b/.claude/skills/tirith-migrate/examples/azure-policy/nsg-rdp-from-internet/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "id": "e372f825-a257-4fb8-9175-797a8a8627d6", "name": "RDP access from the Internet should be blocked", "description": "Stricter than the source: Azure refuses a rule only when access, direction, port and source all match at once; Tirith cannot bind four tests to one rule, so any inbound allow rule from the Internet is refused whatever its port. Port ranges like 3000-4000 are not expressible.", diff --git a/.claude/skills/tirith-migrate/examples/azure-policy/require-tag/policy.json b/.claude/skills/tirith-migrate/examples/azure-policy/require-tag/policy.json index de3115b7..d807afcb 100644 --- a/.claude/skills/tirith-migrate/examples/azure-policy/require-tag/policy.json +++ b/.claude/skills/tirith-migrate/examples/azure-policy/require-tag/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "id": "871b6d14-10aa-478d-b590-94f262ecfa99", "name": "Require a tag on resources", "description": "Every resource that supports tags carries the tag named by tagName. Azure evaluates tags[''] exists; Tirith tests the key of the tags map.", diff --git a/.claude/skills/tirith-migrate/examples/azure-policy/storage-secure-transfer/arm/policy.json b/.claude/skills/tirith-migrate/examples/azure-policy/storage-secure-transfer/arm/policy.json index 8061da63..b628443f 100644 --- a/.claude/skills/tirith-migrate/examples/azure-policy/storage-secure-transfer/arm/policy.json +++ b/.claude/skills/tirith-migrate/examples/azure-policy/storage-secure-transfer/arm/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/json", + "provider": "stackguardian/json", "id": "404c3081-a854-4457-ae30-26a93ef643f9", "name": "Secure transfer to storage accounts should be enabled (ARM template)", "description": "The json provider reads the whole ARM template. The guard passes a template with no storage account; otherwise every storage account's supportsHttpsTrafficOnly must be true and an absent property fails, as Azure's `notEquals true` does.", diff --git a/.claude/skills/tirith-migrate/examples/azure-policy/storage-secure-transfer/policy.json b/.claude/skills/tirith-migrate/examples/azure-policy/storage-secure-transfer/policy.json index 9e176a68..56199460 100644 --- a/.claude/skills/tirith-migrate/examples/azure-policy/storage-secure-transfer/policy.json +++ b/.claude/skills/tirith-migrate/examples/azure-policy/storage-secure-transfer/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "id": "404c3081-a854-4457-ae30-26a93ef643f9", "name": "Secure transfer to storage accounts should be enabled", "description": "supportsHttpsTrafficOnly is true on every storage account. azurerm 4.x names it https_traffic_only_enabled, 3.x enable_https_traffic_only; one evaluator per name, OR-ed, each skipping when its name is absent.", diff --git a/.claude/skills/tirith-migrate/examples/sentinel/mandatory-tags/policy.json b/.claude/skills/tirith-migrate/examples/sentinel/mandatory-tags/policy.json index 65033f04..9a3111d0 100644 --- a/.claude/skills/tirith-migrate/examples/sentinel/mandatory-tags/policy.json +++ b/.claude/skills/tirith-migrate/examples/sentinel/mandatory-tags/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "name": "mandatory-tags", "description": "aws_instance and aws_s3_bucket carry Name, Owner and CostCenter tags", "enforcement": "hard-mandatory" diff --git a/.claude/skills/tirith-migrate/examples/sentinel/prevent-database-destroy/policy.json b/.claude/skills/tirith-migrate/examples/sentinel/prevent-database-destroy/policy.json index d074ac4a..935ddbea 100644 --- a/.claude/skills/tirith-migrate/examples/sentinel/prevent-database-destroy/policy.json +++ b/.claude/skills/tirith-migrate/examples/sentinel/prevent-database-destroy/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "name": "prevent-database-destroy", "description": "No aws_db_instance is deleted by this plan", "enforcement": "hard-mandatory" diff --git a/.claude/skills/tirith-migrate/examples/sentinel/restrict-instance-type/policy.json b/.claude/skills/tirith-migrate/examples/sentinel/restrict-instance-type/policy.json index aac062fc..46af307c 100644 --- a/.claude/skills/tirith-migrate/examples/sentinel/restrict-instance-type/policy.json +++ b/.claude/skills/tirith-migrate/examples/sentinel/restrict-instance-type/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "name": "restrict-instance-type", "description": "aws_instance.instance_type must be in the allow-list", "enforcement": "hard-mandatory" diff --git a/.claude/skills/tirith-migrate/examples/sentinel/restrict-ssh-ingress/policy.json b/.claude/skills/tirith-migrate/examples/sentinel/restrict-ssh-ingress/policy.json index bfdad998..9236b595 100644 --- a/.claude/skills/tirith-migrate/examples/sentinel/restrict-ssh-ingress/policy.json +++ b/.claude/skills/tirith-migrate/examples/sentinel/restrict-ssh-ingress/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "name": "restrict-ssh-ingress", "description": "No security group ingress block admits 0.0.0.0/0 (stricter than the Sentinel original, which only refused it on port 22)", "enforcement": "hard-mandatory" diff --git a/.claude/skills/tirith-policies/SKILL.md b/.claude/skills/tirith-policies/SKILL.md index b43368c8..ab58e84c 100644 --- a/.claude/skills/tirith-policies/SKILL.md +++ b/.claude/skills/tirith-policies/SKILL.md @@ -50,7 +50,7 @@ tirith -policy-path .tirith/policies -input-path should-fail.json --fail-on-erro Decide these four in order. Guessing any of them is the main source of silently broken policies. 1. **Document.** Terraform or OpenTofu plan, Kubernetes manifest, Infracost breakdown, or arbitrary - JSON or YAML. This fixes `meta.required_provider`. Five providers ship; there is **no + JSON or YAML. This fixes `meta.provider`. Five providers ship; there is **no CloudFormation provider**, a template is arbitrary JSON read by `stackguardian/json`. 2. **Operation.** Each provider exposes a closed set: `reference/schema.md`. 3. **Key naming the value.** It differs per provider, and a wrong key is *ignored, not rejected*, @@ -61,7 +61,7 @@ Decide these four in order. Guessing any of them is the main source of silently { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "name": "Every resource carries a costcenter tag" }, "evaluators": [{ diff --git a/.claude/skills/tirith-policies/examples/required-tags/policy.json b/.claude/skills/tirith-policies/examples/required-tags/policy.json index daa3485b..bc6ec37d 100644 --- a/.claude/skills/tirith-policies/examples/required-tags/policy.json +++ b/.claude/skills/tirith-policies/examples/required-tags/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "name": "Every resource carries a costcenter tag" }, "evaluators": [ diff --git a/.claude/skills/tirith-policies/reference/other-providers.md b/.claude/skills/tirith-policies/reference/other-providers.md index 8afb189c..732f8074 100644 --- a/.claude/skills/tirith-policies/reference/other-providers.md +++ b/.claude/skills/tirith-policies/reference/other-providers.md @@ -6,7 +6,7 @@ and the check does not measure what you think. ## Kubernetes -`"required_provider": "stackguardian/kubernetes"`, reading YAML or JSON manifests. Multi-document +`"provider": "stackguardian/kubernetes"`, reading YAML or JSON manifests. Multi-document YAML is supported. - `operation_type`: `attribute` @@ -33,7 +33,7 @@ one container has a probe, which is the wrong question. Test for the presence of ## Infracost -`"required_provider": "stackguardian/infracost"`, reading an `infracost breakdown --format json` +`"provider": "stackguardian/infracost"`, reading an `infracost breakdown --format json` document. - `operation_type`: `total_monthly_cost` or `total_hourly_cost` @@ -54,7 +54,7 @@ exceed the ceiling, and prefer `["*"]` unless you specifically need one type. ## JSON — anything else -`"required_provider": "stackguardian/json"` reads any JSON document: a Terraform state file, an API +`"provider": "stackguardian/json"` reads any JSON document: a Terraform state file, an API response, a CI configuration, a lockfile. - `operation_type`: `get_value` @@ -73,6 +73,6 @@ condition is applied to each. ## StackGuardian workflows -`"required_provider": "stackguardian/sg_workflow"` reads a workflow definition, naming the value +`"provider": "stackguardian/sg_workflow"` reads a workflow definition, naming the value with **`workflow_attribute`**, for rules about the pipeline itself rather than the infrastructure — for example that a Terraform workflow requires approval before apply. diff --git a/.claude/skills/tirith-policies/reference/schema.md b/.claude/skills/tirith-policies/reference/schema.md index 46947ef8..f577ced3 100644 --- a/.claude/skills/tirith-policies/reference/schema.md +++ b/.claude/skills/tirith-policies/reference/schema.md @@ -16,7 +16,7 @@ python -c "from tirith.core.evaluators import EVALUATORS_DICT; print(sorted(EVAL | Key | Required | Notes | | --- | --- | --- | | `meta.version` | yes | `"v1"` | -| `meta.required_provider` | yes | One of the providers below | +| `meta.provider` | yes | One of the providers below | | `meta.name` / `description` / `severity` / `tags` | no | Passed through to the result document | | `evaluators[]` | yes | Each needs `id`, `provider_args`, `condition` | | `eval_expression` | yes | Combines evaluator ids with `&&`, `\|\|`, `!`, parentheses | @@ -36,7 +36,7 @@ presence, and `!` in `eval_expression` for negation. ## Providers and operations -| `required_provider` | Reads | `operation_type` | +| `provider` | Reads | `operation_type` | | --- | --- | --- | | `stackguardian/terraform_plan` | `terraform show -json` output | `action`, `attribute`, `count`, `direct_dependencies`, `direct_references`, `provider_config`, `terraform_version` | | `stackguardian/kubernetes` | Kubernetes manifests (YAML or JSON) | `attribute` | diff --git a/.claude/skills/tirith-policies/reference/terraform-plan.md b/.claude/skills/tirith-policies/reference/terraform-plan.md index d4cf8f35..ff0d51b6 100644 --- a/.claude/skills/tirith-policies/reference/terraform-plan.md +++ b/.claude/skills/tirith-policies/reference/terraform-plan.md @@ -1,6 +1,6 @@ # The plan provider — OpenTofu and Terraform -`"required_provider": "stackguardian/terraform_plan"` reads the output of +`"provider": "stackguardian/terraform_plan"` reads the output of `tofu show -json tfplan` or `terraform show -json tfplan`. The provider is named for Terraform because it predates the fork, but it reads either tool's plan: both emit the same `resource_changes` structure, and nothing in the provider inspects which binary produced it. diff --git a/.claude/skills/tirith-policies/reference/variables.md b/.claude/skills/tirith-policies/reference/variables.md index 72db5a72..7b4f65d5 100644 --- a/.claude/skills/tirith-policies/reference/variables.md +++ b/.claude/skills/tirith-policies/reference/variables.md @@ -14,7 +14,7 @@ literal string, so the check quietly measures the wrong thing instead of failing ```json title="policy.json" { - "meta": {"version": "v1", "required_provider": "stackguardian/infracost"}, + "meta": {"version": "v1", "provider": "stackguardian/infracost"}, "evaluators": [{ "id": "within_budget", "provider_args": {"operation_type": "total_monthly_cost", "resource_type": ["*"]}, diff --git a/.claude/skills/tirith-standards/examples/org-standards/naming.json b/.claude/skills/tirith-standards/examples/org-standards/naming.json index daedf8f7..8b5692ff 100644 --- a/.claude/skills/tirith-standards/examples/org-standards/naming.json +++ b/.claude/skills/tirith-standards/examples/org-standards/naming.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "name": "S3 bucket names follow the acme-- convention", "description": "Guarded so a plan touching no bucket passes rather than erroring. Without the guard this policy cannot be green on an unrelated change: at the default tolerance an absent resource type is severity 1 and fails (exit 3), and at error_tolerance 1 it is skipped, which leaves final_result null and exits 1. Buckets using bucket_prefix arrive with a null name and will fail; scope the rule if the repository uses them." }, diff --git a/.claude/skills/tirith-standards/reference/standards.md b/.claude/skills/tirith-standards/reference/standards.md index 0cadc767..bf6c01c5 100644 --- a/.claude/skills/tirith-standards/reference/standards.md +++ b/.claude/skills/tirith-standards/reference/standards.md @@ -1,7 +1,7 @@ # The standards catalogue One row per standard. Every `provider_args` block below is -`"required_provider": "stackguardian/terraform_plan"` unless it says otherwise, and every +`"provider": "stackguardian/terraform_plan"` unless it says otherwise, and every `error_tolerance` goes **inside `condition`**. **Every entry scoped to a resource type needs the guard evaluator from `SKILL.md`**, not just the diff --git a/.cursor/rules/tirith-policies.mdc b/.cursor/rules/tirith-policies.mdc index 75a73dc9..cbc2f9ea 100644 --- a/.cursor/rules/tirith-policies.mdc +++ b/.cursor/rules/tirith-policies.mdc @@ -26,7 +26,7 @@ Five providers, and no CloudFormation provider: a CloudFormation template is arb by `stackguardian/json`. There is no `stackguardian/terraform_state` either; read a state file the same way, with `key_path`. -**Structure:** `meta` (`version`, `required_provider`, `name`) · `evaluators[]` (each with `id`, +**Structure:** `meta` (`version`, `provider`, `name`) · `evaluators[]` (each with `id`, `provider_args`, `condition`) · `eval_expression`. **`eval_expression` operators are `&&`, `||`, `!` and parentheses.** Not `and` / `or`: the engine diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 6a43440c..64f836cb 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -9,12 +9,14 @@ on: pull_request: paths: - 'documentation/**' + - 'tests/docs/**' - '.github/workflows/build_docs.yml' push: branches: - main paths: - 'documentation/**' + - 'tests/docs/**' permissions: contents: read @@ -41,3 +43,7 @@ jobs: - name: Build working-directory: documentation run: npm run build + + - name: Test browser provider metadata + working-directory: documentation + run: npm run test:provider-metadata diff --git a/README.md b/README.md index 2b7c163a..eea11e97 100644 --- a/README.md +++ b/README.md @@ -453,7 +453,7 @@ skipped. A job that treats every non-zero code alike reports an outage as a poli cannot tell a working gate from a broken one. One limit worth stating plainly: a *misconfigured* policy — an unsupported `condition.type`, an unknown -`required_provider` — comes back from the engine as a failed check and exits `3`. The result message +`provider` — comes back from the engine as a failed check and exits `3`. The result message names the fault, but the exit code does not, so a job branching on the code sees a violation. It fails closed, which is the safe direction. `tirith lint` catches this class before the policy ever runs. @@ -494,6 +494,12 @@ discovery, the sticky pull-request comment, the check run and the exit codes for ## Example Tirith policies +> [!NOTE] +> New policies should use `meta.provider`. `meta.required_provider` remains accepted as a legacy +> alias, and `meta.provider` wins when both are present. Result JSON continues to expose the +> selected provider as `meta.required_provider` for output compatibility. + + [Examples using various providers](tests/providers) ### `error_tolerance`, and the third outcome @@ -529,7 +535,7 @@ VPC and EC2 instance policy ```json { "meta": { - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "version": "v1" }, "evaluators": [ @@ -565,7 +571,7 @@ Make sure that all `aws_s3_bucket` are referenced by `aws_s3_bucket_intelligent_ ```json { "meta": { - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "version": "v1" }, "evaluators": [ @@ -594,7 +600,7 @@ Make sure that all AWS ELBs are attached to security group (using Terraform plan { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -621,7 +627,7 @@ Policy: { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -860,7 +866,7 @@ Cost control policy ```json { "meta": { - "required_provider": "stackguardian/infracost", + "provider": "stackguardian/infracost", "version": "v1" }, "evaluators": [ @@ -886,7 +892,7 @@ Policy: { "meta": { "version": "v1", - "required_provider": "stackguardian/infracost" + "provider": "stackguardian/infracost" }, "evaluators": [ { @@ -1008,7 +1014,7 @@ JSON Output: ```json { "meta": { - "required_provider": "stackguardian/sg_workflow", + "provider": "stackguardian/sg_workflow", "version": "v1" }, "evaluators": [ @@ -1036,7 +1042,7 @@ Policy: { "meta": { "version": "v1", - "required_provider": "stackguardian/sg_workflow" + "provider": "stackguardian/sg_workflow" }, "evaluators": [ { @@ -1162,7 +1168,7 @@ Example Policy { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { @@ -1367,7 +1373,7 @@ Kubernetes (using Kubernetes provider) { "meta": { "version": "v1", - "required_provider": "stackguardian/kubernetes" + "provider": "stackguardian/kubernetes" }, "evaluators": [ { @@ -1447,7 +1453,7 @@ Create two files, one for input.json one for policy.json. { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { diff --git a/docs/lint.md b/docs/lint.md index fce8543b..af8696bd 100644 --- a/docs/lint.md +++ b/docs/lint.md @@ -12,7 +12,7 @@ registries rather than a copy of them: | Finding | Severity | Why it matters | | --- | --- | --- | | `condition.type` is not one of the thirteen evaluators | error | The engine fails the check with a message, exit 3, and `errors` stays empty; a job sees a violation | -| `meta.required_provider` missing or unknown | error | The engine looks for a provider named `core`, which does not exist | +| `meta.provider` missing or unknown | error | The engine looks for a provider named `core`, which does not exist | | `provider_args.operation_type` missing or not one the provider has | error | | | A required argument of the operation missing | error | | | A `provider_args` key the operation does not read | warning | Ignored rather than rejected, so the check reads nothing and passes | @@ -52,7 +52,7 @@ no path, both commands use `.tirith/policies` if it exists, otherwise the curren reorders a list, and the output parses back to an equal document. - Top level: `meta`, `evaluators`, `eval_expression`. A `$schema` key comes first if present. -- `meta`: `version`, `required_provider`, `id`, `name`, `description`, `severity`, `enforcement`, `tags`, `remediation`. +- `meta`: `version`, `provider`, `id`, `name`, `description`, `severity`, `enforcement`, `tags`, `remediation`. - Each evaluator: `id`, `description`, `provider_args`, `condition`. `operation_type` leads `provider_args`; `type`, `value`, `error_tolerance` is the order inside `condition`. - Keys not in these lists keep their relative order after the listed ones. - Whitespace is exactly Python's `json.dumps(indent=2, ensure_ascii=False)` plus one trailing newline, so a policy written by any tool that uses the standard library is already canonical. Non-ASCII stays as written. diff --git a/documentation/docs/tirith-policies/tirith-create-first-policy.md b/documentation/docs/tirith-policies/tirith-create-first-policy.md index f1339f3f..29d869a6 100644 --- a/documentation/docs/tirith-policies/tirith-create-first-policy.md +++ b/documentation/docs/tirith-policies/tirith-create-first-policy.md @@ -43,7 +43,7 @@ Here is the `policy.json` file: { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { diff --git a/documentation/docs/tirith-policies/tirith-policy-cookbook.md b/documentation/docs/tirith-policies/tirith-policy-cookbook.md index aac36e8f..5adf8618 100644 --- a/documentation/docs/tirith-policies/tirith-policy-cookbook.md +++ b/documentation/docs/tirith-policies/tirith-policy-cookbook.md @@ -21,7 +21,7 @@ Every `aws_instance` in a Terraform plan must use an instance type from an appro { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "id": "allowed-instance-types", "name": "Only approved EC2 instance types", "description": "Every aws_instance in the plan must use an instance type from the approved list.", @@ -108,7 +108,7 @@ With `terraform_resource_type` set to `"*"`, the check runs against every resour { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "id": "require-environment-tag", "name": "Every resource carries an Environment tag", "description": "Every resource in the plan must be tagged with Environment set to dev, staging, or prod." @@ -207,7 +207,7 @@ A public-ingress check. The attribute path `ingress.*.cidr_blocks` extracts the { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "id": "no-public-ingress", "name": "No security group ingress from 0.0.0.0/0", "description": "No ingress rule of any aws_security_group may allow traffic from 0.0.0.0/0.", @@ -293,7 +293,7 @@ Uses the `stackguardian/infracost` provider against an [Infracost](https://www.i { "meta": { "version": "v1", - "required_provider": "stackguardian/infracost", + "provider": "stackguardian/infracost", "id": "monthly-cost-ceiling", "name": "Monthly cost stays under the ceiling", "description": "The estimated total monthly cost of all resources must not exceed 500 USD." @@ -369,7 +369,7 @@ By default a value the provider cannot find fails the check. `error_tolerance` t { "meta": { "version": "v1", - "required_provider": "stackguardian/json", + "provider": "stackguardian/json", "id": "encryption-and-optional-logging", "name": "Encryption required, logging checked when configured", "description": "Encryption must be enabled. The logging level is validated only when the logging block exists." @@ -459,7 +459,7 @@ The same cost-ceiling policy, with the limit supplied at run time. A variable re { "meta": { "version": "v1", - "required_provider": "stackguardian/infracost", + "provider": "stackguardian/infracost", "id": "parameterized-cost-ceiling", "name": "Monthly cost stays under a configurable ceiling", "description": "The estimated total monthly cost must not exceed the ceiling supplied as a variable." diff --git a/documentation/docs/tirith-policies/tirith-policy-reference.md b/documentation/docs/tirith-policies/tirith-policy-reference.md index 6d271a3b..40135e5c 100644 --- a/documentation/docs/tirith-policies/tirith-policy-reference.md +++ b/documentation/docs/tirith-policies/tirith-policy-reference.md @@ -15,7 +15,7 @@ A Tirith policy is a single JSON document with exactly three top-level keys. It { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { @@ -50,7 +50,7 @@ If `meta`, `evaluators`, or `eval_expression` is missing, the run aborts before | Key | Required | Type | Default | Behavior | |---|---|---|---|---| -| `required_provider` | effectively yes | string | `"core"` | Selects the provider used by every evaluator in the policy. See [below](#metarequired_provider). | +| `provider` | effectively yes | string | `"core"` | Selects the provider used by every evaluator in the policy. See [below](#metaprovider). | | `version` | no | string | none | Not interpreted. Always echoed into the result `meta` (as `null` when absent). | | `id` | no | any (conventionally string) | none | Not interpreted. Echoed verbatim into the result `meta` only when present. | | `name` | no | any (conventionally string) | none | Same as `id`. | @@ -62,7 +62,7 @@ If `meta`, `evaluators`, or `eval_expression` is missing, the run aborts before An empty `meta` object (`"meta": {}`) is accepted; the policy then falls back to the default provider, which fails every check (see next section). -### `meta.required_provider` +### `meta.provider` The registered providers are: @@ -74,7 +74,9 @@ The registered providers are: Each provider defines its own `provider_args`; see the [provider documentation](../tirith-providers/overview.md). -When `required_provider` is absent it defaults to `"core"`, and no provider named `core` is registered. An unregistered provider name — the default included — is **not** a hard error: every evaluator in the policy simply receives no values and fails with the message `Could not find input value`. The final verdict is a failure (exit code `3` under `--fail-on-error`), which can be mistaken for a genuine policy violation. Always set `required_provider` explicitly. +When both `provider` and its legacy alias `required_provider` are absent, the provider defaults to `"core"`, and no provider named `core` is registered. An unregistered provider name — the default included — is **not** a hard error: every evaluator in the policy simply receives no values and fails with the message `Could not find input value`. The final verdict is a failure (exit code `3` under `--fail-on-error`), which can be mistaken for a genuine policy violation. Always set `provider` explicitly. + +For backward compatibility, `meta.required_provider` is still accepted as a legacy alias. If both keys are present, `meta.provider` takes precedence. The result document continues to report the selected provider as `meta.required_provider`, preserving the existing JSON output contract. ### `meta.enforcement` @@ -103,7 +105,7 @@ Each entry in the `evaluators` array is an object with these keys: | Key | Required | Type | Default | Behavior | |---|---|---|---|---| | `id` | yes | string | — | The name this check is referenced by in `eval_expression`. Missing `id` aborts the run (`ERROR`, exit `1`). | -| `provider_args` | yes | object | — | Arguments for the provider selected by `meta.required_provider`. Missing `provider_args` aborts the run (`ERROR`, exit `1`). | +| `provider_args` | yes | object | — | Arguments for the provider selected by `meta.provider`. Missing `provider_args` aborts the run (`ERROR`, exit `1`). | | `condition` | yes | object | — | The comparison applied to every value the provider extracts. Missing `condition` aborts the run (`ERROR`, exit `1`). | | `description` | no | string | none | Informational. Echoed into the result for this check (as `null` when absent). | diff --git a/documentation/docs/tirith-policies/tirith-policy-structure.md b/documentation/docs/tirith-policies/tirith-policy-structure.md index 8f9a2824..92e3cc79 100644 --- a/documentation/docs/tirith-policies/tirith-policy-structure.md +++ b/documentation/docs/tirith-policies/tirith-policy-structure.md @@ -22,7 +22,7 @@ Tirith policies are written in JSON format, following a structured approach that { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { diff --git a/documentation/docs/tirith-policies/tirith-policy-variables.md b/documentation/docs/tirith-policies/tirith-policy-variables.md index c37229f8..0904587c 100644 --- a/documentation/docs/tirith-policies/tirith-policy-variables.md +++ b/documentation/docs/tirith-policies/tirith-policy-variables.md @@ -25,7 +25,7 @@ literal string, so the check quietly measures the wrong thing instead of failing { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { diff --git a/documentation/docs/tirith-providers/infracost.md b/documentation/docs/tirith-providers/infracost.md index a3b694fc..74b48931 100644 --- a/documentation/docs/tirith-providers/infracost.md +++ b/documentation/docs/tirith-providers/infracost.md @@ -10,7 +10,7 @@ slug: infracost-provider/ --- ``` -required_provider: stackguardian/infracost +provider: stackguardian/infracost ``` Sums estimated costs from an Infracost cost breakdown, either for all resources or for a chosen set of resource types. @@ -59,7 +59,7 @@ Verified end-to-end against the test fixtures — the total monthly cost of the { "meta": { "version": "v1", - "required_provider": "stackguardian/infracost" + "provider": "stackguardian/infracost" }, "evaluators": [ { diff --git a/documentation/docs/tirith-providers/json.md b/documentation/docs/tirith-providers/json.md index 033b7509..501d02b3 100644 --- a/documentation/docs/tirith-providers/json.md +++ b/documentation/docs/tirith-providers/json.md @@ -10,7 +10,7 @@ slug: json-provider/ --- ``` -required_provider: stackguardian/json +provider: stackguardian/json ``` Extracts values from any JSON or YAML document by key path. Use this provider when no specialized provider exists for your input format. @@ -58,7 +58,7 @@ Verified end-to-end against the test fixtures: { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { @@ -119,7 +119,7 @@ The provider also works on YAML — this policy checks an Ansible playbook (a YA { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { diff --git a/documentation/docs/tirith-providers/kubernetes.md b/documentation/docs/tirith-providers/kubernetes.md index 739d32c9..b4e5badc 100644 --- a/documentation/docs/tirith-providers/kubernetes.md +++ b/documentation/docs/tirith-providers/kubernetes.md @@ -10,7 +10,7 @@ slug: kubernetes-provider/ --- ``` -required_provider: stackguardian/kubernetes +provider: stackguardian/kubernetes ``` Extracts attribute values from Kubernetes manifests of a chosen `kind` (Pod, Deployment, Service, ...). @@ -58,7 +58,7 @@ Verified end-to-end against the test fixtures — every container of every `Pod` { "meta": { "version": "v1", - "required_provider": "stackguardian/kubernetes" + "provider": "stackguardian/kubernetes" }, "evaluators": [ { diff --git a/documentation/docs/tirith-providers/overview.md b/documentation/docs/tirith-providers/overview.md index 67a674fb..4f9801d5 100644 --- a/documentation/docs/tirith-providers/overview.md +++ b/documentation/docs/tirith-providers/overview.md @@ -2,7 +2,7 @@ id: providers-overview title: Providers Overview sidebar_label: Overview -description: What a Tirith provider is, how required_provider selects one, how provider_args are passed, and the list of available providers. +description: What a Tirith provider is, how provider selects one, how provider_args are passed, and the list of available providers. keywords: - tirith site_name: Tirith @@ -13,13 +13,13 @@ A **provider** is the part of Tirith that knows how to read one specific kind of ## Selecting a provider -The provider is selected once for the whole policy with `meta.required_provider`: +The provider is selected once for the whole policy with `meta.provider`: ```json { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ ... ], "eval_expression": "..." @@ -28,7 +28,7 @@ The provider is selected once for the whole policy with `meta.required_provider` The value must be one of the exact strings below. If the string does not match any known provider, every evaluator in the policy fails with an error. -| `required_provider` | Summary | Expected input document | +| `provider` | Summary | Expected input document | |---|---|---| | [`stackguardian/terraform_plan`](terraform-plan.md) | Inspects resource changes, actions, counts, dependencies, references, provider configuration, and the Terraform version in a Terraform plan. | Terraform plan in JSON form (`terraform show -json `) | | [`stackguardian/infracost`](infracost.md) | Sums estimated monthly or hourly costs from an Infracost breakdown. | Infracost output (`infracost breakdown --format json`) | diff --git a/documentation/docs/tirith-providers/sg-workflow.md b/documentation/docs/tirith-providers/sg-workflow.md index f6403d80..a8fa08d6 100644 --- a/documentation/docs/tirith-providers/sg-workflow.md +++ b/documentation/docs/tirith-providers/sg-workflow.md @@ -10,7 +10,7 @@ slug: sg-workflow-provider/ --- ``` -required_provider: stackguardian/sg_workflow +provider: stackguardian/sg_workflow ``` Reads attributes of a StackGuardian workflow definition. @@ -73,7 +73,7 @@ Verified end-to-end against the test fixtures: { "meta": { "version": "v1", - "required_provider": "stackguardian/sg_workflow" + "provider": "stackguardian/sg_workflow" }, "evaluators": [ { diff --git a/documentation/docs/tirith-providers/terraform-plan.md b/documentation/docs/tirith-providers/terraform-plan.md index d214b87b..4bf34c16 100644 --- a/documentation/docs/tirith-providers/terraform-plan.md +++ b/documentation/docs/tirith-providers/terraform-plan.md @@ -10,7 +10,7 @@ slug: terraform-plan-provider/ --- ``` -required_provider: stackguardian/terraform_plan +provider: stackguardian/terraform_plan ``` Inspects a Terraform plan: attribute values of changed resources, the actions applied to them, resource counts, explicit dependencies, references between resources, provider configuration, and the Terraform version. @@ -67,7 +67,7 @@ Example (adapted from a test fixture; requires every resource in the plan to car { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -111,7 +111,7 @@ Example (adapted from a test fixture; fails when a virtual network would be dele { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -180,7 +180,7 @@ Example (verified against a test fixture; requires every EC2 instance to declare { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -253,7 +253,7 @@ Example (from a test fixture; every S3 bucket must have an intelligent-tiering c ```json { "meta": { - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "version": "v1" }, "evaluators": [ diff --git a/documentation/docs/tirith-reference/eval-expressions.md b/documentation/docs/tirith-reference/eval-expressions.md index 970376a1..d9400ded 100644 --- a/documentation/docs/tirith-reference/eval-expressions.md +++ b/documentation/docs/tirith-reference/eval-expressions.md @@ -13,7 +13,7 @@ A policy's top-level `eval_expression` is a boolean expression over the `id`s of ```json { - "meta": { "version": "v1", "required_provider": "stackguardian/json" }, + "meta": { "version": "v1", "provider": "stackguardian/json" }, "evaluators": [ { "id": "check_region", "provider_args": { "...": "..." }, "condition": { "...": "..." } }, { "id": "check_tags", "provider_args": { "...": "..." }, "condition": { "...": "..." } }, diff --git a/documentation/docs/tirith-reference/evaluators.md b/documentation/docs/tirith-reference/evaluators.md index 2d32e0d1..8fc5f7d1 100644 --- a/documentation/docs/tirith-reference/evaluators.md +++ b/documentation/docs/tirith-reference/evaluators.md @@ -358,7 +358,7 @@ Policy (`policy.json`), using the `stackguardian/json` provider: { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { diff --git a/documentation/docs/tirith-usage/cli-reference.md b/documentation/docs/tirith-usage/cli-reference.md index cae67099..5125818f 100644 --- a/documentation/docs/tirith-usage/cli-reference.md +++ b/documentation/docs/tirith-usage/cli-reference.md @@ -60,7 +60,7 @@ The policy file to evaluate — a JSON document with `meta`, `evaluators` and an See the [policy reference](../tirith-policies/tirith-policy-reference.md) for the schema, the [evaluators reference](../tirith-reference/evaluators.md) for the available condition types, and the [providers overview](../tirith-providers/overview.md) for what kinds of input each -`required_provider` reads. +`provider` reads. If the flag is missing, `tirith` prints an error to stderr and exits `1`. diff --git a/documentation/docs/tirith-usage/exit-codes.md b/documentation/docs/tirith-usage/exit-codes.md index c42ff18e..28c2cd9a 100644 --- a/documentation/docs/tirith-usage/exit-codes.md +++ b/documentation/docs/tirith-usage/exit-codes.md @@ -82,7 +82,7 @@ found nothing — checked precisely nothing, and reporting that as green is exac exists to prevent. It is not a violation either, so it is `1` rather than `3`. **One limit worth stating plainly:** a *misconfigured* policy — an unsupported `condition.type`, -an unknown `required_provider` — comes back from the engine as an ordinary failed check with no +an unknown `provider` — comes back from the engine as an ordinary failed check with no error attached, so it is indistinguishable from a real violation and exits `3`. It fails closed, which is the safe direction, but it will point at your infrastructure when the fault is in the policy. diff --git a/documentation/docs/tirith-usage/lint-and-fmt.md b/documentation/docs/tirith-usage/lint-and-fmt.md index e1fad8e1..4426703d 100644 --- a/documentation/docs/tirith-usage/lint-and-fmt.md +++ b/documentation/docs/tirith-usage/lint-and-fmt.md @@ -40,7 +40,7 @@ infrastructure violation: - a condition `type` that does not exist, against the closed list of [thirteen conditions](../tirith-reference/evaluators.md) - a `provider_args` key that belongs to a different provider than the one in - `meta.required_provider` + `meta.provider` - an evaluator that `eval_expression` never names, so it is computed and then discarded - `error_tolerance` placed on the evaluator instead of inside `condition`, where it is silently ignored and the check still fails @@ -135,7 +135,7 @@ Keys are ordered: | Object | Order | |---|---| | top level | `$schema`, `meta`, `evaluators`, `eval_expression` | -| `meta` | `version`, `required_provider`, `id`, `name`, `description`, `severity`, `enforcement`, `tags`, `remediation` | +| `meta` | `version`, `provider`, `id`, `name`, `description`, `severity`, `enforcement`, `tags`, `remediation` | | each evaluator | `id`, `description`, `provider_args`, `condition` | | `provider_args` | `operation_type` first, then the provider's own keys in their original order | | `condition` | `type`, `value`, `error_tolerance` | diff --git a/documentation/package.json b/documentation/package.json index fb08d2a9..0a65a58e 100644 --- a/documentation/package.json +++ b/documentation/package.json @@ -3,6 +3,7 @@ "version": "0.0.0", "private": true, "scripts": { + "test:provider-metadata": "node --test ../tests/docs/test_provider_metadata.mjs", "docusaurus": "docusaurus", "start": "docusaurus start", "build": "docusaurus build", diff --git a/documentation/src/data/agentBrief.js b/documentation/src/data/agentBrief.js index a704d206..9b47e981 100644 --- a/documentation/src/data/agentBrief.js +++ b/documentation/src/data/agentBrief.js @@ -5,4 +5,4 @@ * static/llms.txt and re-run documentation/scripts/generate-llms-full.py. */ -export const AGENT_BRIEF = "# Tirith\n\n> Tirith is an Apache-2.0 policy gate for infrastructure-as-code. It reads the plan a\n> pipeline already produces, evaluates it against declarative JSON policies committed in\n> the repository, and returns one verdict with an exit code the pipeline can gate on. It\n> runs locally or in any CI, on your own runner, with no account and no network call.\n\nPolicies are data, not code: a policy is a JSON file, so it is diffable and reviewable by\nanyone on the team. There is no rule DSL and no embedded language.\n\n## Minimal working example\n\n```bash\nterraform plan -out=tfplan -input=false\nterraform show -json tfplan > plan.json\ntirith -policy-path .tirith/policies -input-path plan.json --fail-on-error\n```\n\nWithout `--fail-on-error`, Tirith reports findings but does not fail the job.\n\n## Facts an answer about Tirith usually gets wrong\n\n- **Not on PyPI.** `pip install tirith` installs an unrelated project of the same name, and\n `pip install py-tirith` finds nothing. The package name in `setup.py` is `py-tirith` and it\n is not published. Install from git:\n `pip install \"git+https://github.com/StackGuardian/tirith.git@1.2.1\"`\n- **The interactive interface needs an extra**, and the same git URL:\n `pip install 'py-tirith[tui] @ git+https://github.com/StackGuardian/tirith.git'`, then\n `tirith ui`. Requires Python 3.9; Tirith itself supports 3.8 and newer.\n- **`tirith lint` and `tirith fmt` exist since 1.2.1.** They are not in 1.2.0. The CLI\n dispatches four subcommands:\n `lint`, `fmt`, `ui` and `platform`. Lint checks a policy's shape without a plan document; fmt\n rewrites it into the canonical layout. Both exit `3` on a finding, with no `--fail-on-error`.\n- **Exit codes are a contract, not a convention.** `0` passed, `3` a policy failed, `1`\n Tirith could not reach a verdict. `3` is deliberately not `1`: a caller has to be able to page\n the platform team on one and the change author on the other. (`2` is not a Tirith code at all:\n argparse exits `2` on a usage error, so a caller seeing it passed a bad argument.)\n- **`final_result: null` is not a pass.** It means every check was skipped, so the policy\n evaluated nothing, and it exits `1`. A check that could not run is reported as unevaluated\n rather than as success.\n- **Local mode makes no network call.** That is a published governance commitment. Only\n `tirith platform check` talks to a network, and it is optional.\n- **Five providers ship**, named in `meta.required_provider`: `stackguardian/terraform_plan`,\n `stackguardian/kubernetes`, `stackguardian/infracost`, `stackguardian/json`,\n `stackguardian/sg_workflow`. There is no CloudFormation provider; a CloudFormation template\n is read as a JSON document by the `json` provider.\n- **Thirteen condition types ship**: Equals, NotEquals, GreaterThan, GreaterThanEqualTo,\n LessThan, LessThanEqualTo, Contains, NotContains, ContainedIn, NotContainedIn, IsEmpty,\n IsNotEmpty, RegexMatch.\n- **The argument key differs per provider, and a wrong key is ignored rather than rejected.**\n `terraform_plan` reads `terraform_resource_attribute` alongside `terraform_resource_type`;\n `kubernetes` reads `attribute_path` alongside `kubernetes_kind`; `json` reads `key_path`;\n `sg_workflow` reads `workflow_attribute`. Give an evaluator the wrong key and it reads\n nothing and the check passes. This is the highest-cost mistake in the schema.\n- **`error_tolerance` belongs inside the `condition` object**, not on the evaluator. Placed on\n the evaluator it is silently ignored and the check still fails.\n- **`error_tolerance` is a threshold, not a switch.** A problem of severity N fails the check\n when `N > error_tolerance` and is skipped otherwise; the default is `0`. The plan provider\n raises severity `0` for a resource being deleted, `1` for a resource type absent from the\n plan, and `2` for an attribute absent from the resource. So the default skips deletes and\n fails everything else.\n- **A policy scoped to one resource type has no green outcome on a plan that does not touch\n that type.** At the default tolerance the absent type is severity `1`, the check fails, exit\n `3`. At `error_tolerance: 1` it is skipped, so every check is skipped, `final_result` is\n `null`, exit `1`. Guard it instead: `count` returns `0` for an absent type rather than\n raising severity `1`, so an evaluator of\n `{\"operation_type\": \"count\", \"terraform_resource_type\": T}` with `Equals: 0`, combined as\n `eval_expression: \"not_applicable || the_rule\"`, passes cleanly on unrelated plans and still\n fails on real violations.\n- **The pre-commit hooks are published.** `.pre-commit-hooks.yaml` carries `tirith-lint` and\n `tirith-fmt`, both run on the policy files a commit touches. Pin `rev` to `1.2.1`, the first\n tag that publishes them.\n\n## Fetching this documentation\n\nEvery page is also served as plain markdown at its route plus `.md`, so there is no need to\nparse the rendered HTML. For example\nhttps://stackguardian.github.io/tirith/docs/tirith-usage/exit-codes.md\n\nThe whole documentation set as one file, in sidebar order:\nhttps://stackguardian.github.io/tirith/llms-full.txt\n\n## Working in a repository that uses Tirith\n\nIf you are a coding agent about to write a policy, install the skill pack first. It is the\nclosed vocabulary: thirteen condition types, the argument key each provider reads, and the\ninstruction to run a policy against a document that should fail before calling it done.\n\n```\ncurl -fsSL https://stackguardian.github.io/tirith/skill.sh | sh\n```\n\nThree skills: tirith-policies (eleven markdown files and a worked example), tirith-standards\n(generating a policy set from an existing Terraform or OpenTofu repository, including the guard\nabove) and tirith-migrate (Sentinel translation). Add --cursor for the Cursor rule, --global to\ninstall into the home directory instead of the repository. The script installs no\npackage, changes no PATH and executes nothing after the download; its source is\ndocumentation/static/skill.sh in the repository.\n\n## Start here\n\n- [Getting started](https://stackguardian.github.io/tirith/docs/getting-started-with-tirith/): what Tirith is and the shortest path to a first verdict.\n- [Quick installation](https://stackguardian.github.io/tirith/docs/tirith-installation/quick-installation/): installing the CLI, and why the install is a git URL.\n- [Learn](https://stackguardian.github.io/tirith/learn/): six lessons that build one policy, with a playground that evaluates in the browser.\n- [Creating your first policy](https://stackguardian.github.io/tirith/docs/tirith-policies/tirith-create-first-policy/): a policy written and evaluated step by step.\n\n## Writing policies\n\n- [Policy reference](https://stackguardian.github.io/tirith/docs/tirith-policies/tirith-policy-reference/): field-by-field reference for the policy file format.\n- [Policy structure](https://stackguardian.github.io/tirith/docs/tirith-policies/tirith-policy-structure/): meta, evaluators and eval_expression, and how they combine.\n- [Evaluators and conditions](https://stackguardian.github.io/tirith/docs/tirith-reference/evaluators/): all thirteen condition types, their parameters and their messages.\n- [Evaluation expressions](https://stackguardian.github.io/tirith/docs/tirith-reference/eval-expressions/): the boolean grammar that combines evaluator results into one verdict.\n- [Policy conditions](https://stackguardian.github.io/tirith/docs/tirith-policies/tirith-policy-conditions/): condition types by the kind of value they compare.\n- [Error tolerance](https://stackguardian.github.io/tirith/docs/tirith-policies/tirith-policy-error-tolerance/): how a missing key is handled, and why a skip is not a pass.\n- [Policy variables](https://stackguardian.github.io/tirith/docs/tirith-policies/tirith-policy-variables/): dynamic values in policies.\n- [Policy cookbook](https://stackguardian.github.io/tirith/docs/tirith-policies/tirith-policy-cookbook/): complete runnable policies for common checks.\n- [Example policies](https://stackguardian.github.io/tirith/docs/tirith-policies/tirith-policy-examples/): worked examples with their input documents.\n\n## Providers\n\n- [Providers overview](https://stackguardian.github.io/tirith/docs/tirith-providers/providers-overview/): what a provider is and how required_provider selects one.\n- [Terraform plan provider](https://stackguardian.github.io/tirith/docs/tirith-providers/terraform-plan-provider/): operations over a plan document, including attribute, action and count.\n- [JSON provider](https://stackguardian.github.io/tirith/docs/tirith-providers/json-provider/): get_value over any JSON or YAML document, including another tool's output.\n- [Kubernetes provider](https://stackguardian.github.io/tirith/docs/tirith-providers/kubernetes-provider/): the attribute operation over manifests.\n- [Infracost provider](https://stackguardian.github.io/tirith/docs/tirith-providers/infracost-provider/): cost policies over an Infracost breakdown.\n- [SG Workflow provider](https://stackguardian.github.io/tirith/docs/tirith-providers/sg-workflow-provider/): StackGuardian workflow documents.\n\n## Running it\n\n- [CLI reference](https://stackguardian.github.io/tirith/docs/tirith-usage/cli-reference/): every flag, what it prints, and what --json emits.\n- [Lint and format](https://stackguardian.github.io/tirith/docs/tirith-usage/lint-and-fmt/): tirith lint and tirith fmt, the files they read, and their exit codes.\n- [Exit codes](https://stackguardian.github.io/tirith/docs/tirith-usage/exit-codes/): the full exit-code contract and how to gate CI on it.\n- [CI integration](https://stackguardian.github.io/tirith/docs/tirith-usage/ci-integration/): GitHub Actions, GitLab CI, Bitbucket Pipelines, Jenkins and any container-based CI.\n- [The interactive interface](https://stackguardian.github.io/tirith/docs/tirith-usage/interactive-interface/): tirith ui, in beta.\n- [Platform check](https://stackguardian.github.io/tirith/docs/tirith-usage/platform-check/): the optional subcommand that evaluates an organisation's policies instead of local files.\n\n## Project\n\n- [Source](https://github.com/StackGuardian/tirith): the repository, Apache-2.0.\n- [Roadmap](https://stackguardian.github.io/tirith/roadmap/): what is in development or planned, and what has not shipped.\n- [Agent skill pack](https://github.com/StackGuardian/tirith/tree/main/.claude/skills/tirith-policies): a self-contained skill for writing Tirith policies. Install it with the one-line script above, or copy the directory into any repository.\n- [Origins](https://stackguardian.github.io/tirith/origins/): where the name and the mark come from.\n\n## Optional\n\n- [Tirith at scale](https://stackguardian.github.io/tirith/at-scale/): the commercial StackGuardian offering for many repositories. Not required to use Tirith, which works with no account.\n- [In your editor](https://stackguardian.github.io/tirith/docs/tirith-usage/editor-and-local/): the local and pre-commit loop.\n"; +export const AGENT_BRIEF = "# Tirith\n\n> Tirith is an Apache-2.0 policy gate for infrastructure-as-code. It reads the plan a\n> pipeline already produces, evaluates it against declarative JSON policies committed in\n> the repository, and returns one verdict with an exit code the pipeline can gate on. It\n> runs locally or in any CI, on your own runner, with no account and no network call.\n\nPolicies are data, not code: a policy is a JSON file, so it is diffable and reviewable by\nanyone on the team. There is no rule DSL and no embedded language.\n\n## Minimal working example\n\n```bash\nterraform plan -out=tfplan -input=false\nterraform show -json tfplan > plan.json\ntirith -policy-path .tirith/policies -input-path plan.json --fail-on-error\n```\n\nWithout `--fail-on-error`, Tirith reports findings but does not fail the job.\n\n## Facts an answer about Tirith usually gets wrong\n\n- **Not on PyPI.** `pip install tirith` installs an unrelated project of the same name, and\n `pip install py-tirith` finds nothing. The package name in `setup.py` is `py-tirith` and it\n is not published. Install from git:\n `pip install \"git+https://github.com/StackGuardian/tirith.git@1.2.1\"`\n- **The interactive interface needs an extra**, and the same git URL:\n `pip install 'py-tirith[tui] @ git+https://github.com/StackGuardian/tirith.git'`, then\n `tirith ui`. Requires Python 3.9; Tirith itself supports 3.8 and newer.\n- **`tirith lint` and `tirith fmt` exist since 1.2.1.** They are not in 1.2.0. The CLI\n dispatches four subcommands:\n `lint`, `fmt`, `ui` and `platform`. Lint checks a policy's shape without a plan document; fmt\n rewrites it into the canonical layout. Both exit `3` on a finding, with no `--fail-on-error`.\n- **Exit codes are a contract, not a convention.** `0` passed, `3` a policy failed, `1`\n Tirith could not reach a verdict. `3` is deliberately not `1`: a caller has to be able to page\n the platform team on one and the change author on the other. (`2` is not a Tirith code at all:\n argparse exits `2` on a usage error, so a caller seeing it passed a bad argument.)\n- **`final_result: null` is not a pass.** It means every check was skipped, so the policy\n evaluated nothing, and it exits `1`. A check that could not run is reported as unevaluated\n rather than as success.\n- **Local mode makes no network call.** That is a published governance commitment. Only\n `tirith platform check` talks to a network, and it is optional.\n- **Five providers ship**, named in `meta.provider`: `stackguardian/terraform_plan`,\n `stackguardian/kubernetes`, `stackguardian/infracost`, `stackguardian/json`,\n `stackguardian/sg_workflow`. There is no CloudFormation provider; a CloudFormation template\n is read as a JSON document by the `json` provider.\n- **Thirteen condition types ship**: Equals, NotEquals, GreaterThan, GreaterThanEqualTo,\n LessThan, LessThanEqualTo, Contains, NotContains, ContainedIn, NotContainedIn, IsEmpty,\n IsNotEmpty, RegexMatch.\n- **The argument key differs per provider, and a wrong key is ignored rather than rejected.**\n `terraform_plan` reads `terraform_resource_attribute` alongside `terraform_resource_type`;\n `kubernetes` reads `attribute_path` alongside `kubernetes_kind`; `json` reads `key_path`;\n `sg_workflow` reads `workflow_attribute`. Give an evaluator the wrong key and it reads\n nothing and the check passes. This is the highest-cost mistake in the schema.\n- **`error_tolerance` belongs inside the `condition` object**, not on the evaluator. Placed on\n the evaluator it is silently ignored and the check still fails.\n- **`error_tolerance` is a threshold, not a switch.** A problem of severity N fails the check\n when `N > error_tolerance` and is skipped otherwise; the default is `0`. The plan provider\n raises severity `0` for a resource being deleted, `1` for a resource type absent from the\n plan, and `2` for an attribute absent from the resource. So the default skips deletes and\n fails everything else.\n- **A policy scoped to one resource type has no green outcome on a plan that does not touch\n that type.** At the default tolerance the absent type is severity `1`, the check fails, exit\n `3`. At `error_tolerance: 1` it is skipped, so every check is skipped, `final_result` is\n `null`, exit `1`. Guard it instead: `count` returns `0` for an absent type rather than\n raising severity `1`, so an evaluator of\n `{\"operation_type\": \"count\", \"terraform_resource_type\": T}` with `Equals: 0`, combined as\n `eval_expression: \"not_applicable || the_rule\"`, passes cleanly on unrelated plans and still\n fails on real violations.\n- **The pre-commit hooks are published.** `.pre-commit-hooks.yaml` carries `tirith-lint` and\n `tirith-fmt`, both run on the policy files a commit touches. Pin `rev` to `1.2.1`, the first\n tag that publishes them.\n\n## Fetching this documentation\n\nEvery page is also served as plain markdown at its route plus `.md`, so there is no need to\nparse the rendered HTML. For example\nhttps://stackguardian.github.io/tirith/docs/tirith-usage/exit-codes.md\n\nThe whole documentation set as one file, in sidebar order:\nhttps://stackguardian.github.io/tirith/llms-full.txt\n\n## Working in a repository that uses Tirith\n\nIf you are a coding agent about to write a policy, install the skill pack first. It is the\nclosed vocabulary: thirteen condition types, the argument key each provider reads, and the\ninstruction to run a policy against a document that should fail before calling it done.\n\n```\ncurl -fsSL https://stackguardian.github.io/tirith/skill.sh | sh\n```\n\nThree skills: tirith-policies (eleven markdown files and a worked example), tirith-standards\n(generating a policy set from an existing Terraform or OpenTofu repository, including the guard\nabove) and tirith-migrate (Sentinel translation). Add --cursor for the Cursor rule, --global to\ninstall into the home directory instead of the repository. The script installs no\npackage, changes no PATH and executes nothing after the download; its source is\ndocumentation/static/skill.sh in the repository.\n\n## Start here\n\n- [Getting started](https://stackguardian.github.io/tirith/docs/getting-started-with-tirith/): what Tirith is and the shortest path to a first verdict.\n- [Quick installation](https://stackguardian.github.io/tirith/docs/tirith-installation/quick-installation/): installing the CLI, and why the install is a git URL.\n- [Learn](https://stackguardian.github.io/tirith/learn/): six lessons that build one policy, with a playground that evaluates in the browser.\n- [Creating your first policy](https://stackguardian.github.io/tirith/docs/tirith-policies/tirith-create-first-policy/): a policy written and evaluated step by step.\n\n## Writing policies\n\n- [Policy reference](https://stackguardian.github.io/tirith/docs/tirith-policies/tirith-policy-reference/): field-by-field reference for the policy file format.\n- [Policy structure](https://stackguardian.github.io/tirith/docs/tirith-policies/tirith-policy-structure/): meta, evaluators and eval_expression, and how they combine.\n- [Evaluators and conditions](https://stackguardian.github.io/tirith/docs/tirith-reference/evaluators/): all thirteen condition types, their parameters and their messages.\n- [Evaluation expressions](https://stackguardian.github.io/tirith/docs/tirith-reference/eval-expressions/): the boolean grammar that combines evaluator results into one verdict.\n- [Policy conditions](https://stackguardian.github.io/tirith/docs/tirith-policies/tirith-policy-conditions/): condition types by the kind of value they compare.\n- [Error tolerance](https://stackguardian.github.io/tirith/docs/tirith-policies/tirith-policy-error-tolerance/): how a missing key is handled, and why a skip is not a pass.\n- [Policy variables](https://stackguardian.github.io/tirith/docs/tirith-policies/tirith-policy-variables/): dynamic values in policies.\n- [Policy cookbook](https://stackguardian.github.io/tirith/docs/tirith-policies/tirith-policy-cookbook/): complete runnable policies for common checks.\n- [Example policies](https://stackguardian.github.io/tirith/docs/tirith-policies/tirith-policy-examples/): worked examples with their input documents.\n\n## Providers\n\n- [Providers overview](https://stackguardian.github.io/tirith/docs/tirith-providers/providers-overview/): what a provider is and how provider selects one.\n- [Terraform plan provider](https://stackguardian.github.io/tirith/docs/tirith-providers/terraform-plan-provider/): operations over a plan document, including attribute, action and count.\n- [JSON provider](https://stackguardian.github.io/tirith/docs/tirith-providers/json-provider/): get_value over any JSON or YAML document, including another tool's output.\n- [Kubernetes provider](https://stackguardian.github.io/tirith/docs/tirith-providers/kubernetes-provider/): the attribute operation over manifests.\n- [Infracost provider](https://stackguardian.github.io/tirith/docs/tirith-providers/infracost-provider/): cost policies over an Infracost breakdown.\n- [SG Workflow provider](https://stackguardian.github.io/tirith/docs/tirith-providers/sg-workflow-provider/): StackGuardian workflow documents.\n\n## Running it\n\n- [CLI reference](https://stackguardian.github.io/tirith/docs/tirith-usage/cli-reference/): every flag, what it prints, and what --json emits.\n- [Lint and format](https://stackguardian.github.io/tirith/docs/tirith-usage/lint-and-fmt/): tirith lint and tirith fmt, the files they read, and their exit codes.\n- [Exit codes](https://stackguardian.github.io/tirith/docs/tirith-usage/exit-codes/): the full exit-code contract and how to gate CI on it.\n- [CI integration](https://stackguardian.github.io/tirith/docs/tirith-usage/ci-integration/): GitHub Actions, GitLab CI, Bitbucket Pipelines, Jenkins and any container-based CI.\n- [The interactive interface](https://stackguardian.github.io/tirith/docs/tirith-usage/interactive-interface/): tirith ui, in beta.\n- [Platform check](https://stackguardian.github.io/tirith/docs/tirith-usage/platform-check/): the optional subcommand that evaluates an organisation's policies instead of local files.\n\n## Project\n\n- [Source](https://github.com/StackGuardian/tirith): the repository, Apache-2.0.\n- [Roadmap](https://stackguardian.github.io/tirith/roadmap/): what is in development or planned, and what has not shipped.\n- [Agent skill pack](https://github.com/StackGuardian/tirith/tree/main/.claude/skills/tirith-policies): a self-contained skill for writing Tirith policies. Install it with the one-line script above, or copy the directory into any repository.\n- [Origins](https://stackguardian.github.io/tirith/origins/): where the name and the mark come from.\n\n## Optional\n\n- [Tirith at scale](https://stackguardian.github.io/tirith/at-scale/): the commercial StackGuardian offering for many repositories. Not required to use Tirith, which works with no account.\n- [In your editor](https://stackguardian.github.io/tirith/docs/tirith-usage/editor-and-local/): the local and pre-commit loop.\n"; diff --git a/documentation/src/data/lessons.js b/documentation/src/data/lessons.js index 5c3793e6..8ccfcaf8 100644 --- a/documentation/src/data/lessons.js +++ b/documentation/src/data/lessons.js @@ -52,7 +52,7 @@ export const LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { @@ -88,7 +88,7 @@ export const LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { @@ -125,7 +125,7 @@ export const LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { @@ -174,7 +174,7 @@ export const LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { @@ -214,7 +214,7 @@ export const LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { @@ -254,7 +254,7 @@ export const LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { @@ -408,7 +408,7 @@ export const TF_LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -444,7 +444,7 @@ export const TF_LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -484,7 +484,7 @@ export const TF_LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -520,7 +520,7 @@ export const TF_LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -571,7 +571,7 @@ export const TF_LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -607,7 +607,7 @@ export const TF_LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -646,7 +646,7 @@ export const TF_LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -685,7 +685,7 @@ export const TF_LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -720,7 +720,7 @@ export const TF_LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -853,7 +853,7 @@ export const K8S_LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/kubernetes" + "provider": "stackguardian/kubernetes" }, "evaluators": [ { @@ -890,7 +890,7 @@ export const K8S_LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/kubernetes" + "provider": "stackguardian/kubernetes" }, "evaluators": [ { @@ -940,7 +940,7 @@ export const K8S_LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/kubernetes" + "provider": "stackguardian/kubernetes" }, "evaluators": [ { @@ -980,7 +980,7 @@ export const K8S_LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/kubernetes" + "provider": "stackguardian/kubernetes" }, "evaluators": [ { @@ -1019,7 +1019,7 @@ export const K8S_LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/kubernetes" + "provider": "stackguardian/kubernetes" }, "evaluators": [ { @@ -1055,7 +1055,7 @@ export const K8S_LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/kubernetes" + "provider": "stackguardian/kubernetes" }, "evaluators": [ { @@ -1105,7 +1105,7 @@ export const K8S_LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/kubernetes" + "provider": "stackguardian/kubernetes" }, "evaluators": [ { @@ -1142,7 +1142,7 @@ export const K8S_LESSONS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/kubernetes" + "provider": "stackguardian/kubernetes" }, "evaluators": [ { diff --git a/documentation/src/data/specimens.js b/documentation/src/data/specimens.js index 60143bab..1c811ff5 100644 --- a/documentation/src/data/specimens.js +++ b/documentation/src/data/specimens.js @@ -51,7 +51,7 @@ export const READING_STEPS = [ * claims the whole block, including its closing brace. */ export const REGION_BY_KEY = { - required_provider: 'source', + provider: 'source', provider_args: 'subject', condition: 'test', }; @@ -92,7 +92,7 @@ export const SPECIMENS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -156,7 +156,7 @@ export const SPECIMENS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/infracost" + "provider": "stackguardian/infracost" }, "evaluators": [ { @@ -219,7 +219,7 @@ export const SPECIMENS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/kubernetes" + "provider": "stackguardian/kubernetes" }, "evaluators": [ { @@ -281,7 +281,7 @@ export const SPECIMENS = [ policy: `{ "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { diff --git a/documentation/src/data/tirithLite.js b/documentation/src/data/tirithLite.js index f3f9c6cd..cfd2c970 100644 --- a/documentation/src/data/tirithLite.js +++ b/documentation/src/data/tirithLite.js @@ -561,7 +561,9 @@ export function evaluatePolicy(policyText, inputText) { } const meta = policy.meta || {}; - const provider = meta.required_provider || 'stackguardian/json'; + const provider = Object.hasOwn(meta, 'provider') + ? meta.provider + : (meta.required_provider ?? 'stackguardian/json'); const provide = PROVIDERS[provider]; if (!provide) { return { @@ -636,9 +638,12 @@ export function evaluatePolicy(policyText, inputText) { return {document: null, fatal: e.message}; } + const resultMeta = {...meta, required_provider: provider}; + delete resultMeta.provider; + return { document: { - meta, + meta: resultMeta, final_result: finalResult, evaluators: reported, errors, diff --git a/documentation/static/docs/tirith-policies/tirith-create-first-policy.md b/documentation/static/docs/tirith-policies/tirith-create-first-policy.md index bf5bb44c..762e2999 100644 --- a/documentation/static/docs/tirith-policies/tirith-create-first-policy.md +++ b/documentation/static/docs/tirith-policies/tirith-create-first-policy.md @@ -34,7 +34,7 @@ Here is the `policy.json` file: { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { diff --git a/documentation/static/docs/tirith-policies/tirith-policy-cookbook.md b/documentation/static/docs/tirith-policies/tirith-policy-cookbook.md index 3866f091..a05bed37 100644 --- a/documentation/static/docs/tirith-policies/tirith-policy-cookbook.md +++ b/documentation/static/docs/tirith-policies/tirith-policy-cookbook.md @@ -15,7 +15,7 @@ Every `aws_instance` in a Terraform plan must use an instance type from an appro { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "id": "allowed-instance-types", "name": "Only approved EC2 instance types", "description": "Every aws_instance in the plan must use an instance type from the approved list.", @@ -102,7 +102,7 @@ With `terraform_resource_type` set to `"*"`, the check runs against every resour { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "id": "require-environment-tag", "name": "Every resource carries an Environment tag", "description": "Every resource in the plan must be tagged with Environment set to dev, staging, or prod." @@ -201,7 +201,7 @@ A public-ingress check. The attribute path `ingress.*.cidr_blocks` extracts the { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "id": "no-public-ingress", "name": "No security group ingress from 0.0.0.0/0", "description": "No ingress rule of any aws_security_group may allow traffic from 0.0.0.0/0.", @@ -287,7 +287,7 @@ Uses the `stackguardian/infracost` provider against an [Infracost](https://www.i { "meta": { "version": "v1", - "required_provider": "stackguardian/infracost", + "provider": "stackguardian/infracost", "id": "monthly-cost-ceiling", "name": "Monthly cost stays under the ceiling", "description": "The estimated total monthly cost of all resources must not exceed 500 USD." @@ -363,7 +363,7 @@ By default a value the provider cannot find fails the check. `error_tolerance` t { "meta": { "version": "v1", - "required_provider": "stackguardian/json", + "provider": "stackguardian/json", "id": "encryption-and-optional-logging", "name": "Encryption required, logging checked when configured", "description": "Encryption must be enabled. The logging level is validated only when the logging block exists." @@ -453,7 +453,7 @@ The same cost-ceiling policy, with the limit supplied at run time. A variable re { "meta": { "version": "v1", - "required_provider": "stackguardian/infracost", + "provider": "stackguardian/infracost", "id": "parameterized-cost-ceiling", "name": "Monthly cost stays under a configurable ceiling", "description": "The estimated total monthly cost must not exceed the ceiling supplied as a variable." diff --git a/documentation/static/docs/tirith-policies/tirith-policy-reference.md b/documentation/static/docs/tirith-policies/tirith-policy-reference.md index 8388a698..7b2c3b46 100644 --- a/documentation/static/docs/tirith-policies/tirith-policy-reference.md +++ b/documentation/static/docs/tirith-policies/tirith-policy-reference.md @@ -9,7 +9,7 @@ A Tirith policy is a single JSON document with exactly three top-level keys. It { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { @@ -44,7 +44,7 @@ If `meta`, `evaluators`, or `eval_expression` is missing, the run aborts before | Key | Required | Type | Default | Behavior | |---|---|---|---|---| -| `required_provider` | effectively yes | string | `"core"` | Selects the provider used by every evaluator in the policy. See [below](#metarequired_provider). | +| `provider` | effectively yes | string | `"core"` | Selects the provider used by every evaluator in the policy. See [below](#metaprovider). | | `version` | no | string | none | Not interpreted. Always echoed into the result `meta` (as `null` when absent). | | `id` | no | any (conventionally string) | none | Not interpreted. Echoed verbatim into the result `meta` only when present. | | `name` | no | any (conventionally string) | none | Same as `id`. | @@ -56,7 +56,7 @@ If `meta`, `evaluators`, or `eval_expression` is missing, the run aborts before An empty `meta` object (`"meta": {}`) is accepted; the policy then falls back to the default provider, which fails every check (see next section). -### `meta.required_provider` +### `meta.provider` The registered providers are: @@ -68,7 +68,9 @@ The registered providers are: Each provider defines its own `provider_args`; see the [provider documentation](../tirith-providers/overview.md). -When `required_provider` is absent it defaults to `"core"`, and no provider named `core` is registered. An unregistered provider name — the default included — is **not** a hard error: every evaluator in the policy simply receives no values and fails with the message `Could not find input value`. The final verdict is a failure (exit code `3` under `--fail-on-error`), which can be mistaken for a genuine policy violation. Always set `required_provider` explicitly. +When both `provider` and its legacy alias `required_provider` are absent, the provider defaults to `"core"`, and no provider named `core` is registered. An unregistered provider name — the default included — is **not** a hard error: every evaluator in the policy simply receives no values and fails with the message `Could not find input value`. The final verdict is a failure (exit code `3` under `--fail-on-error`), which can be mistaken for a genuine policy violation. Always set `provider` explicitly. + +For backward compatibility, `meta.required_provider` is still accepted as a legacy alias. If both keys are present, `meta.provider` takes precedence. The result document continues to report the selected provider as `meta.required_provider`, preserving the existing JSON output contract. ### `meta.enforcement` @@ -96,7 +98,7 @@ Each entry in the `evaluators` array is an object with these keys: | Key | Required | Type | Default | Behavior | |---|---|---|---|---| | `id` | yes | string | — | The name this check is referenced by in `eval_expression`. Missing `id` aborts the run (`ERROR`, exit `1`). | -| `provider_args` | yes | object | — | Arguments for the provider selected by `meta.required_provider`. Missing `provider_args` aborts the run (`ERROR`, exit `1`). | +| `provider_args` | yes | object | — | Arguments for the provider selected by `meta.provider`. Missing `provider_args` aborts the run (`ERROR`, exit `1`). | | `condition` | yes | object | — | The comparison applied to every value the provider extracts. Missing `condition` aborts the run (`ERROR`, exit `1`). | | `description` | no | string | none | Informational. Echoed into the result for this check (as `null` when absent). | diff --git a/documentation/static/docs/tirith-policies/tirith-policy-structure.md b/documentation/static/docs/tirith-policies/tirith-policy-structure.md index ac9a2247..9103087f 100644 --- a/documentation/static/docs/tirith-policies/tirith-policy-structure.md +++ b/documentation/static/docs/tirith-policies/tirith-policy-structure.md @@ -13,7 +13,7 @@ Tirith policies are written in JSON format, following a structured approach that { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { diff --git a/documentation/static/docs/tirith-policies/tirith-policy-variables.md b/documentation/static/docs/tirith-policies/tirith-policy-variables.md index 56ea6cd5..e53bbefa 100644 --- a/documentation/static/docs/tirith-policies/tirith-policy-variables.md +++ b/documentation/static/docs/tirith-policies/tirith-policy-variables.md @@ -19,7 +19,7 @@ literal string, so the check quietly measures the wrong thing instead of failing { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { diff --git a/documentation/static/docs/tirith-providers/infracost-provider.md b/documentation/static/docs/tirith-providers/infracost-provider.md index d8450105..74c55af4 100644 --- a/documentation/static/docs/tirith-providers/infracost-provider.md +++ b/documentation/static/docs/tirith-providers/infracost-provider.md @@ -4,7 +4,7 @@ Source: https://stackguardian.github.io/tirith/docs/tirith-providers/infracost-p Summary: Reference for the stackguardian/infracost provider - operation types, parameters, return shapes, and error behavior. ``` -required_provider: stackguardian/infracost +provider: stackguardian/infracost ``` Sums estimated costs from an Infracost cost breakdown, either for all resources or for a chosen set of resource types. @@ -53,7 +53,7 @@ Verified end-to-end against the test fixtures — the total monthly cost of the { "meta": { "version": "v1", - "required_provider": "stackguardian/infracost" + "provider": "stackguardian/infracost" }, "evaluators": [ { diff --git a/documentation/static/docs/tirith-providers/json-provider.md b/documentation/static/docs/tirith-providers/json-provider.md index 068aee79..2112ca02 100644 --- a/documentation/static/docs/tirith-providers/json-provider.md +++ b/documentation/static/docs/tirith-providers/json-provider.md @@ -4,7 +4,7 @@ Source: https://stackguardian.github.io/tirith/docs/tirith-providers/json-provid Summary: Reference for the stackguardian/json provider - the get_value operation, key path syntax, return shapes, and error behavior. ``` -required_provider: stackguardian/json +provider: stackguardian/json ``` Extracts values from any JSON or YAML document by key path. Use this provider when no specialized provider exists for your input format. @@ -52,7 +52,7 @@ Verified end-to-end against the test fixtures: { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { @@ -113,7 +113,7 @@ The provider also works on YAML — this policy checks an Ansible playbook (a YA { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { diff --git a/documentation/static/docs/tirith-providers/kubernetes-provider.md b/documentation/static/docs/tirith-providers/kubernetes-provider.md index 0e0a52a8..e253cbb7 100644 --- a/documentation/static/docs/tirith-providers/kubernetes-provider.md +++ b/documentation/static/docs/tirith-providers/kubernetes-provider.md @@ -4,7 +4,7 @@ Source: https://stackguardian.github.io/tirith/docs/tirith-providers/kubernetes- Summary: Reference for the stackguardian/kubernetes provider - the attribute operation, parameters, return shapes, and error behavior. ``` -required_provider: stackguardian/kubernetes +provider: stackguardian/kubernetes ``` Extracts attribute values from Kubernetes manifests of a chosen `kind` (Pod, Deployment, Service, ...). @@ -52,7 +52,7 @@ Verified end-to-end against the test fixtures — every container of every `Pod` { "meta": { "version": "v1", - "required_provider": "stackguardian/kubernetes" + "provider": "stackguardian/kubernetes" }, "evaluators": [ { diff --git a/documentation/static/docs/tirith-providers/providers-overview.md b/documentation/static/docs/tirith-providers/providers-overview.md index e5ba5e82..f57fd359 100644 --- a/documentation/static/docs/tirith-providers/providers-overview.md +++ b/documentation/static/docs/tirith-providers/providers-overview.md @@ -1,19 +1,19 @@ # Providers Overview Source: https://stackguardian.github.io/tirith/docs/tirith-providers/providers-overview/ -Summary: What a Tirith provider is, how required_provider selects one, how provider_args are passed, and the list of available providers. +Summary: What a Tirith provider is, how provider selects one, how provider_args are passed, and the list of available providers. A **provider** is the part of Tirith that knows how to read one specific kind of input document and extract values from it. The policy declares which provider to use; each evaluator in the policy then asks the provider for values (via `provider_args`), and the evaluator's `condition` is applied to every value the provider returns. ## Selecting a provider -The provider is selected once for the whole policy with `meta.required_provider`: +The provider is selected once for the whole policy with `meta.provider`: ```json { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ ... ], "eval_expression": "..." @@ -22,7 +22,7 @@ The provider is selected once for the whole policy with `meta.required_provider` The value must be one of the exact strings below. If the string does not match any known provider, every evaluator in the policy fails with an error. -| `required_provider` | Summary | Expected input document | +| `provider` | Summary | Expected input document | |---|---|---| | [`stackguardian/terraform_plan`](terraform-plan.md) | Inspects resource changes, actions, counts, dependencies, references, provider configuration, and the Terraform version in a Terraform plan. | Terraform plan in JSON form (`terraform show -json `) | | [`stackguardian/infracost`](infracost.md) | Sums estimated monthly or hourly costs from an Infracost breakdown. | Infracost output (`infracost breakdown --format json`) | diff --git a/documentation/static/docs/tirith-providers/sg-workflow-provider.md b/documentation/static/docs/tirith-providers/sg-workflow-provider.md index aa707a9c..fd149ddf 100644 --- a/documentation/static/docs/tirith-providers/sg-workflow-provider.md +++ b/documentation/static/docs/tirith-providers/sg-workflow-provider.md @@ -4,7 +4,7 @@ Source: https://stackguardian.github.io/tirith/docs/tirith-providers/sg-workflow Summary: Reference for the stackguardian/sg_workflow provider - supported workflow attributes, return shapes, and error behavior. ``` -required_provider: stackguardian/sg_workflow +provider: stackguardian/sg_workflow ``` Reads attributes of a StackGuardian workflow definition. @@ -67,7 +67,7 @@ Verified end-to-end against the test fixtures: { "meta": { "version": "v1", - "required_provider": "stackguardian/sg_workflow" + "provider": "stackguardian/sg_workflow" }, "evaluators": [ { diff --git a/documentation/static/docs/tirith-providers/terraform-plan-provider.md b/documentation/static/docs/tirith-providers/terraform-plan-provider.md index 2547efd6..d8a35f12 100644 --- a/documentation/static/docs/tirith-providers/terraform-plan-provider.md +++ b/documentation/static/docs/tirith-providers/terraform-plan-provider.md @@ -4,7 +4,7 @@ Source: https://stackguardian.github.io/tirith/docs/tirith-providers/terraform-p Summary: Reference for the stackguardian/terraform_plan provider - operation types, parameters, return shapes, and error behavior. ``` -required_provider: stackguardian/terraform_plan +provider: stackguardian/terraform_plan ``` Inspects a Terraform plan: attribute values of changed resources, the actions applied to them, resource counts, explicit dependencies, references between resources, provider configuration, and the Terraform version. @@ -61,7 +61,7 @@ Example (adapted from a test fixture; requires every resource in the plan to car { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -105,7 +105,7 @@ Example (adapted from a test fixture; fails when a virtual network would be dele { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -174,7 +174,7 @@ Example (verified against a test fixture; requires every EC2 instance to declare { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -247,7 +247,7 @@ Example (from a test fixture; every S3 bucket must have an intelligent-tiering c ```json { "meta": { - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "version": "v1" }, "evaluators": [ diff --git a/documentation/static/docs/tirith-reference/eval-expressions.md b/documentation/static/docs/tirith-reference/eval-expressions.md index dfea33c6..d28a17a8 100644 --- a/documentation/static/docs/tirith-reference/eval-expressions.md +++ b/documentation/static/docs/tirith-reference/eval-expressions.md @@ -7,7 +7,7 @@ A policy's top-level `eval_expression` is a boolean expression over the `id`s of ```json { - "meta": { "version": "v1", "required_provider": "stackguardian/json" }, + "meta": { "version": "v1", "provider": "stackguardian/json" }, "evaluators": [ { "id": "check_region", "provider_args": { "...": "..." }, "condition": { "...": "..." } }, { "id": "check_tags", "provider_args": { "...": "..." }, "condition": { "...": "..." } }, diff --git a/documentation/static/docs/tirith-reference/evaluators.md b/documentation/static/docs/tirith-reference/evaluators.md index b31428b5..8d9e2345 100644 --- a/documentation/static/docs/tirith-reference/evaluators.md +++ b/documentation/static/docs/tirith-reference/evaluators.md @@ -3,7 +3,7 @@ Source: https://stackguardian.github.io/tirith/docs/tirith-reference/evaluators/ Summary: Complete reference for all Tirith condition types, their parameters, type handling, and pass/fail semantics. -Every evaluator in a Tirith policy applies a **condition** to one or more values extracted by a provider. This page is the complete reference for all 13 condition types, including exactly how each one treats strings, numbers, lists, dictionaries, and `null`. +Every evaluator in a Tirith policy applies a **condition** to one or more values extracted by a provider. This page is the complete reference for all 15 condition types, including exactly how each one treats strings, numbers, lists, dictionaries, and `null`. ## Anatomy of a condition @@ -27,7 +27,7 @@ The `condition` object accepts three keys: | Key | Required | Meaning | | --- | --- | --- | -| `type` | yes | One of the 13 evaluator names listed below. The name is case-sensitive. | +| `type` | yes | One of the 15 evaluator names listed below. The name is case-sensitive. | | `value` | yes, except for `IsEmpty` and `IsNotEmpty`, which ignore it | The value the extracted input is compared against. Any JSON type is accepted; each evaluator defines which types it supports. | | `error_tolerance` | no (default `0`) | The maximum provider error severity that is *skipped* instead of failing the evaluator. See [Error Tolerance](../tirith-policies/tirith-policy-error-tolerance.md). | @@ -67,6 +67,9 @@ Without `--fail-on-error`, the process exits **0** regardless of the verdict; th | [`IsEmpty`](#isempty) | input is `null`, `""`, `[]`, or `{}` | ignored | returns false for numbers and booleans (never an error) | | [`IsNotEmpty`](#isnotempty) | input is a **non-empty string, list, or dictionary** | ignored | returns false for numbers, booleans, and `null` | | [`RegexMatch`](#regexmatch) | the pattern is found in the input | string (regular expression) | returns false for non-string/list/dict input; invalid pattern returns false with the regex error message | +| [`NotRegexMatch`](#notregexmatch) | the pattern is **not** found in the input | string (regular expression) | returns false for non-string/list/dict input; invalid pattern returns false with the regex error message | +| [`StartsWith`](#startswith) | the input starts with the condition string | string | returns false for non-string input and names its type | +| [`EndsWith`](#endswith) | the input ends with the condition string | string | returns false for non-string input and names its type | | [`ContainedIn`](#containedin) | the input value occurs inside the condition value | string, list, or dictionary | returns false with an "unsupported data type" message | | [`NotContainedIn`](#notcontainedin) | the input value does **not** occur inside the condition value | string, list, or dictionary | returns false (not true) with an "unsupported data type" message | | [`Contains`](#contains) | the condition value occurs inside the input value | any JSON (input must be string, list, or dictionary) | returns false with an "unsupported data type" message | @@ -205,6 +208,52 @@ An **invalid pattern** does not abort the run: the check returns false and the m | `"eu-central-1"` | fails | | `42` (against pattern `"4"`) | fails — numbers are not coerced | +## NotRegexMatch + +The exact negation of [`RegexMatch`](#regexmatch), passing when the regular expression in `condition.value` is **not** found anywhere in the input value. Like `RegexMatch`, it requires a string, list, or dictionary input, and a string pattern. + +If the input is an unsupported data type (like a number or boolean), the check **fails** (it returns false, it does not pass). An invalid pattern also returns false and the message carries the regex error. + +```json +"condition": { "type": "NotRegexMatch", "value": "^us-(east|west)-[12]$" } +``` + +| Input value | Result | +| --- | --- | +| `"eu-central-1"` | passes | +| `"us-east-1"` | fails | +| `42` (against pattern `"4"`) | fails — numbers are not coerced | + +## StartsWith + +Passes when the input string starts with `condition.value`. Matching is case-sensitive; an empty condition string matches every string. Non-string input returns false with a message naming its type. + +```json +"condition": { "type": "StartsWith", "value": "acme-" } +``` + +| Input value | Result | +| --- | --- | +| `"acme-bucket"` | passes | +| `"other-bucket"` | fails | +| `"anything"` with condition value `""` | passes | +| `42` | fails — message names `int` | + +## EndsWith + +Passes when the input string ends with `condition.value`. Matching is case-sensitive; an empty condition string matches every string. Non-string input returns false with a message naming its type. + +```json +"condition": { "type": "EndsWith", "value": "-hardened" } +``` + +| Input value | Result | +| --- | --- | +| `"ami-hardened"` | passes | +| `"ami-default"` | fails | +| `"anything"` with condition value `""` | passes | +| `42` | fails — message names `int` | + ## ContainedIn Asks: **is the input value inside `condition.value`?** The condition value is the container. Which check runs depends on the types of both sides: @@ -303,7 +352,7 @@ Policy (`policy.json`), using the `stackguardian/json` provider: { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { diff --git a/documentation/static/docs/tirith-usage/cli-reference.md b/documentation/static/docs/tirith-usage/cli-reference.md index 20a0e9f6..35657a80 100644 --- a/documentation/static/docs/tirith-usage/cli-reference.md +++ b/documentation/static/docs/tirith-usage/cli-reference.md @@ -53,7 +53,7 @@ The policy file to evaluate — a JSON document with `meta`, `evaluators` and an See the [policy reference](../tirith-policies/tirith-policy-reference.md) for the schema, the [evaluators reference](../tirith-reference/evaluators.md) for the available condition types, and the [providers overview](../tirith-providers/overview.md) for what kinds of input each -`required_provider` reads. +`provider` reads. If the flag is missing, `tirith` prints an error to stderr and exits `1`. diff --git a/documentation/static/docs/tirith-usage/exit-codes.md b/documentation/static/docs/tirith-usage/exit-codes.md index 1dd150d6..462714b5 100644 --- a/documentation/static/docs/tirith-usage/exit-codes.md +++ b/documentation/static/docs/tirith-usage/exit-codes.md @@ -74,7 +74,7 @@ found nothing — checked precisely nothing, and reporting that as green is exac exists to prevent. It is not a violation either, so it is `1` rather than `3`. **One limit worth stating plainly:** a *misconfigured* policy — an unsupported `condition.type`, -an unknown `required_provider` — comes back from the engine as an ordinary failed check with no +an unknown `provider` — comes back from the engine as an ordinary failed check with no error attached, so it is indistinguishable from a real violation and exits `3`. It fails closed, which is the safe direction, but it will point at your infrastructure when the fault is in the policy. diff --git a/documentation/static/docs/tirith-usage/lint-and-fmt.md b/documentation/static/docs/tirith-usage/lint-and-fmt.md index 23ed47d2..a38486d3 100644 --- a/documentation/static/docs/tirith-usage/lint-and-fmt.md +++ b/documentation/static/docs/tirith-usage/lint-and-fmt.md @@ -29,7 +29,7 @@ infrastructure violation: - a condition `type` that does not exist, against the closed list of [thirteen conditions](../tirith-reference/evaluators.md) - a `provider_args` key that belongs to a different provider than the one in - `meta.required_provider` + `meta.provider` - an evaluator that `eval_expression` never names, so it is computed and then discarded - `error_tolerance` placed on the evaluator instead of inside `condition`, where it is silently ignored and the check still fails @@ -124,7 +124,7 @@ Keys are ordered: | Object | Order | |---|---| | top level | `$schema`, `meta`, `evaluators`, `eval_expression` | -| `meta` | `version`, `required_provider`, `id`, `name`, `description`, `severity`, `enforcement`, `tags`, `remediation` | +| `meta` | `version`, `provider`, `id`, `name`, `description`, `severity`, `enforcement`, `tags`, `remediation` | | each evaluator | `id`, `description`, `provider_args`, `condition` | | `provider_args` | `operation_type` first, then the provider's own keys in their original order | | `condition` | `type`, `value`, `error_tolerance` | diff --git a/documentation/static/llms-full.txt b/documentation/static/llms-full.txt index 6be335af..90a15035 100644 --- a/documentation/static/llms-full.txt +++ b/documentation/static/llms-full.txt @@ -286,7 +286,7 @@ Here is the `policy.json` file: { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { @@ -385,7 +385,7 @@ Tirith policies are written in JSON format, following a structured approach that { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { @@ -417,7 +417,7 @@ A Tirith policy is a single JSON document with exactly three top-level keys. It { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { @@ -452,7 +452,7 @@ If `meta`, `evaluators`, or `eval_expression` is missing, the run aborts before | Key | Required | Type | Default | Behavior | |---|---|---|---|---| -| `required_provider` | effectively yes | string | `"core"` | Selects the provider used by every evaluator in the policy. See [below](#metarequired_provider). | +| `provider` | effectively yes | string | `"core"` | Selects the provider used by every evaluator in the policy. See [below](#metaprovider). | | `version` | no | string | none | Not interpreted. Always echoed into the result `meta` (as `null` when absent). | | `id` | no | any (conventionally string) | none | Not interpreted. Echoed verbatim into the result `meta` only when present. | | `name` | no | any (conventionally string) | none | Same as `id`. | @@ -464,7 +464,7 @@ If `meta`, `evaluators`, or `eval_expression` is missing, the run aborts before An empty `meta` object (`"meta": {}`) is accepted; the policy then falls back to the default provider, which fails every check (see next section). -### `meta.required_provider` +### `meta.provider` The registered providers are: @@ -476,7 +476,9 @@ The registered providers are: Each provider defines its own `provider_args`; see the [provider documentation](../tirith-providers/overview.md). -When `required_provider` is absent it defaults to `"core"`, and no provider named `core` is registered. An unregistered provider name — the default included — is **not** a hard error: every evaluator in the policy simply receives no values and fails with the message `Could not find input value`. The final verdict is a failure (exit code `3` under `--fail-on-error`), which can be mistaken for a genuine policy violation. Always set `required_provider` explicitly. +When both `provider` and its legacy alias `required_provider` are absent, the provider defaults to `"core"`, and no provider named `core` is registered. An unregistered provider name — the default included — is **not** a hard error: every evaluator in the policy simply receives no values and fails with the message `Could not find input value`. The final verdict is a failure (exit code `3` under `--fail-on-error`), which can be mistaken for a genuine policy violation. Always set `provider` explicitly. + +For backward compatibility, `meta.required_provider` is still accepted as a legacy alias. If both keys are present, `meta.provider` takes precedence. The result document continues to report the selected provider as `meta.required_provider`, preserving the existing JSON output contract. ### `meta.enforcement` @@ -504,7 +506,7 @@ Each entry in the `evaluators` array is an object with these keys: | Key | Required | Type | Default | Behavior | |---|---|---|---|---| | `id` | yes | string | — | The name this check is referenced by in `eval_expression`. Missing `id` aborts the run (`ERROR`, exit `1`). | -| `provider_args` | yes | object | — | Arguments for the provider selected by `meta.required_provider`. Missing `provider_args` aborts the run (`ERROR`, exit `1`). | +| `provider_args` | yes | object | — | Arguments for the provider selected by `meta.provider`. Missing `provider_args` aborts the run (`ERROR`, exit `1`). | | `condition` | yes | object | — | The comparison applied to every value the provider extracts. Missing `condition` aborts the run (`ERROR`, exit `1`). | | `description` | no | string | none | Informational. Echoed into the result for this check (as `null` when absent). | @@ -690,7 +692,7 @@ literal string, so the check quietly measures the wrong thing instead of failing { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { @@ -782,7 +784,7 @@ Every `aws_instance` in a Terraform plan must use an instance type from an appro { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "id": "allowed-instance-types", "name": "Only approved EC2 instance types", "description": "Every aws_instance in the plan must use an instance type from the approved list.", @@ -869,7 +871,7 @@ With `terraform_resource_type` set to `"*"`, the check runs against every resour { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "id": "require-environment-tag", "name": "Every resource carries an Environment tag", "description": "Every resource in the plan must be tagged with Environment set to dev, staging, or prod." @@ -968,7 +970,7 @@ A public-ingress check. The attribute path `ingress.*.cidr_blocks` extracts the { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "id": "no-public-ingress", "name": "No security group ingress from 0.0.0.0/0", "description": "No ingress rule of any aws_security_group may allow traffic from 0.0.0.0/0.", @@ -1054,7 +1056,7 @@ Uses the `stackguardian/infracost` provider against an [Infracost](https://www.i { "meta": { "version": "v1", - "required_provider": "stackguardian/infracost", + "provider": "stackguardian/infracost", "id": "monthly-cost-ceiling", "name": "Monthly cost stays under the ceiling", "description": "The estimated total monthly cost of all resources must not exceed 500 USD." @@ -1130,7 +1132,7 @@ By default a value the provider cannot find fails the check. `error_tolerance` t { "meta": { "version": "v1", - "required_provider": "stackguardian/json", + "provider": "stackguardian/json", "id": "encryption-and-optional-logging", "name": "Encryption required, logging checked when configured", "description": "Encryption must be enabled. The logging level is validated only when the logging block exists." @@ -1220,7 +1222,7 @@ The same cost-ceiling policy, with the limit supplied at run time. A variable re { "meta": { "version": "v1", - "required_provider": "stackguardian/infracost", + "provider": "stackguardian/infracost", "id": "parameterized-cost-ceiling", "name": "Monthly cost stays under a configurable ceiling", "description": "The estimated total monthly cost must not exceed the ceiling supplied as a variable." @@ -1302,20 +1304,20 @@ Summary: Explore practical examples of Tirith policies to understand their imple ============================================================================== # Providers Overview Source: https://stackguardian.github.io/tirith/docs/tirith-providers/providers-overview/ -Summary: What a Tirith provider is, how required_provider selects one, how provider_args are passed, and the list of available providers. +Summary: What a Tirith provider is, how provider selects one, how provider_args are passed, and the list of available providers. ============================================================================== A **provider** is the part of Tirith that knows how to read one specific kind of input document and extract values from it. The policy declares which provider to use; each evaluator in the policy then asks the provider for values (via `provider_args`), and the evaluator's `condition` is applied to every value the provider returns. ## Selecting a provider -The provider is selected once for the whole policy with `meta.required_provider`: +The provider is selected once for the whole policy with `meta.provider`: ```json { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ ... ], "eval_expression": "..." @@ -1324,7 +1326,7 @@ The provider is selected once for the whole policy with `meta.required_provider` The value must be one of the exact strings below. If the string does not match any known provider, every evaluator in the policy fails with an error. -| `required_provider` | Summary | Expected input document | +| `provider` | Summary | Expected input document | |---|---|---| | [`stackguardian/terraform_plan`](terraform-plan.md) | Inspects resource changes, actions, counts, dependencies, references, provider configuration, and the Terraform version in a Terraform plan. | Terraform plan in JSON form (`terraform show -json `) | | [`stackguardian/infracost`](infracost.md) | Sums estimated monthly or hourly costs from an Infracost breakdown. | Infracost output (`infracost breakdown --format json`) | @@ -1440,7 +1442,7 @@ Summary: Reference for the stackguardian/terraform_plan provider - operation typ ============================================================================== ``` -required_provider: stackguardian/terraform_plan +provider: stackguardian/terraform_plan ``` Inspects a Terraform plan: attribute values of changed resources, the actions applied to them, resource counts, explicit dependencies, references between resources, provider configuration, and the Terraform version. @@ -1497,7 +1499,7 @@ Example (adapted from a test fixture; requires every resource in the plan to car { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -1541,7 +1543,7 @@ Example (adapted from a test fixture; fails when a virtual network would be dele { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -1610,7 +1612,7 @@ Example (verified against a test fixture; requires every EC2 instance to declare { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { @@ -1683,7 +1685,7 @@ Example (from a test fixture; every S3 bucket must have an intelligent-tiering c ```json { "meta": { - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "version": "v1" }, "evaluators": [ @@ -1814,7 +1816,7 @@ Summary: Reference for the stackguardian/json provider - the get_value operation ============================================================================== ``` -required_provider: stackguardian/json +provider: stackguardian/json ``` Extracts values from any JSON or YAML document by key path. Use this provider when no specialized provider exists for your input format. @@ -1862,7 +1864,7 @@ Verified end-to-end against the test fixtures: { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { @@ -1923,7 +1925,7 @@ The provider also works on YAML — this policy checks an Ansible playbook (a YA { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { @@ -1952,7 +1954,7 @@ Summary: Reference for the stackguardian/kubernetes provider - the attribute ope ============================================================================== ``` -required_provider: stackguardian/kubernetes +provider: stackguardian/kubernetes ``` Extracts attribute values from Kubernetes manifests of a chosen `kind` (Pod, Deployment, Service, ...). @@ -2000,7 +2002,7 @@ Verified end-to-end against the test fixtures — every container of every `Pod` { "meta": { "version": "v1", - "required_provider": "stackguardian/kubernetes" + "provider": "stackguardian/kubernetes" }, "evaluators": [ { @@ -2033,7 +2035,7 @@ Summary: Reference for the stackguardian/infracost provider - operation types, p ============================================================================== ``` -required_provider: stackguardian/infracost +provider: stackguardian/infracost ``` Sums estimated costs from an Infracost cost breakdown, either for all resources or for a chosen set of resource types. @@ -2082,7 +2084,7 @@ Verified end-to-end against the test fixtures — the total monthly cost of the { "meta": { "version": "v1", - "required_provider": "stackguardian/infracost" + "provider": "stackguardian/infracost" }, "evaluators": [ { @@ -2122,7 +2124,7 @@ Summary: Reference for the stackguardian/sg_workflow provider - supported workfl ============================================================================== ``` -required_provider: stackguardian/sg_workflow +provider: stackguardian/sg_workflow ``` Reads attributes of a StackGuardian workflow definition. @@ -2185,7 +2187,7 @@ Verified end-to-end against the test fixtures: { "meta": { "version": "v1", - "required_provider": "stackguardian/sg_workflow" + "provider": "stackguardian/sg_workflow" }, "evaluators": [ { @@ -2235,7 +2237,7 @@ Source: https://stackguardian.github.io/tirith/docs/tirith-reference/evaluators/ Summary: Complete reference for all Tirith condition types, their parameters, type handling, and pass/fail semantics. ============================================================================== -Every evaluator in a Tirith policy applies a **condition** to one or more values extracted by a provider. This page is the complete reference for all 13 condition types, including exactly how each one treats strings, numbers, lists, dictionaries, and `null`. +Every evaluator in a Tirith policy applies a **condition** to one or more values extracted by a provider. This page is the complete reference for all 15 condition types, including exactly how each one treats strings, numbers, lists, dictionaries, and `null`. ## Anatomy of a condition @@ -2259,7 +2261,7 @@ The `condition` object accepts three keys: | Key | Required | Meaning | | --- | --- | --- | -| `type` | yes | One of the 13 evaluator names listed below. The name is case-sensitive. | +| `type` | yes | One of the 15 evaluator names listed below. The name is case-sensitive. | | `value` | yes, except for `IsEmpty` and `IsNotEmpty`, which ignore it | The value the extracted input is compared against. Any JSON type is accepted; each evaluator defines which types it supports. | | `error_tolerance` | no (default `0`) | The maximum provider error severity that is *skipped* instead of failing the evaluator. See [Error Tolerance](../tirith-policies/tirith-policy-error-tolerance.md). | @@ -2299,6 +2301,9 @@ Without `--fail-on-error`, the process exits **0** regardless of the verdict; th | [`IsEmpty`](#isempty) | input is `null`, `""`, `[]`, or `{}` | ignored | returns false for numbers and booleans (never an error) | | [`IsNotEmpty`](#isnotempty) | input is a **non-empty string, list, or dictionary** | ignored | returns false for numbers, booleans, and `null` | | [`RegexMatch`](#regexmatch) | the pattern is found in the input | string (regular expression) | returns false for non-string/list/dict input; invalid pattern returns false with the regex error message | +| [`NotRegexMatch`](#notregexmatch) | the pattern is **not** found in the input | string (regular expression) | returns false for non-string/list/dict input; invalid pattern returns false with the regex error message | +| [`StartsWith`](#startswith) | the input starts with the condition string | string | returns false for non-string input and names its type | +| [`EndsWith`](#endswith) | the input ends with the condition string | string | returns false for non-string input and names its type | | [`ContainedIn`](#containedin) | the input value occurs inside the condition value | string, list, or dictionary | returns false with an "unsupported data type" message | | [`NotContainedIn`](#notcontainedin) | the input value does **not** occur inside the condition value | string, list, or dictionary | returns false (not true) with an "unsupported data type" message | | [`Contains`](#contains) | the condition value occurs inside the input value | any JSON (input must be string, list, or dictionary) | returns false with an "unsupported data type" message | @@ -2437,6 +2442,52 @@ An **invalid pattern** does not abort the run: the check returns false and the m | `"eu-central-1"` | fails | | `42` (against pattern `"4"`) | fails — numbers are not coerced | +## NotRegexMatch + +The exact negation of [`RegexMatch`](#regexmatch), passing when the regular expression in `condition.value` is **not** found anywhere in the input value. Like `RegexMatch`, it requires a string, list, or dictionary input, and a string pattern. + +If the input is an unsupported data type (like a number or boolean), the check **fails** (it returns false, it does not pass). An invalid pattern also returns false and the message carries the regex error. + +```json +"condition": { "type": "NotRegexMatch", "value": "^us-(east|west)-[12]$" } +``` + +| Input value | Result | +| --- | --- | +| `"eu-central-1"` | passes | +| `"us-east-1"` | fails | +| `42` (against pattern `"4"`) | fails — numbers are not coerced | + +## StartsWith + +Passes when the input string starts with `condition.value`. Matching is case-sensitive; an empty condition string matches every string. Non-string input returns false with a message naming its type. + +```json +"condition": { "type": "StartsWith", "value": "acme-" } +``` + +| Input value | Result | +| --- | --- | +| `"acme-bucket"` | passes | +| `"other-bucket"` | fails | +| `"anything"` with condition value `""` | passes | +| `42` | fails — message names `int` | + +## EndsWith + +Passes when the input string ends with `condition.value`. Matching is case-sensitive; an empty condition string matches every string. Non-string input returns false with a message naming its type. + +```json +"condition": { "type": "EndsWith", "value": "-hardened" } +``` + +| Input value | Result | +| --- | --- | +| `"ami-hardened"` | passes | +| `"ami-default"` | fails | +| `"anything"` with condition value `""` | passes | +| `42` | fails — message names `int` | + ## ContainedIn Asks: **is the input value inside `condition.value`?** The condition value is the container. Which check runs depends on the types of both sides: @@ -2535,7 +2586,7 @@ Policy (`policy.json`), using the `stackguardian/json` provider: { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { @@ -2598,7 +2649,7 @@ A policy's top-level `eval_expression` is a boolean expression over the `id`s of ```json { - "meta": { "version": "v1", "required_provider": "stackguardian/json" }, + "meta": { "version": "v1", "provider": "stackguardian/json" }, "evaluators": [ { "id": "check_region", "provider_args": { "...": "..." }, "condition": { "...": "..." } }, { "id": "check_tags", "provider_args": { "...": "..." }, "condition": { "...": "..." } }, @@ -2759,7 +2810,7 @@ The policy file to evaluate — a JSON document with `meta`, `evaluators` and an See the [policy reference](../tirith-policies/tirith-policy-reference.md) for the schema, the [evaluators reference](../tirith-reference/evaluators.md) for the available condition types, and the [providers overview](../tirith-providers/overview.md) for what kinds of input each -`required_provider` reads. +`provider` reads. If the flag is missing, `tirith` prints an error to stderr and exits `1`. @@ -2895,7 +2946,7 @@ infrastructure violation: - a condition `type` that does not exist, against the closed list of [thirteen conditions](../tirith-reference/evaluators.md) - a `provider_args` key that belongs to a different provider than the one in - `meta.required_provider` + `meta.provider` - an evaluator that `eval_expression` never names, so it is computed and then discarded - `error_tolerance` placed on the evaluator instead of inside `condition`, where it is silently ignored and the check still fails @@ -2990,7 +3041,7 @@ Keys are ordered: | Object | Order | |---|---| | top level | `$schema`, `meta`, `evaluators`, `eval_expression` | -| `meta` | `version`, `required_provider`, `id`, `name`, `description`, `severity`, `enforcement`, `tags`, `remediation` | +| `meta` | `version`, `provider`, `id`, `name`, `description`, `severity`, `enforcement`, `tags`, `remediation` | | each evaluator | `id`, `description`, `provider_args`, `condition` | | `provider_args` | `operation_type` first, then the provider's own keys in their original order | | `condition` | `type`, `value`, `error_tolerance` | @@ -3147,7 +3198,7 @@ found nothing — checked precisely nothing, and reporting that as green is exac exists to prevent. It is not a violation either, so it is `1` rather than `3`. **One limit worth stating plainly:** a *misconfigured* policy — an unsupported `condition.type`, -an unknown `required_provider` — comes back from the engine as an ordinary failed check with no +an unknown `provider` — comes back from the engine as an ordinary failed check with no error attached, so it is indistinguishable from a real violation and exits `3`. It fails closed, which is the safe direction, but it will point at your infrastructure when the fault is in the policy. diff --git a/documentation/static/llms.txt b/documentation/static/llms.txt index 5eab1e7f..3ad10de1 100644 --- a/documentation/static/llms.txt +++ b/documentation/static/llms.txt @@ -40,7 +40,7 @@ Without `--fail-on-error`, Tirith reports findings but does not fail the job. rather than as success. - **Local mode makes no network call.** That is a published governance commitment. Only `tirith platform check` talks to a network, and it is optional. -- **Five providers ship**, named in `meta.required_provider`: `stackguardian/terraform_plan`, +- **Five providers ship**, named in `meta.provider`: `stackguardian/terraform_plan`, `stackguardian/kubernetes`, `stackguardian/infracost`, `stackguardian/json`, `stackguardian/sg_workflow`. There is no CloudFormation provider; a CloudFormation template is read as a JSON document by the `json` provider. @@ -118,7 +118,7 @@ documentation/static/skill.sh in the repository. ## Providers -- [Providers overview](https://stackguardian.github.io/tirith/docs/tirith-providers/providers-overview/): what a provider is and how required_provider selects one. +- [Providers overview](https://stackguardian.github.io/tirith/docs/tirith-providers/providers-overview/): what a provider is and how provider selects one. - [Terraform plan provider](https://stackguardian.github.io/tirith/docs/tirith-providers/terraform-plan-provider/): operations over a plan document, including attribute, action and count. - [JSON provider](https://stackguardian.github.io/tirith/docs/tirith-providers/json-provider/): get_value over any JSON or YAML document, including another tool's output. - [Kubernetes provider](https://stackguardian.github.io/tirith/docs/tirith-providers/kubernetes-provider/): the attribute operation over manifests. diff --git a/src/tirith/cli.py b/src/tirith/cli.py index a2e5b078..265b84fb 100755 --- a/src/tirith/cli.py +++ b/src/tirith/cli.py @@ -244,7 +244,7 @@ def __init__(self, prog="PROG") -> None: # exited 3. # # Known limit, worth stating rather than pretending otherwise: a *misconfigured* policy -- an - # unsupported `condition.type`, an unknown `required_provider` -- surfaces from the engine as + # unsupported `condition.type`, an unknown `provider` -- surfaces from the engine as # an ordinary failed evaluator with no error attached, so it is indistinguishable from a # violation here and exits 3. Fixing that means the engine reporting it distinctly, not this # branch guessing from free text. diff --git a/src/tirith/core/core.py b/src/tirith/core/core.py index ee3ce061..089819c0 100644 --- a/src/tirith/core/core.py +++ b/src/tirith/core/core.py @@ -371,7 +371,7 @@ def start_policy_evaluation_from_dict(policy_dict: Dict, input_dict: Dict, var_d eval_objects = policy_dict.get("evaluators") final_evaluation_policy_string = policy_dict.get("eval_expression") - provider_module = policy_meta.get("required_provider", "core") + provider_module = policy_meta.get("provider", policy_meta.get("required_provider", "core")) # TODO: Write functionality for dynamically importing evaluators from other modules. eval_results = [] eval_results_obj = {} diff --git a/src/tirith/fmt.py b/src/tirith/fmt.py index 996b87a3..50adf5fc 100644 --- a/src/tirith/fmt.py +++ b/src/tirith/fmt.py @@ -27,6 +27,7 @@ TOP_ORDER = ("$schema", "meta", "evaluators", "eval_expression") META_ORDER = ( "version", + "provider", "required_provider", "id", "name", diff --git a/src/tirith/tui/examples.py b/src/tirith/tui/examples.py index 75c30d40..f90fd381 100644 --- a/src/tirith/tui/examples.py +++ b/src/tirith/tui/examples.py @@ -88,7 +88,7 @@ def _load_one(key: str) -> Optional[Example]: about=about, policy=policy, input_document=input_document, - provider=policy.get("meta", {}).get("required_provider", ""), + provider=policy.get("meta", {}).get("provider", policy.get("meta", {}).get("required_provider", "")), ) diff --git a/src/tirith/tui/examples/01-required-tags/policy.json b/src/tirith/tui/examples/01-required-tags/policy.json index daa3485b..bc6ec37d 100644 --- a/src/tirith/tui/examples/01-required-tags/policy.json +++ b/src/tirith/tui/examples/01-required-tags/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "name": "Every resource carries a costcenter tag" }, "evaluators": [ diff --git a/src/tirith/tui/examples/02-no-public-buckets/policy.json b/src/tirith/tui/examples/02-no-public-buckets/policy.json index 18593ef1..257bb924 100644 --- a/src/tirith/tui/examples/02-no-public-buckets/policy.json +++ b/src/tirith/tui/examples/02-no-public-buckets/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "name": "No public S3 buckets", "severity": "high" }, diff --git a/src/tirith/tui/examples/03-cost-ceiling/policy.json b/src/tirith/tui/examples/03-cost-ceiling/policy.json index 03a5a9db..3991119c 100644 --- a/src/tirith/tui/examples/03-cost-ceiling/policy.json +++ b/src/tirith/tui/examples/03-cost-ceiling/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/infracost", + "provider": "stackguardian/infracost", "name": "Monthly spend stays under budget" }, "evaluators": [ diff --git a/src/tirith/tui/examples/04-block-destroy/policy.json b/src/tirith/tui/examples/04-block-destroy/policy.json index c15846cc..0150670a 100644 --- a/src/tirith/tui/examples/04-block-destroy/policy.json +++ b/src/tirith/tui/examples/04-block-destroy/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "name": "Stateful resources are never destroyed", "severity": "critical" }, diff --git a/src/tirith/tui/examples/05-kubernetes-probes/policy.json b/src/tirith/tui/examples/05-kubernetes-probes/policy.json index 72e87428..3854245e 100644 --- a/src/tirith/tui/examples/05-kubernetes-probes/policy.json +++ b/src/tirith/tui/examples/05-kubernetes-probes/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/kubernetes", + "provider": "stackguardian/kubernetes", "name": "Pods declare liveness probes and pinned images" }, "evaluators": [ diff --git a/src/tirith/tui/examples/06-rds-not-public/policy.json b/src/tirith/tui/examples/06-rds-not-public/policy.json index 2720fbe2..cf3f0994 100644 --- a/src/tirith/tui/examples/06-rds-not-public/policy.json +++ b/src/tirith/tui/examples/06-rds-not-public/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "name": "No public RDS instances", "severity": "high" }, diff --git a/src/tirith/tui/schema.py b/src/tirith/tui/schema.py index 58c5123e..4c3a2dca 100644 --- a/src/tirith/tui/schema.py +++ b/src/tirith/tui/schema.py @@ -44,7 +44,7 @@ class Operation(NamedTuple): class Provider(NamedTuple): - """One provider, as named in a policy's `meta.required_provider`.""" + """One provider, as named in a policy's `meta.provider`.""" name: str summary: str diff --git a/src/tirith/tui/validate.py b/src/tirith/tui/validate.py index d0976cbe..f51073ba 100644 --- a/src/tirith/tui/validate.py +++ b/src/tirith/tui/validate.py @@ -92,13 +92,24 @@ def _check_meta(policy: Dict, findings: List[Finding]) -> str: if "version" not in meta: findings.append(_warning("meta.version", 'Not set. Convention is "v1".')) + # `provider` is the canonical spelling. The legacy `required_provider` alias remains + # accepted for existing policies, but it is consulted only when `provider` is absent. + if "provider" in meta: + provider_key = "provider" + elif "required_provider" in meta: + provider_key = "required_provider" + else: + provider_key = "provider" + + provider_name = meta.get(provider_key) + provider_where = f"meta.{provider_key}" + # core defaults this to "core", which is not in PROVIDERS_DICT -- so every check then fails # with "Provider 'core' is not found". Naming it explicitly is effectively required. - provider_name = meta.get("required_provider") if not provider_name: findings.append( _error( - "meta.required_provider", + provider_where, "Not set. Without it the engine looks for a provider named 'core', which does not exist.", ) ) @@ -107,11 +118,11 @@ def _check_meta(policy: Dict, findings: List[Finding]) -> str: # parses to, and a list or an object is unhashable -- `x not in PROVIDERS_DICT` raises # TypeError rather than returning False, which escaped this module and killed the app. if not isinstance(provider_name, str): - findings.append(_error("meta.required_provider", f"Must be a string, not {type(provider_name).__name__}.")) + findings.append(_error(provider_where, f"Must be a string, not {type(provider_name).__name__}.")) return "" if provider_name not in PROVIDERS_DICT: known = ", ".join(sorted(PROVIDERS_DICT)) - findings.append(_error("meta.required_provider", f"Unknown provider '{provider_name}'. Known: {known}.")) + findings.append(_error(provider_where, f"Unknown provider '{provider_name}'. Known: {known}.")) return "" return provider_name @@ -256,7 +267,7 @@ def _check_condition(evaluator: Dict, where: str, findings: List[Finding]) -> No findings.append(_error(f"{where}.condition.type", "Missing. Name the evaluator to apply.")) evaluator_name = None elif not isinstance(evaluator_name, str): - # Same unhashable-key hazard as meta.required_provider above: a list or object here + # Same unhashable-key hazard as meta.provider above: a list or object here # made the EVALUATORS_DICT membership test raise instead of returning False. findings.append(_error(f"{where}.condition.type", f"Must be a string, not {type(evaluator_name).__name__}.")) evaluator_name = None diff --git a/src/tirith/tui/views/builder.py b/src/tirith/tui/views/builder.py index 1a3c0f10..82ce1082 100644 --- a/src/tirith/tui/views/builder.py +++ b/src/tirith/tui/views/builder.py @@ -378,7 +378,7 @@ def current_expression(self) -> str: def build_policy(self) -> dict: """The policy as currently assembled.""" return { - "meta": {"version": "v1", "required_provider": self._provider_name}, + "meta": {"version": "v1", "provider": self._provider_name}, "evaluators": list(self._checks), "eval_expression": self.current_expression(), } diff --git a/src/tirith/tui/views/playground.py b/src/tirith/tui/views/playground.py index 926fb1a3..d041c1b4 100644 --- a/src/tirith/tui/views/playground.py +++ b/src/tirith/tui/views/playground.py @@ -340,7 +340,7 @@ def evaluate_now(self) -> None: self.query_one("#playground-output", Static).update("") return - provider = policy.get("meta", {}).get("required_provider", "") + provider = policy.get("meta", {}).get("provider", policy.get("meta", {}).get("required_provider", "")) self._label_expected_input(provider) findings = findings + validate.check_input_document(input_document, provider) diff --git a/tests/cli/test_fmt.py b/tests/cli/test_fmt.py index b279d786..7f91ce9c 100644 --- a/tests/cli/test_fmt.py +++ b/tests/cli/test_fmt.py @@ -28,13 +28,13 @@ "condition": {"type": "LessThan", "value": 50}, }, ], - "meta": {"name": "n", "required_provider": "stackguardian/terraform_plan", "version": "v1", "custom": True}, + "meta": {"name": "n", "provider": "stackguardian/terraform_plan", "version": "v1", "custom": True}, } CANONICAL_TEXT = """{ "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "name": "n", "custom": true }, @@ -89,7 +89,7 @@ def test_canonical_orders_keys_and_keeps_unknown_ones_after_known(): out = canonical(SCRAMBLED) assert list(out) == ["meta", "evaluators", "eval_expression"] - assert list(out["meta"]) == ["version", "required_provider", "name", "custom"] + assert list(out["meta"]) == ["version", "provider", "name", "custom"] assert list(out["evaluators"][0]) == ["id", "description", "provider_args", "condition"] assert list(out["evaluators"][0]["provider_args"])[0] == "operation_type" assert list(out["evaluators"][0]["condition"]) == ["type", "value", "error_tolerance"] diff --git a/tests/core/test_provider_metadata.py b/tests/core/test_provider_metadata.py new file mode 100644 index 00000000..bb4629bd --- /dev/null +++ b/tests/core/test_provider_metadata.py @@ -0,0 +1,59 @@ +"""Provider metadata spelling and backward compatibility.""" + +from pytest import mark + +from tirith.core.core import start_policy_evaluation_from_dict + + +def _policy(meta): + return { + "meta": {"version": "v1", **meta}, + "evaluators": [ + { + "id": "check0", + "provider_args": {"operation_type": "get_value", "key_path": "a"}, + "condition": {"type": "Equals", "value": 1}, + } + ], + "eval_expression": "check0", + } + + +@mark.passing +def test_meta_provider_is_accepted(): + result = start_policy_evaluation_from_dict(_policy({"provider": "stackguardian/json"}), {"a": 1}) + + assert result["final_result"] is True + assert result["meta"]["required_provider"] == "stackguardian/json" + + +@mark.passing +def test_legacy_meta_required_provider_still_works(): + result = start_policy_evaluation_from_dict(_policy({"required_provider": "stackguardian/json"}), {"a": 1}) + + assert result["final_result"] is True + assert result["meta"]["required_provider"] == "stackguardian/json" + + +@mark.passing +def test_meta_provider_takes_precedence_when_both_are_present(): + result = start_policy_evaluation_from_dict( + _policy( + { + "provider": "stackguardian/json", + "required_provider": "stackguardian/does-not-exist", + } + ), + {"a": 1}, + ) + + assert result["final_result"] is True + assert result["meta"]["required_provider"] == "stackguardian/json" + + +@mark.passing +def test_result_metadata_is_identical_for_both_input_spellings(): + canonical = start_policy_evaluation_from_dict(_policy({"provider": "stackguardian/json"}), {"a": 1}) + legacy = start_policy_evaluation_from_dict(_policy({"required_provider": "stackguardian/json"}), {"a": 1}) + + assert canonical["meta"] == legacy["meta"] diff --git a/tests/docs/test_provider_metadata.mjs b/tests/docs/test_provider_metadata.mjs new file mode 100644 index 00000000..db76e6e4 --- /dev/null +++ b/tests/docs/test_provider_metadata.mjs @@ -0,0 +1,41 @@ +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; +import test from 'node:test'; + +const source = await readFile(new URL('../../documentation/src/data/tirithLite.js', import.meta.url), 'utf8'); +const {evaluatePolicy} = await import(`data:text/javascript;base64,${Buffer.from(source).toString('base64')}`); +const example = new URL('../../src/tirith/tui/examples/01-required-tags/', import.meta.url); +const policy = JSON.parse(await readFile(new URL('policy.json', example), 'utf8')); +const input = await readFile(new URL('input.json', example), 'utf8'); +const provider = policy.meta.provider; + +function evaluate(meta) { + return evaluatePolicy(JSON.stringify({...policy, meta}), input); +} + +test('the browser reads Terraform resources with meta.provider', () => { + const result = evaluate(policy.meta); + assert.equal(result.fatal, undefined); + assert.equal(result.document.meta.required_provider, provider); + assert.equal(Object.hasOwn(result.document.meta, 'provider'), false); + assert.equal(result.document.evaluators[0].result[0].meta.address, 'aws_instance.web'); + assert.equal(result.document.evaluators[0].result[0].passed, true); +}); + +test('legacy and canonical spellings produce identical browser results', () => { + const {provider: ignored, ...meta} = policy.meta; + assert.deepEqual(evaluate({...meta, required_provider: provider}), evaluate(policy.meta)); +}); + +test('meta.provider wins over a conflicting legacy provider', () => { + assert.deepEqual( + evaluate({...policy.meta, required_provider: 'stackguardian/json'}), + evaluate(policy.meta), + ); +}); + +test('an invalid canonical provider cannot fall back to the legacy provider', () => { + const result = evaluate({...policy.meta, provider: 'stackguardian/unknown', required_provider: provider}); + assert.equal(result.document, null); + assert.match(result.fatal, /stackguardian\/unknown/); +}); diff --git a/tests/providers/infracost/policy.json b/tests/providers/infracost/policy.json index c9f315ec..65ffe590 100644 --- a/tests/providers/infracost/policy.json +++ b/tests/providers/infracost/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/infracost" + "provider": "stackguardian/infracost" }, "evaluators": [ { diff --git a/tests/providers/infracost/policy_redshift.json b/tests/providers/infracost/policy_redshift.json index 44c5439e..013eb380 100644 --- a/tests/providers/infracost/policy_redshift.json +++ b/tests/providers/infracost/policy_redshift.json @@ -1,6 +1,6 @@ { "meta": { - "required_provider": "stackguardian/infracost", + "provider": "stackguardian/infracost", "version": "v1" }, "evaluators": [ diff --git a/tests/providers/infracost/policy_s3.json b/tests/providers/infracost/policy_s3.json index 9f8bca4d..d8a2d711 100644 --- a/tests/providers/infracost/policy_s3.json +++ b/tests/providers/infracost/policy_s3.json @@ -18,7 +18,7 @@ } ], "meta": { - "required_provider": "stackguardian/infracost", + "provider": "stackguardian/infracost", "version": "v1" }, "eval_expression": "monthly_cost_below_30" diff --git a/tests/providers/json/policy.json b/tests/providers/json/policy.json index 90ae1781..ac1c14bc 100644 --- a/tests/providers/json/policy.json +++ b/tests/providers/json/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { diff --git a/tests/providers/json/policy_mixed_queries.json b/tests/providers/json/policy_mixed_queries.json index e28679a8..477186ed 100644 --- a/tests/providers/json/policy_mixed_queries.json +++ b/tests/providers/json/policy_mixed_queries.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/json", + "provider": "stackguardian/json", "policy_name": "Mixed Query Language Example", "policy_description": "Demonstrates using both JMESPath and jq_query in the same policy" }, diff --git a/tests/providers/json/policy_playbook.json b/tests/providers/json/policy_playbook.json index 1dc04c58..31b559d7 100644 --- a/tests/providers/json/policy_playbook.json +++ b/tests/providers/json/policy_playbook.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/json" + "provider": "stackguardian/json" }, "evaluators": [ { diff --git a/tests/providers/kubernetes/policy.json b/tests/providers/kubernetes/policy.json index b0585585..a13414f4 100644 --- a/tests/providers/kubernetes/policy.json +++ b/tests/providers/kubernetes/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/kubernetes" + "provider": "stackguardian/kubernetes" }, "evaluators": [ { diff --git a/tests/providers/sg_workflow/policy.json b/tests/providers/sg_workflow/policy.json index df2a82ef..94744dc4 100644 --- a/tests/providers/sg_workflow/policy.json +++ b/tests/providers/sg_workflow/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/sg_workflow" + "provider": "stackguardian/sg_workflow" }, "evaluators": [ { diff --git a/tests/providers/terraform_plan/fixtures/at_least_one_aws_s3_bucket_int_config_refers_to_s3_bucket.tirith.json b/tests/providers/terraform_plan/fixtures/at_least_one_aws_s3_bucket_int_config_refers_to_s3_bucket.tirith.json index ee67a8a3..0e14fde7 100644 --- a/tests/providers/terraform_plan/fixtures/at_least_one_aws_s3_bucket_int_config_refers_to_s3_bucket.tirith.json +++ b/tests/providers/terraform_plan/fixtures/at_least_one_aws_s3_bucket_int_config_refers_to_s3_bucket.tirith.json @@ -1,6 +1,6 @@ { "meta": { - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "version": "v1" }, "evaluators": [ diff --git a/tests/providers/terraform_plan/fixtures/elb_references_to_secroup.tirith.json b/tests/providers/terraform_plan/fixtures/elb_references_to_secroup.tirith.json index 05ab5a03..70536459 100644 --- a/tests/providers/terraform_plan/fixtures/elb_references_to_secroup.tirith.json +++ b/tests/providers/terraform_plan/fixtures/elb_references_to_secroup.tirith.json @@ -1,6 +1,6 @@ { "meta": { - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "version": "v1" }, "evaluators": [ diff --git a/tests/providers/terraform_plan/fixtures/fail_s3_referenced_by_to.tirith.json b/tests/providers/terraform_plan/fixtures/fail_s3_referenced_by_to.tirith.json index 02e365e0..4a0360ea 100644 --- a/tests/providers/terraform_plan/fixtures/fail_s3_referenced_by_to.tirith.json +++ b/tests/providers/terraform_plan/fixtures/fail_s3_referenced_by_to.tirith.json @@ -1,6 +1,6 @@ { "meta": { - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "version": "v1" }, "evaluators": [ diff --git a/tests/providers/terraform_plan/fixtures/policy.json b/tests/providers/terraform_plan/fixtures/policy.json index 3f6a0c3b..981d4831 100644 --- a/tests/providers/terraform_plan/fixtures/policy.json +++ b/tests/providers/terraform_plan/fixtures/policy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { diff --git a/tests/providers/terraform_plan/fixtures/policy_aws_instance.json b/tests/providers/terraform_plan/fixtures/policy_aws_instance.json index 0d1e0151..5f84fe05 100644 --- a/tests/providers/terraform_plan/fixtures/policy_aws_instance.json +++ b/tests/providers/terraform_plan/fixtures/policy_aws_instance.json @@ -58,7 +58,7 @@ } ], "meta": { - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "version": "v1" }, "eval_expression": "eval-id-1 && eval-id-2 && !eval-id-3 && !eval-id-4" diff --git a/tests/providers/terraform_plan/fixtures/policy_aws_instance_ebs.json b/tests/providers/terraform_plan/fixtures/policy_aws_instance_ebs.json index c985e47a..6c43e233 100644 --- a/tests/providers/terraform_plan/fixtures/policy_aws_instance_ebs.json +++ b/tests/providers/terraform_plan/fixtures/policy_aws_instance_ebs.json @@ -30,7 +30,7 @@ } ], "meta": { - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "version": "v1" }, "eval_expression": "eval-id-1 && eval-id-2" diff --git a/tests/providers/terraform_plan/fixtures/policy_costcenter_tags.json b/tests/providers/terraform_plan/fixtures/policy_costcenter_tags.json index 69758340..b5b19e48 100644 --- a/tests/providers/terraform_plan/fixtures/policy_costcenter_tags.json +++ b/tests/providers/terraform_plan/fixtures/policy_costcenter_tags.json @@ -16,7 +16,7 @@ } ], "meta": { - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "version": "v1" }, "eval_expression": "eval-id-1 " diff --git a/tests/providers/terraform_plan/fixtures/policy_destroy_approval.json b/tests/providers/terraform_plan/fixtures/policy_destroy_approval.json index 03393bc7..a22e7eef 100644 --- a/tests/providers/terraform_plan/fixtures/policy_destroy_approval.json +++ b/tests/providers/terraform_plan/fixtures/policy_destroy_approval.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { diff --git a/tests/providers/terraform_plan/fixtures/policy_ec2_depends_on_s3.json b/tests/providers/terraform_plan/fixtures/policy_ec2_depends_on_s3.json index 67458670..9bb4f2c5 100644 --- a/tests/providers/terraform_plan/fixtures/policy_ec2_depends_on_s3.json +++ b/tests/providers/terraform_plan/fixtures/policy_ec2_depends_on_s3.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { diff --git a/tests/providers/terraform_plan/fixtures/policy_elb_secgroup_reference.json b/tests/providers/terraform_plan/fixtures/policy_elb_secgroup_reference.json index 29707849..0ae3d214 100644 --- a/tests/providers/terraform_plan/fixtures/policy_elb_secgroup_reference.json +++ b/tests/providers/terraform_plan/fixtures/policy_elb_secgroup_reference.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { diff --git a/tests/providers/terraform_plan/fixtures/policy_exclude_resource_types.json b/tests/providers/terraform_plan/fixtures/policy_exclude_resource_types.json index 39c1eb05..1d161a13 100644 --- a/tests/providers/terraform_plan/fixtures/policy_exclude_resource_types.json +++ b/tests/providers/terraform_plan/fixtures/policy_exclude_resource_types.json @@ -20,7 +20,7 @@ ], "meta": { - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "version": "v1" }, "eval_expression": "eval-id-1" diff --git a/tests/providers/terraform_plan/fixtures/policy_multiple_resource_tag_check.json b/tests/providers/terraform_plan/fixtures/policy_multiple_resource_tag_check.json index 7cfc8fb8..d7a87b89 100644 --- a/tests/providers/terraform_plan/fixtures/policy_multiple_resource_tag_check.json +++ b/tests/providers/terraform_plan/fixtures/policy_multiple_resource_tag_check.json @@ -15,7 +15,7 @@ } ], "meta": { - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "version": "v1" }, "eval_expression": "eval-id-1" diff --git a/tests/providers/terraform_plan/fixtures/policy_s3_destroy.json b/tests/providers/terraform_plan/fixtures/policy_s3_destroy.json index a0a9dff3..253b3a10 100644 --- a/tests/providers/terraform_plan/fixtures/policy_s3_destroy.json +++ b/tests/providers/terraform_plan/fixtures/policy_s3_destroy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { diff --git a/tests/providers/terraform_plan/fixtures/policy_star_restype_should_skip.json b/tests/providers/terraform_plan/fixtures/policy_star_restype_should_skip.json index af0e372c..58d83d94 100644 --- a/tests/providers/terraform_plan/fixtures/policy_star_restype_should_skip.json +++ b/tests/providers/terraform_plan/fixtures/policy_star_restype_should_skip.json @@ -16,7 +16,7 @@ } ], "meta": { - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "version": "v1" }, "eval_expression": "eval-id-1" diff --git a/tests/providers/terraform_plan/fixtures/policy_vnet_destroy.json b/tests/providers/terraform_plan/fixtures/policy_vnet_destroy.json index ca9e1e59..de927d07 100644 --- a/tests/providers/terraform_plan/fixtures/policy_vnet_destroy.json +++ b/tests/providers/terraform_plan/fixtures/policy_vnet_destroy.json @@ -1,7 +1,7 @@ { "meta": { "version": "v1", - "required_provider": "stackguardian/terraform_plan" + "provider": "stackguardian/terraform_plan" }, "evaluators": [ { diff --git a/tests/providers/terraform_plan/fixtures/policy_vnet_destroy_new.json b/tests/providers/terraform_plan/fixtures/policy_vnet_destroy_new.json index f3dbb8d0..d62dd746 100644 --- a/tests/providers/terraform_plan/fixtures/policy_vnet_destroy_new.json +++ b/tests/providers/terraform_plan/fixtures/policy_vnet_destroy_new.json @@ -31,7 +31,7 @@ } ], "meta": { - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "version": "v1" }, "eval_expression": "eval-id-1 && eval-id-2" diff --git a/tests/providers/terraform_plan/fixtures/s3_referenced_by_intelligent_tiering_config.tirith.json b/tests/providers/terraform_plan/fixtures/s3_referenced_by_intelligent_tiering_config.tirith.json index b70044d2..8aecbd29 100644 --- a/tests/providers/terraform_plan/fixtures/s3_referenced_by_intelligent_tiering_config.tirith.json +++ b/tests/providers/terraform_plan/fixtures/s3_referenced_by_intelligent_tiering_config.tirith.json @@ -1,6 +1,6 @@ { "meta": { - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "version": "v1" }, "evaluators": [ diff --git a/tests/providers/terraform_plan/fixtures/s3_referenced_by_intelligent_tiering_config_err_tolerance_1.tirith.json b/tests/providers/terraform_plan/fixtures/s3_referenced_by_intelligent_tiering_config_err_tolerance_1.tirith.json index f5e9c345..f62ee186 100644 --- a/tests/providers/terraform_plan/fixtures/s3_referenced_by_intelligent_tiering_config_err_tolerance_1.tirith.json +++ b/tests/providers/terraform_plan/fixtures/s3_referenced_by_intelligent_tiering_config_err_tolerance_1.tirith.json @@ -1,6 +1,6 @@ { "meta": { - "required_provider": "stackguardian/terraform_plan", + "provider": "stackguardian/terraform_plan", "version": "v1" }, "evaluators": [ diff --git a/tests/tui/test_ui_cli.py b/tests/tui/test_ui_cli.py index 9a02de6f..b455d3ca 100644 --- a/tests/tui/test_ui_cli.py +++ b/tests/tui/test_ui_cli.py @@ -126,7 +126,7 @@ def test_a_path_that_is_not_a_dash_is_read_from_disk(): document = ui_cli._load(os.path.join(EXAMPLE, "policy.json"), "--policy") - assert document["meta"]["required_provider"] == "stackguardian/terraform_plan" + assert document["meta"]["provider"] == "stackguardian/terraform_plan" @mark.passing diff --git a/tests/tui/test_validate.py b/tests/tui/test_validate.py index 1f58d8d0..1925ab61 100644 --- a/tests/tui/test_validate.py +++ b/tests/tui/test_validate.py @@ -23,7 +23,7 @@ def _valid_policy(): return { - "meta": {"version": "v1", "required_provider": "stackguardian/json"}, + "meta": {"version": "v1", "provider": "stackguardian/json"}, "evaluators": [ { "id": "check0", @@ -108,6 +108,22 @@ def test_accepts_a_minimal_policy(): assert validate.check_policy(_valid_policy()) == [] +@mark.passing +def test_accepts_legacy_required_provider(): + policy = _valid_policy() + policy["meta"]["required_provider"] = policy["meta"].pop("provider") + + assert validate.check_policy(policy) == [] + + +@mark.passing +def test_provider_takes_precedence_over_legacy_required_provider(): + policy = _valid_policy() + policy["meta"]["required_provider"] = "stackguardian/nope" + + assert validate.check_policy(policy) == [] + + @mark.passing def test_missing_meta_is_an_error_not_an_exception(): """ @@ -128,7 +144,7 @@ def test_non_dict_policy_is_reported(): @mark.passing def test_unknown_provider_is_reported(): policy = _valid_policy() - policy["meta"]["required_provider"] = "stackguardian/nope" + policy["meta"]["provider"] = "stackguardian/nope" assert any("Unknown provider" in f.message for f in _errors(policy)) @@ -445,6 +461,21 @@ def test_an_unhashable_provider_is_reported_not_raised(bad): Reachable by an ordinary mistake: the neighbouring resource_type argument really is a list. """ policy = _valid_policy() + policy["meta"]["provider"] = bad + + assert any("Must be a string" in f.message for f in _errors(policy)) + + +@mark.passing +@mark.parametrize("bad", [["stackguardian/json"], {"name": "x"}], ids=["list", "dict"]) +def test_an_unhashable_legacy_required_provider_is_reported_not_raised(bad): + """ + The legacy required_provider alias must remain safe when it is the only + provider key. Invalid unhashable values should be reported instead of + escaping as TypeError. + """ + policy = _valid_policy() + del policy["meta"]["provider"] policy["meta"]["required_provider"] = bad assert any("Must be a string" in f.message for f in _errors(policy))