@intentius/chant 0.57.0 → 0.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/catalog.d.ts +51 -1
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/core.d.ts +36 -1
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +26 -9
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/lineage.d.ts +10 -0
- package/dist/audit/lineage.d.ts.map +1 -0
- package/dist/audit/prior-art.d.ts +385 -0
- package/dist/audit/prior-art.d.ts.map +1 -0
- package/dist/audit/proof.d.ts +16 -0
- package/dist/audit/proof.d.ts.map +1 -1
- package/dist/audit/report-html.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +8 -1
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/report.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts +3 -0
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/audit/secrets.d.ts +15 -0
- package/dist/audit/secrets.d.ts.map +1 -1
- package/dist/audit/terraform-state.d.ts +46 -0
- package/dist/audit/terraform-state.d.ts.map +1 -0
- package/dist/build.d.ts +17 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/claimed-fields.d.ts +161 -0
- package/dist/claimed-fields.d.ts.map +1 -0
- package/dist/cli/build-params-cli.d.ts +2 -2
- package/dist/cli/command-group.d.ts +14 -0
- package/dist/cli/command-group.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +17 -1
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/carve-bridge.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-docs.d.ts +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +16 -6
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/codegen.d.ts +1 -0
- package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/project.d.ts +7 -0
- package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts +13 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts +2 -0
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +82 -16
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts +49 -42
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts +12 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/op-tools.d.ts +16 -1
- package/dist/cli/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/resources/context.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +22 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +40 -9
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/generate-post-synth-barrel.d.ts +1 -1
- package/dist/components/auto-release.d.ts +10 -20
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/capability.d.ts +4 -4
- package/dist/components/cli-support.d.ts +24 -22
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +21 -6
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver-output.d.ts.map +1 -1
- package/dist/components/driver.d.ts +73 -37
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/manifest-persistence.d.ts +1 -9
- package/dist/components/manifest-persistence.d.ts.map +1 -1
- package/dist/components/run-progress.d.ts +6 -6
- package/dist/components/run-progress.d.ts.map +1 -1
- package/dist/components/verbs/run-agent.d.ts +10 -22
- package/dist/components/verbs/run-agent.d.ts.map +1 -1
- package/dist/components/verbs/verify.d.ts +2 -2
- package/dist/deep-observation.d.ts +17 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/detectLexicon.d.ts +13 -0
- package/dist/detectLexicon.d.ts.map +1 -1
- package/dist/discovery/files.d.ts +4 -4
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +33 -0
- package/dist/discovery/param-deps.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
- package/dist/discovery/sandbox/config-wire.d.ts +2 -2
- package/dist/fold/foldable-helpers.d.ts +5 -1
- package/dist/fold/foldable-helpers.d.ts.map +1 -1
- package/dist/fold-provenance.d.ts +236 -0
- package/dist/fold-provenance.d.ts.map +1 -0
- package/dist/graph-ops.d.ts +1 -1
- package/dist/held-elsewhere.d.ts +85 -0
- package/dist/held-elsewhere.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/kubectl-context.d.ts +3 -3
- package/dist/lexicon-config.d.ts +4 -4
- package/dist/lexicon-plugin-helpers.d.ts +1 -1
- package/dist/lexicon.d.ts +273 -16
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +13 -0
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/deep-diff.d.ts +133 -6
- package/dist/lifecycle/deep-diff.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +79 -3
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts +2 -2
- package/dist/lifecycle/run-ledger.d.ts +53 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -0
- package/dist/lifecycle/symptoms.d.ts +3 -3
- package/dist/lifecycle/symptoms.d.ts.map +1 -1
- package/dist/lint/config.d.ts +81 -2
- package/dist/lint/config.d.ts.map +1 -1
- package/dist/lint/post-synth.d.ts +55 -2
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts +1 -1
- package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts.map +1 -1
- package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts +1 -1
- package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts.map +1 -1
- package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts +44 -0
- package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts.map +1 -0
- package/dist/lint/rules/comp/index.d.ts +1 -1
- package/dist/lint/rules/comp/index.d.ts.map +1 -1
- package/dist/lint/rules/op/index.d.ts +16 -0
- package/dist/lint/rules/op/index.d.ts.map +1 -0
- package/dist/lint/rules/op/ops012-activity-contract.d.ts +27 -0
- package/dist/lint/rules/op/ops012-activity-contract.d.ts.map +1 -0
- package/dist/lint/rules/op/ops013-step-output-ref.d.ts +61 -0
- package/dist/lint/rules/op/ops013-step-output-ref.d.ts.map +1 -0
- package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts +55 -0
- package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -0
- package/dist/lint/rules/op/support.d.ts +12 -0
- package/dist/lint/rules/op/support.d.ts.map +1 -0
- package/dist/lint/suppressions.d.ts +108 -0
- package/dist/lint/suppressions.d.ts.map +1 -0
- package/dist/live-endpoint.d.ts +1 -1
- package/dist/op/activities/activity-contracts.d.ts +54 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -0
- package/dist/op/activities/apply.d.ts +262 -0
- package/dist/op/activities/apply.d.ts.map +1 -0
- package/dist/op/activities/build.d.ts +13 -0
- package/dist/op/activities/build.d.ts.map +1 -0
- package/dist/op/activities/converge.d.ts +110 -0
- package/dist/op/activities/converge.d.ts.map +1 -0
- package/dist/op/activities/env-teardown.d.ts +76 -0
- package/dist/op/activities/env-teardown.d.ts.map +1 -0
- package/dist/op/activities/guard-validate.d.ts +64 -0
- package/dist/op/activities/guard-validate.d.ts.map +1 -0
- package/dist/op/activities/http-check.d.ts +34 -0
- package/dist/op/activities/http-check.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +31 -0
- package/dist/op/activities/index.d.ts.map +1 -0
- package/dist/op/activities/lexicon-upgrade.d.ts +182 -0
- package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -0
- package/dist/op/activities/lifecycle.d.ts +42 -0
- package/dist/op/activities/lifecycle.d.ts.map +1 -0
- package/dist/op/activities/pipeline-audit.d.ts +84 -0
- package/dist/op/activities/pipeline-audit.d.ts.map +1 -0
- package/dist/op/activities/policy.d.ts +17 -0
- package/dist/op/activities/policy.d.ts.map +1 -0
- package/dist/op/activities/reconcile.d.ts +149 -0
- package/dist/op/activities/reconcile.d.ts.map +1 -0
- package/dist/op/activities/shell.d.ts +13 -0
- package/dist/op/activities/shell.d.ts.map +1 -0
- package/dist/op/activities/teardown.d.ts +10 -0
- package/dist/op/activities/teardown.d.ts.map +1 -0
- package/dist/op/activities/util.d.ts +7 -0
- package/dist/op/activities/util.d.ts.map +1 -0
- package/dist/op/activities/wait.d.ts +28 -0
- package/dist/op/activities/wait.d.ts.map +1 -0
- package/dist/op/activities/workflow-audit.d.ts +92 -0
- package/dist/op/activities/workflow-audit.d.ts.map +1 -0
- package/dist/op/activity-contract-registry.d.ts +76 -0
- package/dist/op/activity-contract-registry.d.ts.map +1 -0
- package/dist/op/activity-contract.d.ts +25 -18
- package/dist/op/activity-contract.d.ts.map +1 -1
- package/dist/op/activity-failure.d.ts +28 -0
- package/dist/op/activity-failure.d.ts.map +1 -0
- package/dist/op/activity-profiles.d.ts +118 -0
- package/dist/op/activity-profiles.d.ts.map +1 -0
- package/dist/op/activity-registry.d.ts +22 -38
- package/dist/op/activity-registry.d.ts.map +1 -1
- package/dist/op/activity-runtime.d.ts +3 -14
- package/dist/op/activity-runtime.d.ts.map +1 -1
- package/dist/op/builders.d.ts +85 -30
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/change-signal.d.ts +91 -0
- package/dist/op/change-signal.d.ts.map +1 -0
- package/dist/op/composites/apply-op.d.ts +118 -0
- package/dist/op/composites/apply-op.d.ts.map +1 -0
- package/dist/op/composites/converge-op.d.ts +120 -0
- package/dist/op/composites/converge-op.d.ts.map +1 -0
- package/dist/op/composites/index.d.ts +26 -0
- package/dist/op/composites/index.d.ts.map +1 -0
- package/dist/op/composites/lexicon-upgrade-op.d.ts +78 -0
- package/dist/op/composites/lexicon-upgrade-op.d.ts.map +1 -0
- package/dist/op/composites/pipeline-audit-op.d.ts +50 -0
- package/dist/op/composites/pipeline-audit-op.d.ts.map +1 -0
- package/dist/op/composites/reconcile-op.d.ts +57 -0
- package/dist/op/composites/reconcile-op.d.ts.map +1 -0
- package/dist/op/composites/watch-op.d.ts +60 -0
- package/dist/op/composites/watch-op.d.ts.map +1 -0
- package/dist/op/composites/workflow-audit-op.d.ts +57 -0
- package/dist/op/composites/workflow-audit-op.d.ts.map +1 -0
- package/dist/op/converge-rule.d.ts +12 -10
- package/dist/op/converge-rule.d.ts.map +1 -1
- package/dist/op/cron.d.ts +51 -0
- package/dist/op/cron.d.ts.map +1 -0
- package/dist/op/discover.d.ts +9 -0
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/duration.d.ts +11 -0
- package/dist/op/duration.d.ts.map +1 -0
- package/dist/op/emulator-lifecycle.d.ts.map +1 -1
- package/dist/op/gate-name.d.ts +40 -0
- package/dist/op/gate-name.d.ts.map +1 -0
- package/dist/op/gate-summary.d.ts +43 -0
- package/dist/op/gate-summary.d.ts.map +1 -0
- package/dist/op/gate.d.ts +90 -0
- package/dist/op/gate.d.ts.map +1 -0
- package/dist/op/generate-pipeline.d.ts +9 -0
- package/dist/op/generate-pipeline.d.ts.map +1 -1
- package/dist/op/index.d.ts +29 -9
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +114 -28
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-output.d.ts +11 -1
- package/dist/op/local-output.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +185 -0
- package/dist/op/op-ir.d.ts.map +1 -0
- package/dist/op/op-verb-class.d.ts +10 -2
- package/dist/op/op-verb-class.d.ts.map +1 -1
- package/dist/op/operator.d.ts +93 -5
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/receipt-store.d.ts +1 -1
- package/dist/op/receipt-store.d.ts.map +1 -1
- package/dist/op/resource.d.ts +15 -1
- package/dist/op/resource.d.ts.map +1 -1
- package/dist/op/runtime.d.ts +200 -0
- package/dist/op/runtime.d.ts.map +1 -0
- package/dist/op/runtimes/local.d.ts +29 -0
- package/dist/op/runtimes/local.d.ts.map +1 -0
- package/dist/op/step-output-ref.d.ts +15 -13
- package/dist/op/step-output-ref.d.ts.map +1 -1
- package/dist/op/types.d.ts +96 -29
- package/dist/op/types.d.ts.map +1 -1
- package/dist/ownership.d.ts +1 -1
- package/dist/provenance.d.ts +21 -0
- package/dist/provenance.d.ts.map +1 -1
- package/dist/serializer-walker.d.ts.map +1 -1
- package/dist/terraform/bridge.d.ts +26 -7
- package/dist/terraform/bridge.d.ts.map +1 -1
- package/dist/terraform/carve-provider.d.ts +28 -2
- package/dist/terraform/carve-provider.d.ts.map +1 -1
- package/dist/terraform/data-source-shape.d.ts +77 -0
- package/dist/terraform/data-source-shape.d.ts.map +1 -0
- package/dist/terraform/graph.d.ts.map +1 -1
- package/dist/terraform/providers/kubernetes.d.ts +5 -0
- package/dist/terraform/providers/kubernetes.d.ts.map +1 -1
- package/dist/terraform/tier-map.d.ts +16 -4
- package/dist/terraform/tier-map.d.ts.map +1 -1
- package/dist/terraform/types.d.ts +8 -0
- package/dist/terraform/types.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/audit/catalog.test.ts +79 -3
- package/src/audit/catalog.ts +111 -13
- package/src/audit/core.test.ts +26 -0
- package/src/audit/core.ts +84 -18
- package/src/audit/discover.test.ts +39 -0
- package/src/audit/discover.ts +202 -18
- package/src/audit/lineage.ts +100 -0
- package/src/audit/prior-art.ts +153 -0
- package/src/audit/proof.test.ts +67 -0
- package/src/audit/proof.ts +70 -0
- package/src/audit/report-html.ts +9 -4
- package/src/audit/report-model.ts +16 -2
- package/src/audit/report.test.ts +33 -0
- package/src/audit/report.ts +5 -3
- package/src/audit/rules-doc.test.ts +34 -2
- package/src/audit/rules-doc.ts +82 -9
- package/src/audit/secrets.ts +15 -3
- package/src/audit/terraform-state.test.ts +144 -0
- package/src/audit/terraform-state.ts +131 -0
- package/src/build.ts +31 -1
- package/src/claimed-fields.test.ts +145 -0
- package/src/claimed-fields.ts +208 -0
- package/src/cli/build-params-cli.ts +2 -2
- package/src/cli/command-group.ts +14 -0
- package/src/cli/commands/__fixtures__/audit-terraform/infra/.terraform/providers/lock.txt +1 -0
- package/src/cli/commands/__fixtures__/audit-terraform/infra/main.tf +1 -0
- package/src/cli/commands/__fixtures__/audit-terraform/infra/modules/foo/main.tf +1 -0
- package/src/cli/commands/__fixtures__/audit-terraform/infra/variables.tf +3 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +13 -4
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs-cli.ts +4 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +1 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/package-cli.ts +21 -0
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +1 -0
- package/src/cli/commands/audit.test.ts +191 -11
- package/src/cli/commands/audit.ts +78 -28
- package/src/cli/commands/build.test.ts +330 -9
- package/src/cli/commands/build.ts +170 -24
- package/src/cli/commands/carve-bridge.test.ts +237 -14
- package/src/cli/commands/carve-bridge.ts +21 -17
- package/src/cli/commands/carve-emit-k8s.test.ts +17 -10
- package/src/cli/commands/check-lexicon-docs.ts +1 -1
- package/src/cli/commands/check-lexicon-examples.test.ts +29 -1
- package/src/cli/commands/check-lexicon-examples.ts +42 -10
- package/src/cli/commands/init-lexicon/templates/codegen.ts +9 -0
- package/src/cli/commands/init-lexicon/templates/project.ts +44 -4
- package/src/cli/commands/init-lexicon.test.ts +97 -2
- package/src/cli/commands/init-lexicon.ts +4 -2
- package/src/cli/commands/init.test.ts +35 -1
- package/src/cli/commands/init.ts +14 -3
- package/src/cli/commands/lint.test.ts +194 -1
- package/src/cli/commands/lint.ts +205 -7
- package/src/cli/commands/onboard.test.ts +75 -0
- package/src/cli/commands/onboard.ts +54 -23
- package/src/cli/handlers/build.test.ts +81 -1
- package/src/cli/handlers/build.ts +15 -1
- package/src/cli/handlers/graph.test.ts +3 -3
- package/src/cli/handlers/lifecycle.test.ts +195 -12
- package/src/cli/handlers/lifecycle.ts +123 -6
- package/src/cli/handlers/misc.ts +1 -1
- package/src/cli/handlers/operator.test.ts +183 -11
- package/src/cli/handlers/operator.ts +341 -55
- package/src/cli/handlers/run.test.ts +568 -1295
- package/src/cli/handlers/run.ts +446 -912
- package/src/cli/main.test.ts +22 -16
- package/src/cli/main.ts +66 -33
- package/src/cli/mcp/op-tools.ts +147 -88
- package/src/cli/mcp/resource-handlers.ts +10 -37
- package/src/cli/mcp/resources/context.ts +11 -12
- package/src/cli/mcp/server.test.ts +25 -27
- package/src/cli/mcp/server.ts +2 -2
- package/src/cli/plugins.test.ts +68 -2
- package/src/cli/plugins.ts +39 -0
- package/src/cli/registry.ts +40 -9
- package/src/codegen/docs.ts +1 -1
- package/src/codegen/generate-post-synth-barrel.test.ts +1 -1
- package/src/codegen/generate-post-synth-barrel.ts +1 -1
- package/src/components/README.md +4 -3
- package/src/components/__fixtures__/neo4j-fanout.json +1 -1
- package/src/components/auto-release.test.ts +4 -20
- package/src/components/auto-release.ts +17 -42
- package/src/components/capability.ts +4 -4
- package/src/components/cli-support.test.ts +28 -12
- package/src/components/cli-support.ts +60 -47
- package/src/components/component-schema.test.ts +18 -2
- package/src/components/component.schema.json +18 -5
- package/src/components/component.test.ts +2 -2
- package/src/components/component.ts +27 -7
- package/src/components/config-defaults.test.ts +2 -2
- package/src/components/driver-output.ts +14 -2
- package/src/components/driver.test.ts +109 -20
- package/src/components/driver.ts +201 -84
- package/src/components/index.ts +1 -6
- package/src/components/manifest-persistence.test.ts +2 -20
- package/src/components/manifest-persistence.ts +4 -24
- package/src/components/pilots/neo4j-fanout.pilot.ts +3 -3
- package/src/components/pilots/pilots-lint-clean.test.ts +11 -10
- package/src/components/run-progress.ts +6 -6
- package/src/components/verbs/run-agent.test.ts +19 -0
- package/src/components/verbs/run-agent.ts +12 -24
- package/src/components/verbs/verify.ts +2 -2
- package/src/deep-observation.test.ts +75 -0
- package/src/deep-observation.ts +48 -0
- package/src/detectLexicon.ts +18 -1
- package/src/discovery/collect.test.ts +3 -3
- package/src/discovery/files.ts +4 -4
- package/src/discovery/fold-import.test.ts +1 -1
- package/src/discovery/fold-import.ts +105 -2
- package/src/discovery/param-deps.test.ts +112 -1
- package/src/discovery/param-deps.ts +193 -0
- package/src/discovery/sandbox/bundle.ts +15 -1
- package/src/discovery/sandbox/config-wire.test.ts +2 -2
- package/src/discovery/sandbox/config-wire.ts +2 -2
- package/src/fold/fold.test.ts +56 -0
- package/src/fold/foldable-helpers.ts +82 -2
- package/src/fold-provenance-exclusion.test.ts +186 -0
- package/src/fold-provenance.test.ts +495 -0
- package/src/fold-provenance.ts +441 -0
- package/src/graph-ops.test.ts +3 -3
- package/src/graph-ops.ts +1 -1
- package/src/held-elsewhere.test.ts +102 -0
- package/src/held-elsewhere.ts +114 -0
- package/src/index.ts +3 -0
- package/src/kubectl-context.ts +3 -3
- package/src/lexicon-config.test.ts +5 -5
- package/src/lexicon-config.ts +4 -4
- package/src/lexicon-plugin-helpers.ts +1 -1
- package/src/lexicon.ts +296 -16
- package/src/lifecycle/change-set.test.ts +49 -0
- package/src/lifecycle/change-set.ts +67 -0
- package/src/lifecycle/converge-ledger.ts +1 -1
- package/src/lifecycle/deep-diff.test.ts +267 -14
- package/src/lifecycle/deep-diff.ts +217 -14
- package/src/lifecycle/deep-observe.test.ts +3 -1
- package/src/lifecycle/deep-observe.ts +16 -6
- package/src/lifecycle/gate-ledger.test.ts +74 -2
- package/src/lifecycle/gate-ledger.ts +154 -36
- package/src/lifecycle/index.ts +1 -0
- package/src/lifecycle/release-ledger.ts +2 -2
- package/src/lifecycle/run-ledger.test.ts +268 -0
- package/src/lifecycle/run-ledger.ts +201 -0
- package/src/lifecycle/symptoms.ts +3 -3
- package/src/lint/config.test.ts +82 -1
- package/src/lint/config.ts +112 -3
- package/src/lint/pipeline-change-gate.test.ts +2 -2
- package/src/lint/policy.test.ts +1 -1
- package/src/lint/post-synth.test.ts +31 -0
- package/src/lint/post-synth.ts +54 -1
- package/src/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.ts +2 -2
- package/src/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.ts +1 -1
- package/src/lint/rules/comp/comp.test.ts +27 -1
- package/src/lint/rules/comp/comp004-gate-needs-durable-runtime.ts +76 -0
- package/src/lint/rules/comp/index.ts +3 -3
- package/src/lint/rules/op/index.ts +23 -0
- package/src/lint/rules/op/ops012-activity-contract.test.ts +161 -0
- package/src/lint/rules/op/ops012-activity-contract.ts +64 -0
- package/src/lint/rules/op/ops013-step-output-ref.test.ts +237 -0
- package/src/lint/rules/op/ops013-step-output-ref.ts +98 -0
- package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +263 -0
- package/src/lint/rules/op/ops014-converge-rule-refusals.ts +179 -0
- package/src/lint/rules/op/support.ts +11 -0
- package/src/lint/suppressions.test.ts +163 -0
- package/src/lint/suppressions.ts +204 -0
- package/src/live-endpoint.ts +1 -1
- package/src/op/activities/activity-contracts.ts +66 -0
- package/src/op/activities/apply.test.ts +567 -0
- package/src/op/activities/apply.ts +662 -0
- package/src/op/activities/build.ts +26 -0
- package/src/op/activities/converge.test.ts +330 -0
- package/src/op/activities/converge.ts +449 -0
- package/src/op/activities/env-teardown.test.ts +187 -0
- package/src/op/activities/env-teardown.ts +171 -0
- package/src/op/activities/guard-validate.test.ts +139 -0
- package/src/op/activities/guard-validate.ts +223 -0
- package/src/op/activities/http-check.test.ts +48 -0
- package/src/op/activities/http-check.ts +55 -0
- package/src/op/activities/index.ts +119 -0
- package/src/op/activities/lexicon-upgrade.test.ts +492 -0
- package/src/op/activities/lexicon-upgrade.ts +673 -0
- package/src/op/activities/lifecycle.ts +85 -0
- package/src/op/activities/pipeline-audit.test.ts +67 -0
- package/src/op/activities/pipeline-audit.ts +201 -0
- package/src/op/activities/policy.test.ts +19 -0
- package/src/op/activities/policy.ts +37 -0
- package/src/op/activities/reconcile.test.ts +161 -0
- package/src/op/activities/reconcile.ts +354 -0
- package/src/op/activities/shell.ts +26 -0
- package/src/op/activities/teardown.ts +22 -0
- package/src/op/activities/util.ts +6 -0
- package/src/op/activities/wait.ts +69 -0
- package/src/op/activities/workflow-audit.test.ts +90 -0
- package/src/op/activities/workflow-audit.ts +203 -0
- package/src/op/activity-contract-registry.test.ts +98 -0
- package/src/op/activity-contract-registry.ts +114 -0
- package/src/op/activity-contract.test.ts +1 -1
- package/src/op/activity-contract.ts +37 -33
- package/src/op/activity-failure.ts +34 -0
- package/src/op/activity-profiles.test.ts +56 -0
- package/src/op/activity-profiles.ts +116 -0
- package/src/op/activity-registry.test.ts +37 -28
- package/src/op/activity-registry.ts +27 -56
- package/src/op/activity-runtime.ts +3 -47
- package/src/op/builders-exports.test.ts +0 -49
- package/src/op/builders-typed.test.ts +164 -0
- package/src/op/builders.ts +125 -41
- package/src/op/change-signal.test.ts +117 -0
- package/src/op/change-signal.ts +169 -0
- package/src/op/composites/apply-op.ts +218 -0
- package/src/op/composites/composites.test.ts +379 -0
- package/src/op/composites/converge-op.test.ts +168 -0
- package/src/op/composites/converge-op.ts +214 -0
- package/src/op/composites/index.ts +26 -0
- package/src/op/composites/lexicon-upgrade-op.test.ts +68 -0
- package/src/op/composites/lexicon-upgrade-op.ts +134 -0
- package/src/op/composites/pipeline-audit-op.test.ts +25 -0
- package/src/op/composites/pipeline-audit-op.ts +78 -0
- package/src/op/composites/reconcile-op.test.ts +51 -0
- package/src/op/composites/reconcile-op.ts +110 -0
- package/src/op/composites/watch-op.ts +108 -0
- package/src/op/composites/workflow-audit-op.test.ts +25 -0
- package/src/op/composites/workflow-audit-op.ts +88 -0
- package/src/op/converge-rule.ts +12 -10
- package/src/op/cron.test.ts +128 -0
- package/src/op/cron.ts +149 -0
- package/src/op/discover-root.test.ts +105 -1
- package/src/op/discover.test.ts +6 -2
- package/src/op/discover.ts +84 -16
- package/src/op/duration.ts +21 -0
- package/src/op/effect-step.test.ts +29 -14
- package/src/op/emulator-lifecycle.ts +1 -2
- package/src/op/gate-name.test.ts +65 -0
- package/src/op/gate-name.ts +60 -0
- package/src/op/gate-summary.ts +84 -0
- package/src/op/gate.ts +155 -0
- package/src/op/generate-pipeline.test.ts +65 -3
- package/src/op/generate-pipeline.ts +19 -2
- package/src/op/index.ts +48 -8
- package/src/op/local-executor.test.ts +178 -34
- package/src/op/local-executor.ts +371 -102
- package/src/op/local-output.test.ts +95 -11
- package/src/op/local-output.ts +33 -4
- package/src/op/op-ir.test.ts +292 -0
- package/src/op/op-ir.ts +419 -0
- package/src/op/op-verb-class.test.ts +2 -2
- package/src/op/op-verb-class.ts +19 -5
- package/src/op/op.test.ts +28 -7
- package/src/op/operator.test.ts +543 -6
- package/src/op/operator.ts +203 -20
- package/src/op/receipt-store.ts +1 -1
- package/src/op/resource.ts +20 -2
- package/src/op/runtime.ts +203 -0
- package/src/op/runtimes/local.test.ts +181 -0
- package/src/op/runtimes/local.ts +212 -0
- package/src/op/step-output-ref.ts +15 -13
- package/src/op/types.ts +106 -26
- package/src/ownership.ts +1 -1
- package/src/provenance.test.ts +6 -0
- package/src/provenance.ts +21 -1
- package/src/serializer-walker.test.ts +42 -0
- package/src/serializer-walker.ts +21 -2
- package/src/terraform/aws-resources.test.ts +13 -4
- package/src/terraform/bridge.test.ts +22 -9
- package/src/terraform/bridge.ts +89 -28
- package/src/terraform/carve-provider.ts +38 -2
- package/src/terraform/data-source-shape.ts +95 -0
- package/src/terraform/graph.ts +35 -7
- package/src/terraform/providers/kubernetes.ts +48 -2
- package/src/terraform/tier-map.ts +21 -6
- package/src/terraform/types.ts +8 -0
- package/dist/cli/handlers/op-progress.d.ts +0 -90
- package/dist/cli/handlers/op-progress.d.ts.map +0 -1
- package/dist/cli/handlers/run-client.d.ts +0 -127
- package/dist/cli/handlers/run-client.d.ts.map +0 -1
- package/dist/cli/handlers/run-report.d.ts +0 -5
- package/dist/cli/handlers/run-report.d.ts.map +0 -1
- package/dist/components/temporal-codegen-loader.d.ts +0 -27
- package/dist/components/temporal-codegen-loader.d.ts.map +0 -1
- package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts +0 -37
- package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts.map +0 -1
- package/src/cli/handlers/op-progress.test.ts +0 -268
- package/src/cli/handlers/op-progress.ts +0 -248
- package/src/cli/handlers/run-client.test.ts +0 -82
- package/src/cli/handlers/run-client.ts +0 -219
- package/src/cli/handlers/run-report.test.ts +0 -62
- package/src/cli/handlers/run-report.ts +0 -122
- package/src/components/temporal-codegen-loader.ts +0 -41
- package/src/lint/rules/comp/comp004-gate-needs-temporal.ts +0 -66
package/src/audit/proof.test.ts
CHANGED
|
@@ -124,6 +124,73 @@ describe("proveFix — guidance findings are not auto-fixed", () => {
|
|
|
124
124
|
});
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
+
/** A Terraform root with both of #2110's deterministic findings in it. */
|
|
128
|
+
const TF_ROOT = `variable "log_level" {
|
|
129
|
+
type = string
|
|
130
|
+
sensitive = false
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
resource "aws_s3_bucket" "assets" {
|
|
134
|
+
bucket = "\${var.assets_bucket}"
|
|
135
|
+
tags = {
|
|
136
|
+
name = "\${var.project}-assets"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
lifecycle {
|
|
140
|
+
prevent_destroy = false
|
|
141
|
+
create_before_destroy = var.replace_first
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
`;
|
|
145
|
+
|
|
146
|
+
describe("proveFix — TF019, delete a meta-argument set to its default false", () => {
|
|
147
|
+
test("drops both redundant lines and leaves the expression alone", () => {
|
|
148
|
+
const res = proveFix("TF019", TF_ROOT);
|
|
149
|
+
expect(res.applied).toBe(true);
|
|
150
|
+
expect(res.patched).not.toContain("sensitive = false");
|
|
151
|
+
expect(res.patched).not.toContain("prevent_destroy = false");
|
|
152
|
+
expect(res.patched).toContain("create_before_destroy = var.replace_first");
|
|
153
|
+
expect(res.diff).toContain("- sensitive = false");
|
|
154
|
+
expect(res.diff).toContain("- prevent_destroy = false");
|
|
155
|
+
// Deletions only: nothing is added.
|
|
156
|
+
expect((res.diff ?? "").split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++"))).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("a root with no redundant default is a no-op, not a patch", () => {
|
|
160
|
+
const res = proveFix("TF019", 'resource "aws_s3_bucket" "assets" {\n bucket = var.assets_bucket\n}\n');
|
|
161
|
+
expect(res.applied).toBe(false);
|
|
162
|
+
expect(res.reason).toBe("noop");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("a line with a trailing comment is left alone: deleting it would delete the comment", () => {
|
|
166
|
+
const res = proveFix("TF019", " sensitive = false # deliberate, see #123\n");
|
|
167
|
+
expect(res.applied).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("proveFix — TF016, unwrap an interpolation-only value", () => {
|
|
172
|
+
test("unquotes the single interpolation and leaves a real template alone", () => {
|
|
173
|
+
const res = proveFix("TF016", TF_ROOT);
|
|
174
|
+
expect(res.applied).toBe(true);
|
|
175
|
+
expect(res.patched).toContain("bucket = var.assets_bucket");
|
|
176
|
+
expect(res.patched).toContain('name = "${var.project}-assets"');
|
|
177
|
+
expect(res.diff).toContain("+ bucket = var.assets_bucket");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("the two fixes combine into one patch, as a quick-win diff does", () => {
|
|
181
|
+
const first = proveFix("TF016", TF_ROOT);
|
|
182
|
+
const second = proveFix("TF019", first.patched ?? TF_ROOT);
|
|
183
|
+
expect(second.patched).toContain("bucket = var.assets_bucket");
|
|
184
|
+
expect(second.patched).not.toContain("prevent_destroy");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("a root written the modern way is a no-op", () => {
|
|
188
|
+
const res = proveFix("TF016", 'output "arn" {\n value = aws_s3_bucket.assets.arn\n}\n');
|
|
189
|
+
expect(res.applied).toBe(false);
|
|
190
|
+
expect(res.reason).toBe("noop");
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
127
194
|
describe("unifiedDiff", () => {
|
|
128
195
|
test("identical input produces an empty diff", () => {
|
|
129
196
|
expect(unifiedDiff(WF, WF)).toBe("");
|
package/src/audit/proof.ts
CHANGED
|
@@ -147,6 +147,70 @@ function narrowWriteAll(content: string): { patched: string; changed: boolean }
|
|
|
147
147
|
return { patched: content.replace(re, "permissions:\n contents: read"), changed: true };
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* TF019 — drop a meta-argument explicitly set to its own default `false`
|
|
152
|
+
* (`sensitive`, `ephemeral`, `prevent_destroy`, `create_before_destroy`). The
|
|
153
|
+
* fix is the deletion of the whole line, which is what tflint-ruleset-redeploy's
|
|
154
|
+
* own autofix for `terraform_redundant_default` does. Only a literal `false` is
|
|
155
|
+
* touched: `prevent_destroy = var.protect` is left alone, and so is a line with
|
|
156
|
+
* a trailing comment, where deleting the line would delete prose too.
|
|
157
|
+
*/
|
|
158
|
+
const REDUNDANT_DEFAULT_RE = /^[ \t]*(?:sensitive|ephemeral|prevent_destroy|create_before_destroy)[ \t]*=[ \t]*false[ \t]*$/;
|
|
159
|
+
|
|
160
|
+
function dropRedundantDefaults(content: string): { patched: string; changed: boolean } {
|
|
161
|
+
const kept = content.split("\n").filter((line) => !REDUNDANT_DEFAULT_RE.test(line));
|
|
162
|
+
const patched = kept.join("\n");
|
|
163
|
+
return { patched, changed: patched !== content };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* TF016 — unwrap an attribute whose whole value is one interpolation
|
|
168
|
+
* (`x = "${var.y}"` becomes `x = var.y`), the deprecated pre-0.12 style
|
|
169
|
+
* tflint's `terraform_deprecated_interpolation` reports. Anchored on the
|
|
170
|
+
* source text rather than the parsed body for the reason TF016's own module
|
|
171
|
+
* gives: `@cdktf/hcl2json` renders a bare reference and a quoted interpolation
|
|
172
|
+
* identically, so the quotes only exist here.
|
|
173
|
+
*/
|
|
174
|
+
const INTERPOLATION_ONLY_RE = /^([ \t]*)([A-Za-z_][A-Za-z0-9_-]*)([ \t]*=[ \t]*)"\$\{([^"]+)\}"([ \t]*(?:#.*)?)$/;
|
|
175
|
+
|
|
176
|
+
/** One `name = "${expr}"` line, split into the parts the fix reassembles. */
|
|
177
|
+
export interface InterpolationOnlyLine {
|
|
178
|
+
/** The attribute's name. */
|
|
179
|
+
name: string;
|
|
180
|
+
/** The expression inside the interpolation, without the `${` and `}`. */
|
|
181
|
+
expr: string;
|
|
182
|
+
/** The same line with the quotes and the `${}` taken off. */
|
|
183
|
+
unwrapped: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Parse a line whose whole value is one interpolation. Exported because a rule
|
|
188
|
+
* and its fix have to agree on what counts: the terraform lexicon's TF016
|
|
189
|
+
* check calls this over a block's source text, and `unwrapInterpolations`
|
|
190
|
+
* below rewrites exactly the lines it accepts.
|
|
191
|
+
*/
|
|
192
|
+
export function interpolationOnlyLine(line: string): InterpolationOnlyLine | undefined {
|
|
193
|
+
const m = INTERPOLATION_ONLY_RE.exec(line);
|
|
194
|
+
if (!m) return undefined;
|
|
195
|
+
const expr = m[4];
|
|
196
|
+
// A nested `${` means the template is not one interpolation (it embeds
|
|
197
|
+
// another), and a `}` before the end means the interpolation closed early,
|
|
198
|
+
// so the quotes are carrying literal text as well.
|
|
199
|
+
if (expr.includes("${") || expr.includes("}")) return undefined;
|
|
200
|
+
return { name: m[2], expr, unwrapped: `${m[1]}${m[2]}${m[3]}${expr}${m[5]}` };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function unwrapInterpolations(content: string): { patched: string; changed: boolean } {
|
|
204
|
+
let changed = false;
|
|
205
|
+
const lines = content.split("\n").map((line) => {
|
|
206
|
+
const parsed = interpolationOnlyLine(line);
|
|
207
|
+
if (!parsed) return line;
|
|
208
|
+
changed = true;
|
|
209
|
+
return parsed.unwrapped;
|
|
210
|
+
});
|
|
211
|
+
return { patched: lines.join("\n"), changed };
|
|
212
|
+
}
|
|
213
|
+
|
|
150
214
|
/**
|
|
151
215
|
* Produce a deterministic fix + diff for a finding, if one is mechanical.
|
|
152
216
|
* Returns `applied: false` with guidance for non-deterministic findings.
|
|
@@ -181,6 +245,12 @@ export function proveFix(checkId: string, content: string, opts: ProveOptions =
|
|
|
181
245
|
case "GHA033":
|
|
182
246
|
result = narrowWriteAll(content);
|
|
183
247
|
break;
|
|
248
|
+
case "TF016":
|
|
249
|
+
result = unwrapInterpolations(content);
|
|
250
|
+
break;
|
|
251
|
+
case "TF019":
|
|
252
|
+
result = dropRedundantDefaults(content);
|
|
253
|
+
break;
|
|
184
254
|
default:
|
|
185
255
|
return notApplied(checkId, "needs-input", cat?.remediation || "No deterministic fix implemented for this rule yet.");
|
|
186
256
|
}
|
package/src/audit/report-html.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { AuditFinding } from "./core";
|
|
12
|
-
import { ruleDocUrl, type RuleMeta } from "./catalog";
|
|
12
|
+
import { ruleDocUrl, type RuleMeta, lineageLabel } from "./catalog";
|
|
13
13
|
import { buildReportModel, buildReportJson, type AuditSnapshot, type BuildModelOptions, type EnrichedFinding, type GuidanceCluster, type QuickWinFile, type ReportCounts } from "./report-model";
|
|
14
14
|
|
|
15
15
|
export type { AuditSnapshot } from "./report-model";
|
|
@@ -48,9 +48,14 @@ function ruleLink(id: string): string {
|
|
|
48
48
|
|
|
49
49
|
/** External backing for a merge-worthy rule (OSSF Scorecard, CIS, vendor docs). */
|
|
50
50
|
function authorityLinks(meta: RuleMeta): string {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
const parts: string[] = [];
|
|
52
|
+
if (meta.authority?.length) {
|
|
53
|
+
parts.push(`per ${meta.authority.map((a) => `<a href="${esc(a.url)}">${esc(a.name)}</a>`).join(", ")}`);
|
|
54
|
+
}
|
|
55
|
+
if (meta.lineage?.length) {
|
|
56
|
+
parts.push(`prior art: ${meta.lineage.map((l) => `<a href="${esc(l.url)}">${esc(lineageLabel(l))}</a>`).join(", ")}`);
|
|
57
|
+
}
|
|
58
|
+
return parts.length ? ` <span class="muted">${parts.join(" · ")}</span>` : "";
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
function renderDiff(diff: string): string {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { AuditFinding } from "./core";
|
|
9
9
|
import type { UnclaimedFile } from "./discover";
|
|
10
|
-
import { RULE_CATALOG, ruleDocUrl, type Authority, type Category, type FixKind, type RuleMeta, type Tier } from "./catalog";
|
|
10
|
+
import { RULE_CATALOG, ruleDocUrl, type Authority, type Category, type FixKind, type Lineage, type RuleMeta, type Tier } from "./catalog";
|
|
11
11
|
import { proveFix, unifiedDiff, type ProveOptions } from "./proof";
|
|
12
12
|
import type { Severity } from "../lint/rule";
|
|
13
13
|
|
|
@@ -98,6 +98,8 @@ export interface SerializedFinding {
|
|
|
98
98
|
line?: number;
|
|
99
99
|
/** Redaction-safe fingerprint of a flagged value, when applicable (see `secrets.ts`). */
|
|
100
100
|
fingerprint?: string;
|
|
101
|
+
/** Set when the finding is an absence rather than a defect — see `PostSynthDiagnostic.missing` (chant #2113). */
|
|
102
|
+
missing?: { kind: string; scope: string };
|
|
101
103
|
lexicon: string;
|
|
102
104
|
tier: Tier;
|
|
103
105
|
fixKind: FixKind;
|
|
@@ -106,6 +108,8 @@ export interface SerializedFinding {
|
|
|
106
108
|
title: string;
|
|
107
109
|
remediation: string;
|
|
108
110
|
authority: Authority[];
|
|
111
|
+
/** Prior art: upstream tools whose rules check the same condition (credit, not authority). */
|
|
112
|
+
lineage: Lineage[];
|
|
109
113
|
/** Link to this rule's entry in the audit rules reference. */
|
|
110
114
|
docUrl: string;
|
|
111
115
|
}
|
|
@@ -246,7 +250,13 @@ export function buildReportModel(findings: AuditFinding[], opts: BuildModelOptio
|
|
|
246
250
|
const contents = new Map((opts.files ?? []).map((f) => [f.path, f.content]));
|
|
247
251
|
|
|
248
252
|
const mergeWorthy = enriched.filter((f) => f.meta.tier === "merge-worthy");
|
|
249
|
-
|
|
253
|
+
// A deterministic fix is a quick win whatever the finding's tier. Every
|
|
254
|
+
// deterministic rule was merge-worthy until terraform's TF016 and TF019
|
|
255
|
+
// (#2110): both are hygiene, and both delete or unquote exactly the flagged
|
|
256
|
+
// line, which is the same safe mechanical patch a merge-worthy quick win
|
|
257
|
+
// offers. Filing them under "needs a judgement call" or in the hygiene table
|
|
258
|
+
// would hide a ready-to-apply diff.
|
|
259
|
+
const quickWinFindings = enriched.filter((f) => f.meta.fixKind === "deterministic");
|
|
250
260
|
const needsReviewFindings = mergeWorthy.filter((f) => f.meta.fixKind === "guidance");
|
|
251
261
|
|
|
252
262
|
// De-noise: drop a report-only finding only when a specific merge-worthy
|
|
@@ -254,6 +264,8 @@ export function buildReportModel(findings: AuditFinding[], opts: BuildModelOptio
|
|
|
254
264
|
const mwOnEntity = new Set(mergeWorthy.filter((f) => f.entity).map((f) => `${f.file}:${f.entity}:${f.checkId}`));
|
|
255
265
|
const reportOnly = enriched.filter((f) => {
|
|
256
266
|
if (f.meta.tier !== "report-only") return false;
|
|
267
|
+
// Counted and rendered as a quick win instead, so the three buckets stay disjoint.
|
|
268
|
+
if (f.meta.fixKind === "deterministic") return false;
|
|
257
269
|
const supers = SUPERSEDED_BY[f.checkId];
|
|
258
270
|
if (supers && f.entity && supers.some((id) => mwOnEntity.has(`${f.file}:${f.entity}:${id}`))) return false;
|
|
259
271
|
return true;
|
|
@@ -304,6 +316,7 @@ export function buildReportJson(
|
|
|
304
316
|
entity: f.entity,
|
|
305
317
|
line: f.line,
|
|
306
318
|
fingerprint: f.fingerprint,
|
|
319
|
+
missing: f.missing,
|
|
307
320
|
lexicon: f.lexicon,
|
|
308
321
|
tier: f.meta.tier,
|
|
309
322
|
fixKind: f.meta.fixKind,
|
|
@@ -311,6 +324,7 @@ export function buildReportJson(
|
|
|
311
324
|
title: f.meta.title,
|
|
312
325
|
remediation: f.meta.remediation,
|
|
313
326
|
authority: f.meta.authority ?? [],
|
|
327
|
+
lineage: f.meta.lineage ?? [],
|
|
314
328
|
docUrl: ruleDocUrl(f.checkId),
|
|
315
329
|
})),
|
|
316
330
|
unclaimed: opts.unclaimed && opts.unclaimed.length > 0 ? opts.unclaimed : undefined,
|
package/src/audit/report.test.ts
CHANGED
|
@@ -149,6 +149,39 @@ describe("efficiency dimension (#444)", () => {
|
|
|
149
149
|
});
|
|
150
150
|
});
|
|
151
151
|
|
|
152
|
+
describe("a report-only rule with a deterministic fix (#2110)", () => {
|
|
153
|
+
const TF_ROOT = `variable "log_level" {
|
|
154
|
+
type = string
|
|
155
|
+
sensitive = false
|
|
156
|
+
}
|
|
157
|
+
`;
|
|
158
|
+
const findings: AuditFinding[] = [
|
|
159
|
+
{
|
|
160
|
+
checkId: "TF019",
|
|
161
|
+
severity: "info",
|
|
162
|
+
message: "sensitive = false is already the default.",
|
|
163
|
+
file: "infra/prod",
|
|
164
|
+
lexicon: "terraform",
|
|
165
|
+
entity: "infra/prod/var.log_level.sensitive",
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
test("renders in quick wins with its diff, not in the hygiene table", async () => {
|
|
170
|
+
const catalog = await resolveAuditCatalog(["terraform"]);
|
|
171
|
+
const out = renderMarkdown(findings, { catalog, files: [{ path: "infra/prod", content: TF_ROOT }] });
|
|
172
|
+
expect(out).toContain("## Quick wins (deterministic)");
|
|
173
|
+
expect(out).toContain("- sensitive = false");
|
|
174
|
+
expect(out).toContain("1 finding — 1 quick-win, 0 needs-review, 0 report-only");
|
|
175
|
+
expect(out).not.toContain("Report-only (hygiene)");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("the credit rides beside the fix", async () => {
|
|
179
|
+
const catalog = await resolveAuditCatalog(["terraform"]);
|
|
180
|
+
const out = renderMarkdown(findings, { catalog, files: [{ path: "infra/prod", content: TF_ROOT }] });
|
|
181
|
+
expect(out).toContain("prior art: [tflint-ruleset-redeploy terraform_redundant_default]");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
152
185
|
describe("catalog threading (#687)", () => {
|
|
153
186
|
test("buildReportModel uses a passed-in catalog over core's static one", async () => {
|
|
154
187
|
const { buildReportModel } = await import("./report-model");
|
package/src/audit/report.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { AuditFinding } from "./core";
|
|
14
|
-
import { ruleDocUrl, type RuleMeta } from "./catalog";
|
|
14
|
+
import { lineageLabel, ruleDocUrl, type RuleMeta } from "./catalog";
|
|
15
15
|
import type { ProveOptions } from "./proof";
|
|
16
16
|
import { buildReportModel, type EnrichedFinding, type GuidanceCluster, type QuickWinFile } from "./report-model";
|
|
17
17
|
|
|
@@ -22,8 +22,10 @@ function ruleLink(id: string): string {
|
|
|
22
22
|
|
|
23
23
|
/** External backing for a merge-worthy rule (OSSF Scorecard, CIS, vendor docs). */
|
|
24
24
|
function authorityLinks(meta: RuleMeta): string {
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const authority = meta.authority?.length ? ` — per ${meta.authority.map((a) => `[${a.name}](${a.url})`).join(", ")}` : "";
|
|
26
|
+
// Prior art rides beside the authority: the tool that checked this first.
|
|
27
|
+
const lineage = meta.lineage?.length ? ` · prior art: ${meta.lineage.map((l) => `[${lineageLabel(l)}](${l.url})`).join(", ")}` : "";
|
|
28
|
+
return authority + lineage;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export interface RenderOptions {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { readFileSync } from "fs";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
|
-
import { renderRulesReference } from "./rules-doc";
|
|
5
|
-
import { resolveAuditCatalog, ruleDocUrl } from "./catalog";
|
|
4
|
+
import { renderRulesReference, ruleBlock } from "./rules-doc";
|
|
5
|
+
import { resolveAuditCatalog, ruleDocUrl, auditRule } from "./catalog";
|
|
6
6
|
import { AUDIT_LEXICONS } from "./discover";
|
|
7
7
|
|
|
8
8
|
const PAGE = fileURLToPath(new URL("../../../../docs/src/content/docs/lint-rules/audit-rules.mdx", import.meta.url));
|
|
@@ -23,3 +23,35 @@ describe("audit rules reference", () => {
|
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
25
|
});
|
|
26
|
+
|
|
27
|
+
describe("ruleBlock, aliases and deprecated (chant #2113)", () => {
|
|
28
|
+
test("renders aliases as a list", () => {
|
|
29
|
+
const m = { ...auditRule("NEW001", "report-only", "guidance", "T", "r"), aliases: ["OLD001", "LEGACY_OLD"] };
|
|
30
|
+
const block = ruleBlock(m);
|
|
31
|
+
expect(block).toContain("Also known as: `OLD001`, `LEGACY_OLD`.");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("greys a deprecated rule (tags line, not the heading; the heading is the anchor `ruleDocUrl` targets)", () => {
|
|
35
|
+
const m = { ...auditRule("OLD002", "report-only", "guidance", "T", "r") };
|
|
36
|
+
m.deprecated = "superseded by NEW002";
|
|
37
|
+
const block = ruleBlock(m);
|
|
38
|
+
expect(block).toContain("### OLD002"); // heading unchanged: `ruleDocUrl("OLD002")` must still resolve
|
|
39
|
+
expect(block).toContain("~~deprecated~~");
|
|
40
|
+
expect(block).toContain(":::note[Deprecated]");
|
|
41
|
+
expect(block).toContain("superseded by NEW002");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("a deprecated rule with no string reason gets a generic note", () => {
|
|
45
|
+
const m = { ...auditRule("OLD003", "report-only", "guidance", "T", "r") };
|
|
46
|
+
m.deprecated = true;
|
|
47
|
+
expect(ruleBlock(m)).toContain("This rule no longer runs.");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("a rule with neither aliases nor deprecated renders unchanged from before #2113", () => {
|
|
51
|
+
const m = auditRule("PLAIN001", "report-only", "guidance", "T", "r");
|
|
52
|
+
const block = ruleBlock(m);
|
|
53
|
+
expect(block).not.toContain("Also known as");
|
|
54
|
+
expect(block).not.toContain(":::note[Deprecated]");
|
|
55
|
+
expect(block).not.toContain("~~deprecated~~");
|
|
56
|
+
});
|
|
57
|
+
});
|
package/src/audit/rules-doc.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* (lowercased) — the anchor `ruleDocUrl()` links to.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { resolveAuditCatalog, type RuleMeta } from "./catalog";
|
|
10
|
+
import { PRIOR_ART, resolveAuditCatalog, type Lineage, type PriorArtEntry, type RuleMeta } from "./catalog";
|
|
11
11
|
import { AUDIT_LEXICONS } from "./discover";
|
|
12
12
|
|
|
13
13
|
const GROUPS: Array<{ heading: string; prefixes: string[]; blurb: string }> = [
|
|
@@ -21,6 +21,7 @@ const GROUPS: Array<{ heading: string; prefixes: string[]; blurb: string }> = [
|
|
|
21
21
|
{ heading: "GCP Config Connector (WGC)", prefixes: ["WGC"], blurb: "Run against Config Connector (cnrm.cloud.google.com) manifests." },
|
|
22
22
|
{ heading: "Helm (WHM)", prefixes: ["WHM"], blurb: "Run against Helm charts (Chart.yaml + templates)." },
|
|
23
23
|
{ heading: "fountain (FTN)", prefixes: ["FTN"], blurb: "Run against fountain manifests (`apiVersion: fountain.dev/v1`) — standalone `fountain apply` YAML is parsed back into the entity graph, so the same rules fire on `chant build` and `chant audit`." },
|
|
24
|
+
{ heading: "Terraform (TF)", prefixes: ["TF"], blurb: "Run against Terraform root modules (a directory of `.tf` files) parsed into the entity graph, so the same rules fire on `chant build` over `terraform.roots` and on `chant audit` (#2083)." },
|
|
24
25
|
{ heading: "Secrets & credentials (SEC)", prefixes: ["SEC"], blurb: "Lexicon-independent — scans the raw text of every scanned file for likely credentials, regardless of which audit lexicons are installed. Matched values are always redacted; see [suppressing false positives](/chant/cli/audit/#suppressing-a-secrets-finding)." },
|
|
25
26
|
{ heading: "Wrangler config (WRG)", prefixes: ["WRG"], blurb: "Lexicon-independent, audit-only (#446) — scans `wrangler.toml`, Cloudflare Workers' native deploy config, which the engine cannot otherwise parse (it is not YAML/JSON). No authoring surface: chant does not write Wrangler config, it only reads it for these checks." },
|
|
26
27
|
{ heading: "nginx config (NGX)", prefixes: ["NGX"], blurb: "Lexicon-independent, audit-only (#1979) — parses nginx's native directive/block config (`nginx.conf` and `.conf` files under nginx-ish directories, confirmed by content). No authoring surface: chant does not write nginx config, it only reads it for these checks." },
|
|
@@ -41,12 +42,84 @@ function mdxEscape(text: string): string {
|
|
|
41
42
|
return text.replace(/\{/g, "\\{").replace(/</g, "\\<");
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
function
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
function lineageLink(l: Lineage): string {
|
|
46
|
+
const tool = (PRIOR_ART as Record<string, PriorArtEntry>)[l.tool]?.name ?? l.tool;
|
|
47
|
+
const label = l.rule ? `${tool} \`${l.rule}\`` : tool;
|
|
48
|
+
return `[${mdxEscape(label)}](${l.url}) (${l.relation})`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A rule's backing, as a two-column table: the authority that says the finding
|
|
53
|
+
* matters, and the prior art that checked it first. A table rather than two
|
|
54
|
+
* "Label: ..." lines so that 379 rules do not read as 379 repeated sentences.
|
|
55
|
+
*/
|
|
56
|
+
function backingTable(m: RuleMeta): string {
|
|
57
|
+
const rows: string[] = [];
|
|
58
|
+
if (m.authority?.length) rows.push(`| Authority | ${m.authority.map((a) => `[${a.name}](${a.url})`).join(" · ")} |`);
|
|
59
|
+
if (m.lineage?.length) rows.push(`| Prior art | ${m.lineage.map(lineageLink).join(" · ")} |`);
|
|
60
|
+
return rows.length ? `\n\n| | |\n|---|---|\n${rows.join("\n")}` : "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A rule kept the day it retires (chant #2113) rather than dropped: still
|
|
65
|
+
* gets a heading (so old links and `lint.rules`/suppression entries that
|
|
66
|
+
* name it still land somewhere), but greyed via a `:::note` aside instead of
|
|
67
|
+
* reading like a live rule, and with the `deprecated` string, when one was
|
|
68
|
+
* given, saying what replaced it.
|
|
69
|
+
*/
|
|
70
|
+
function deprecatedNote(deprecated: boolean | string): string {
|
|
71
|
+
const reason = typeof deprecated === "string" ? mdxEscape(deprecated) : "This rule no longer runs.";
|
|
72
|
+
return `\n\n:::note[Deprecated]\n${reason} The id is kept so a reference to it still resolves.\n:::`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function aliasesLine(aliases: string[]): string {
|
|
76
|
+
return `\n\nAlso known as: ${aliases.map((a) => `\`${mdxEscape(a)}\``).join(", ")}.`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Exported for `rules-doc.test.ts` to exercise the alias/deprecated rendering directly against a synthetic `RuleMeta`, without needing a real catalog entry to carry either (chant #2113). */
|
|
80
|
+
export function ruleBlock(m: RuleMeta): string {
|
|
81
|
+
// The heading text is what Starlight slugifies into `#<id>`, the anchor
|
|
82
|
+
// `ruleDocUrl()` links to (`./catalog.ts`), so it stays exactly `### <ID>`
|
|
83
|
+
// even for a deprecated rule. The "greyed out" treatment goes below the
|
|
84
|
+
// heading instead, so an old link/alias reference still lands on the id.
|
|
85
|
+
const tags = m.deprecated ? `${m.tier} · ${m.fixKind} · ~~deprecated~~` : `${m.tier} · ${m.fixKind}`;
|
|
86
|
+
return (
|
|
87
|
+
`### ${m.id}\n\n**${mdxEscape(m.title)}** — ${tags}\n\n${mdxEscape(m.remediation)}` +
|
|
88
|
+
`${backingTable(m)}` +
|
|
89
|
+
`${m.aliases?.length ? aliasesLine(m.aliases) : ""}` +
|
|
90
|
+
`${m.deprecated ? deprecatedNote(m.deprecated) : ""}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The closing section: every tool the rules credit, with its licence and how
|
|
96
|
+
* many rules point at it. Rendered from the same registry the entries key into,
|
|
97
|
+
* so a tool cannot be credited on a rule without appearing here.
|
|
98
|
+
*/
|
|
99
|
+
function priorArtSection(catalog: Record<string, RuleMeta>): string {
|
|
100
|
+
const counts = new Map<string, number>();
|
|
101
|
+
for (const m of Object.values(catalog)) {
|
|
102
|
+
for (const l of m.lineage ?? []) counts.set(l.tool, (counts.get(l.tool) ?? 0) + 1);
|
|
103
|
+
}
|
|
104
|
+
const rows = (Object.entries(PRIOR_ART) as Array<[string, PriorArtEntry]>)
|
|
105
|
+
.filter(([key]) => counts.has(key))
|
|
106
|
+
.sort(([a], [b]) => (counts.get(b) ?? 0) - (counts.get(a) ?? 0) || a.localeCompare(b))
|
|
107
|
+
.map(([key, t]) => `| [${t.name}](${t.url}) | ${t.kind} | ${t.license} | ${counts.get(key)} |`);
|
|
108
|
+
if (rows.length === 0) return "";
|
|
109
|
+
return `## Prior art
|
|
110
|
+
|
|
111
|
+
chant's rules were written against the output chant emits, but much of what they check was checked first by a dedicated tool. Each rule above that has a counterpart names it under **Prior art**, with the upstream rule id and how the two relate.
|
|
112
|
+
|
|
113
|
+
- \`equivalent\` is the same condition at the same scope.
|
|
114
|
+
- \`overlaps\` shares the core condition, with one side narrower or broader, or expressed for a different dialect of the same control.
|
|
115
|
+
- \`extends\` means chant checks a strict superset of the upstream rule.
|
|
116
|
+
|
|
117
|
+
This is credit rather than authority, and it never changes a rule's tier or category. A credit counts once per upstream rule cited, so a chant rule that names two zizmor audits adds two.
|
|
118
|
+
|
|
119
|
+
| Tool | Kind | Licence | Credits |
|
|
120
|
+
|---|---|---|---|
|
|
121
|
+
${rows.join("\n")}
|
|
122
|
+
`;
|
|
50
123
|
}
|
|
51
124
|
|
|
52
125
|
/**
|
|
@@ -73,8 +146,8 @@ diataxis: reference
|
|
|
73
146
|
|
|
74
147
|
This is the reference for every rule [\`chant audit\`](/chant/cli/audit/) can report. Each finding in a report links to its rule here.
|
|
75
148
|
|
|
76
|
-
Each rule is tagged with its **tier** — \`merge-worthy\` (a security or correctness issue worth a PR) or \`report-only\` (hygiene) — and its **fix kind** — \`deterministic\` (a safe mechanical fix the report can apply as a diff) or \`guidance\` (needs a judgement call).
|
|
149
|
+
Each rule is tagged with its **tier** — \`merge-worthy\` (a security or correctness issue worth a PR) or \`report-only\` (hygiene) — and its **fix kind** — \`deterministic\` (a safe mechanical fix the report can apply as a diff) or \`guidance\` (needs a judgement call). Where an open-source tool checks the same thing, the rule says so under **Prior art**; the [Prior art](#prior-art) section at the end lists every tool the rules on this page credit.
|
|
77
150
|
|
|
78
|
-
${sections.join("\n\n")}
|
|
151
|
+
${[...sections, priorArtSection(catalog)].filter(Boolean).join("\n\n")}
|
|
79
152
|
`;
|
|
80
153
|
}
|
package/src/audit/secrets.ts
CHANGED
|
@@ -173,7 +173,13 @@ export function parseSecretsConfig(content: string): SecretsScanOptions {
|
|
|
173
173
|
|
|
174
174
|
// ── Shannon entropy ─────────────────────────────────────────────────────
|
|
175
175
|
|
|
176
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Shannon entropy in bits per character. Exported so a lexicon's own
|
|
178
|
+
* secret-shape heuristic can score a candidate on the same scale this
|
|
179
|
+
* module's SEC010 catch-all uses (the terraform lexicon's
|
|
180
|
+
* `lint/secret-shape.ts`, #2110) instead of inventing a second one.
|
|
181
|
+
*/
|
|
182
|
+
export function shannonEntropy(s: string): number {
|
|
177
183
|
const freq = new Map<string, number>();
|
|
178
184
|
for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
179
185
|
let entropy = 0;
|
|
@@ -188,8 +194,14 @@ function shannonEntropy(s: string): number {
|
|
|
188
194
|
|
|
189
195
|
const PLACEHOLDER_RE = /example|placeholder|dummy|changeme|change-me|redacted|sample|fixme|todo|xxxxxxxx|your[-_]?(api[-_]?)?(key|token|secret)|fake[-_]?(key|token|secret)|test[-_]?(key|token|secret)/i;
|
|
190
196
|
|
|
191
|
-
/**
|
|
192
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Cheap, format-agnostic "this doesn't look like a real secret" filter.
|
|
199
|
+
* Exported for the same reason {@link shannonEntropy} is: the placeholder
|
|
200
|
+
* vocabulary a scanner has to ignore (`changeme`, `example`, `your-api-key`,
|
|
201
|
+
* a run of one repeated character) is the same wherever the candidate came
|
|
202
|
+
* from, and a second copy of this list would drift from this one.
|
|
203
|
+
*/
|
|
204
|
+
export function looksLikePlaceholder(value: string): boolean {
|
|
193
205
|
if (PLACEHOLDER_RE.test(value)) return true;
|
|
194
206
|
const uniq = new Set(value.toLowerCase()).size;
|
|
195
207
|
if (value.length > 8 && uniq <= 2) return true; // e.g. "0000000000000000"
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { describe, expect, test } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
auditTerraformState,
|
|
7
|
+
gitignoreCoversTerraformState,
|
|
8
|
+
isTerraformStateFile,
|
|
9
|
+
isTerraformStatePath,
|
|
10
|
+
isTerraformWorkDir,
|
|
11
|
+
type ScannableFile,
|
|
12
|
+
} from "./terraform-state";
|
|
13
|
+
import { collectCandidates } from "./discover";
|
|
14
|
+
import { RULE_CATALOG, RULE_CATEGORY } from "./catalog";
|
|
15
|
+
|
|
16
|
+
const file = (path: string): ScannableFile => ({ path, content: "" });
|
|
17
|
+
|
|
18
|
+
function tmpRepo(): string {
|
|
19
|
+
return mkdtempSync(join(tmpdir(), "chant-tf023-"));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("the TF023 path detectors", () => {
|
|
23
|
+
test.each([
|
|
24
|
+
"terraform.tfstate",
|
|
25
|
+
"terraform.tfstate.backup",
|
|
26
|
+
"infra/prod/terraform.tfstate",
|
|
27
|
+
"infra/prod.tfstate",
|
|
28
|
+
"infra/prod.tfstate.backup",
|
|
29
|
+
])("%s is state", (path) => {
|
|
30
|
+
expect(isTerraformStateFile(path)).toBe(true);
|
|
31
|
+
expect(isTerraformStatePath(path)).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test.each(["main.tf", "terraform.tfvars", "state.json", "docs/tfstate.md"])("%s is not", (path) => {
|
|
35
|
+
expect(isTerraformStateFile(path)).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("the working directory is matched as a path segment, at any depth", () => {
|
|
39
|
+
expect(isTerraformWorkDir(".terraform")).toBe(true);
|
|
40
|
+
expect(isTerraformWorkDir(".terraform/providers/registry.terraform.io/hashicorp/aws/5.0.0")).toBe(true);
|
|
41
|
+
expect(isTerraformWorkDir("infra/prod/.terraform")).toBe(true);
|
|
42
|
+
expect(isTerraformWorkDir("infra/terraform/main.tf")).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("auditTerraformState", () => {
|
|
47
|
+
test("reports a committed state file as an error, without reading it", () => {
|
|
48
|
+
const findings = auditTerraformState([file("infra/prod/terraform.tfstate")]);
|
|
49
|
+
expect(findings).toHaveLength(1);
|
|
50
|
+
expect(findings[0].checkId).toBe("TF023");
|
|
51
|
+
expect(findings[0].severity).toBe("error");
|
|
52
|
+
expect(findings[0].lexicon).toBe("terraform");
|
|
53
|
+
expect(findings[0].file).toBe("infra/prod/terraform.tfstate");
|
|
54
|
+
expect(findings[0].message).toContain("secret store");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("reports a backup state file too", () => {
|
|
58
|
+
expect(auditTerraformState([file("terraform.tfstate.backup")])).toHaveLength(1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("reports a `.terraform` directory once, not once per file inside it", () => {
|
|
62
|
+
const findings = auditTerraformState([
|
|
63
|
+
file(".terraform"),
|
|
64
|
+
file(".terraform/modules/modules.json"),
|
|
65
|
+
file(".terraform/providers/registry.terraform.io/hashicorp/aws/5.0.0/terraform-provider-aws"),
|
|
66
|
+
]);
|
|
67
|
+
expect(findings).toHaveLength(1);
|
|
68
|
+
expect(findings[0].file).toBe(".terraform");
|
|
69
|
+
expect(findings[0].entity).toBe(".terraform");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("reports each root's own working directory separately", () => {
|
|
73
|
+
const findings = auditTerraformState([file("infra/prod/.terraform"), file("infra/staging/.terraform")]);
|
|
74
|
+
expect(findings.map((f) => f.file)).toEqual(["infra/prod/.terraform", "infra/staging/.terraform"]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("says nothing about a repository with neither", () => {
|
|
78
|
+
expect(auditTerraformState([{ path: "main.tf", content: 'resource "null_resource" "a" {}' }])).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("is catalogued in core, as a merge-worthy security rule", () => {
|
|
82
|
+
expect(RULE_CATALOG.TF023.tier).toBe("merge-worthy");
|
|
83
|
+
expect(RULE_CATALOG.TF023.category).toBe("security");
|
|
84
|
+
expect(RULE_CATEGORY.TF023).toBe("security");
|
|
85
|
+
expect(RULE_CATALOG.TF023.authority?.[0].url).toContain("#gitignore");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("gitignoreCoversTerraformState", () => {
|
|
90
|
+
test.each([
|
|
91
|
+
["*.tfstate", "terraform.tfstate"],
|
|
92
|
+
["*.tfstate.*", "terraform.tfstate.backup"],
|
|
93
|
+
[".terraform/", ".terraform"],
|
|
94
|
+
[".terraform*", ".terraform"],
|
|
95
|
+
["/terraform.tfstate", "terraform.tfstate"],
|
|
96
|
+
["**/.terraform", "infra/prod/.terraform"],
|
|
97
|
+
["# comment\n\n*.tfstate\n", "infra/prod/terraform.tfstate"],
|
|
98
|
+
])("%s covers %s", (gitignore, path) => {
|
|
99
|
+
expect(gitignoreCoversTerraformState(gitignore, path)).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test.each([
|
|
103
|
+
["node_modules/\ndist/", "terraform.tfstate"],
|
|
104
|
+
["*.tfvars", "terraform.tfstate"],
|
|
105
|
+
["!terraform.tfstate", "terraform.tfstate"],
|
|
106
|
+
])("%s does not cover %s", (gitignore, path) => {
|
|
107
|
+
expect(gitignoreCoversTerraformState(gitignore, path)).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("discovery hands TF023 its paths, and nothing else changes", () => {
|
|
112
|
+
test("a state file becomes a candidate with no content, so nothing reads it", () => {
|
|
113
|
+
const dir = tmpRepo();
|
|
114
|
+
writeFileSync(join(dir, "main.tf"), 'resource "null_resource" "a" {}\n');
|
|
115
|
+
writeFileSync(join(dir, "terraform.tfstate"), JSON.stringify({ version: 4, resources: [] }));
|
|
116
|
+
const candidates = collectCandidates(dir);
|
|
117
|
+
const state = candidates.find((c) => c.path === "terraform.tfstate");
|
|
118
|
+
expect(state).toBeDefined();
|
|
119
|
+
expect(state!.content).toBe("");
|
|
120
|
+
expect(auditTerraformState(candidates)).toHaveLength(1);
|
|
121
|
+
rmSync(dir, { recursive: true, force: true });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("the walk records `.terraform` without descending into it", () => {
|
|
125
|
+
const dir = tmpRepo();
|
|
126
|
+
writeFileSync(join(dir, "main.tf"), 'resource "null_resource" "a" {}\n');
|
|
127
|
+
mkdirSync(join(dir, ".terraform", "providers"), { recursive: true });
|
|
128
|
+
writeFileSync(join(dir, ".terraform", "providers", "huge.json"), "{}");
|
|
129
|
+
const candidates = collectCandidates(dir);
|
|
130
|
+
expect(candidates.map((c) => c.path).sort()).toEqual([".terraform", "main.tf"]);
|
|
131
|
+
expect(auditTerraformState(candidates)).toHaveLength(1);
|
|
132
|
+
rmSync(dir, { recursive: true, force: true });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("a gitignored state file is on disk, not in the repository, so it is dropped", () => {
|
|
136
|
+
const dir = tmpRepo();
|
|
137
|
+
writeFileSync(join(dir, ".gitignore"), ".terraform/\n*.tfstate\n*.tfstate.*\n");
|
|
138
|
+
writeFileSync(join(dir, "main.tf"), 'resource "null_resource" "a" {}\n');
|
|
139
|
+
writeFileSync(join(dir, "terraform.tfstate"), "{}");
|
|
140
|
+
mkdirSync(join(dir, ".terraform"), { recursive: true });
|
|
141
|
+
expect(auditTerraformState(collectCandidates(dir))).toEqual([]);
|
|
142
|
+
rmSync(dir, { recursive: true, force: true });
|
|
143
|
+
});
|
|
144
|
+
});
|