Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.",
Expand Down
Original file line number Diff line number Diff line change
@@ -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.",
Expand Down
Original file line number Diff line number Diff line change
@@ -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.",
Expand Down
Original file line number Diff line number Diff line change
@@ -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.",
Expand Down
Original file line number Diff line number Diff line change
@@ -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.",
Expand Down
Original file line number Diff line number Diff line change
@@ -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['<tagName>'] exists; Tirith tests the key of the tags map.",
Expand Down
Original file line number Diff line number Diff line change
@@ -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.",
Expand Down
Original file line number Diff line number Diff line change
@@ -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.",
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/tirith-policies/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*,
Expand All @@ -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": [{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"meta": {
"version": "v1",
"required_provider": "stackguardian/terraform_plan",
"provider": "stackguardian/terraform_plan",
"name": "Every resource carries a costcenter tag"
},
"evaluators": [
Expand Down
8 changes: 4 additions & 4 deletions .claude/skills/tirith-policies/reference/other-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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`
Expand All @@ -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`
Expand All @@ -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.
4 changes: 2 additions & 2 deletions .claude/skills/tirith-policies/reference/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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` |
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/tirith-policies/reference/variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": ["*"]},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"meta": {
"version": "v1",
"required_provider": "stackguardian/terraform_plan",
"provider": "stackguardian/terraform_plan",
"name": "S3 bucket names follow the acme-<env>-<purpose> 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."
},
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/tirith-standards/reference/standards.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion .cursor/rules/tirith-policies.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/build_docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
30 changes: 18 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -529,7 +535,7 @@ VPC and EC2 instance policy
```json
{
"meta": {
"required_provider": "stackguardian/terraform_plan",
"provider": "stackguardian/terraform_plan",
"version": "v1"
},
"evaluators": [
Expand Down Expand Up @@ -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": [
Expand Down Expand Up @@ -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": [
{
Expand All @@ -621,7 +627,7 @@ Policy:
{
"meta": {
"version": "v1",
"required_provider": "stackguardian/terraform_plan"
"provider": "stackguardian/terraform_plan"
},
"evaluators": [
{
Expand Down Expand Up @@ -860,7 +866,7 @@ Cost control policy
```json
{
"meta": {
"required_provider": "stackguardian/infracost",
"provider": "stackguardian/infracost",
"version": "v1"
},
"evaluators": [
Expand All @@ -886,7 +892,7 @@ Policy:
{
"meta": {
"version": "v1",
"required_provider": "stackguardian/infracost"
"provider": "stackguardian/infracost"
},
"evaluators": [
{
Expand Down Expand Up @@ -1008,7 +1014,7 @@ JSON Output:
```json
{
"meta": {
"required_provider": "stackguardian/sg_workflow",
"provider": "stackguardian/sg_workflow",
"version": "v1"
},
"evaluators": [
Expand Down Expand Up @@ -1036,7 +1042,7 @@ Policy:
{
"meta": {
"version": "v1",
"required_provider": "stackguardian/sg_workflow"
"provider": "stackguardian/sg_workflow"
},
"evaluators": [
{
Expand Down Expand Up @@ -1162,7 +1168,7 @@ Example Policy
{
"meta": {
"version": "v1",
"required_provider": "stackguardian/json"
"provider": "stackguardian/json"
},
"evaluators": [
{
Expand Down Expand Up @@ -1367,7 +1373,7 @@ Kubernetes (using Kubernetes provider)
{
"meta": {
"version": "v1",
"required_provider": "stackguardian/kubernetes"
"provider": "stackguardian/kubernetes"
},
"evaluators": [
{
Expand Down Expand Up @@ -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": [
{
Expand Down
4 changes: 2 additions & 2 deletions docs/lint.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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.
Expand Down
Loading
Loading