@intentius/chant 0.124.51 → 0.124.54
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/discover.d.ts.map +1 -1
- package/dist/audit/discover.js +3 -1
- package/dist/behaviour.d.ts +1 -1
- package/dist/behaviour.d.ts.map +1 -1
- package/dist/capabilities/templates.d.ts +65 -0
- package/dist/capabilities/templates.d.ts.map +1 -0
- package/dist/capabilities/templates.js +193 -0
- package/dist/capabilities/templates.json +145 -0
- package/dist/change-set-graph.d.ts +35 -3
- package/dist/change-set-graph.d.ts.map +1 -1
- package/dist/change-set-graph.js +60 -9
- package/dist/change-set.d.ts +65 -7
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +23 -12
- package/dist/ci-identity.d.ts +7 -2
- package/dist/ci-identity.d.ts.map +1 -1
- package/dist/ci-identity.js +4 -1
- package/dist/cli/commands/import-index.d.ts +52 -0
- package/dist/cli/commands/import-index.d.ts.map +1 -0
- package/dist/cli/commands/import-index.js +226 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/lint.js +4 -2
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/components.js +11 -2
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +16 -3
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/graph.js +22 -4
- package/dist/cli/handlers/index-cloud.d.ts +14 -0
- package/dist/cli/handlers/index-cloud.d.ts.map +1 -0
- package/dist/cli/handlers/index-cloud.js +22 -0
- package/dist/cli/handlers/index-diff.d.ts +18 -0
- package/dist/cli/handlers/index-diff.d.ts.map +1 -0
- package/dist/cli/handlers/index-diff.js +142 -0
- package/dist/cli/handlers/lifecycle-drift.d.ts +25 -0
- package/dist/cli/handlers/lifecycle-drift.d.ts.map +1 -0
- package/dist/cli/handlers/lifecycle-drift.js +64 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.js +188 -28
- package/dist/cli/handlers/misc.d.ts +9 -0
- package/dist/cli/handlers/misc.d.ts.map +1 -1
- package/dist/cli/handlers/misc.js +77 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +17 -3
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +89 -15
- package/dist/cli/handlers/query.d.ts.map +1 -1
- package/dist/cli/handlers/query.js +28 -4
- package/dist/cli/handlers/scenario.d.ts.map +1 -1
- package/dist/cli/handlers/scenario.js +8 -7
- package/dist/cli/handlers/search.d.ts +34 -4
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/handlers/search.js +216 -36
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +55 -1
- package/dist/cli/mcp/lifecycle-tools.d.ts.map +1 -1
- package/dist/cli/mcp/lifecycle-tools.js +3 -2
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.js +4 -3
- package/dist/cli/mcp/workspace-tools.js +1 -1
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +55 -2
- package/dist/cli/registry.d.ts +19 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cloud-lister/aws.d.ts +35 -0
- package/dist/cloud-lister/aws.d.ts.map +1 -0
- package/dist/cloud-lister/aws.js +203 -0
- package/dist/cloud-lister/azure.d.ts +34 -0
- package/dist/cloud-lister/azure.d.ts.map +1 -0
- package/dist/cloud-lister/azure.js +129 -0
- package/dist/cloud-lister/common.d.ts +40 -0
- package/dist/cloud-lister/common.d.ts.map +1 -0
- package/dist/cloud-lister/common.js +83 -0
- package/dist/cloud-lister/credentials.d.ts +50 -0
- package/dist/cloud-lister/credentials.d.ts.map +1 -0
- package/dist/cloud-lister/credentials.js +86 -0
- package/dist/cloud-lister/fakes.d.ts +27 -0
- package/dist/cloud-lister/fakes.d.ts.map +1 -0
- package/dist/cloud-lister/fakes.js +50 -0
- package/dist/cloud-lister/gcp.d.ts +42 -0
- package/dist/cloud-lister/gcp.d.ts.map +1 -0
- package/dist/cloud-lister/gcp.js +143 -0
- package/dist/cloud-lister/guard.d.ts +49 -0
- package/dist/cloud-lister/guard.d.ts.map +1 -0
- package/dist/cloud-lister/guard.js +103 -0
- package/dist/cloud-lister/ids.d.ts +23 -0
- package/dist/cloud-lister/ids.d.ts.map +1 -0
- package/dist/cloud-lister/ids.js +42 -0
- package/dist/cloud-lister/index.d.ts +67 -0
- package/dist/cloud-lister/index.d.ts.map +1 -0
- package/dist/cloud-lister/index.js +102 -0
- package/dist/cloud-lister/kubernetes.d.ts +64 -0
- package/dist/cloud-lister/kubernetes.d.ts.map +1 -0
- package/dist/cloud-lister/kubernetes.js +276 -0
- package/dist/cloud-lister/markers.d.ts +31 -0
- package/dist/cloud-lister/markers.d.ts.map +1 -0
- package/dist/cloud-lister/markers.js +73 -0
- package/dist/cloud-lister/mask.d.ts +31 -0
- package/dist/cloud-lister/mask.d.ts.map +1 -0
- package/dist/cloud-lister/mask.js +74 -0
- package/dist/cloud-lister/sigv4.d.ts +29 -0
- package/dist/cloud-lister/sigv4.d.ts.map +1 -0
- package/dist/cloud-lister/sigv4.js +54 -0
- package/dist/cloud-lister/transport.d.ts +55 -0
- package/dist/cloud-lister/transport.d.ts.map +1 -0
- package/dist/cloud-lister/transport.js +86 -0
- package/dist/cloud-lister/types.d.ts +121 -0
- package/dist/cloud-lister/types.d.ts.map +1 -0
- package/dist/cloud-lister/types.js +24 -0
- package/dist/components/capability-plugin-loader.d.ts.map +1 -1
- package/dist/components/capability-plugin-loader.js +6 -0
- package/dist/components/capability-plugin.d.ts +9 -0
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/capability.d.ts +224 -0
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +21 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +58 -14
- package/dist/components/covered-round.d.ts +173 -0
- package/dist/components/covered-round.d.ts.map +1 -0
- package/dist/components/covered-round.js +200 -0
- package/dist/components/declared-reads.d.ts +38 -0
- package/dist/components/declared-reads.d.ts.map +1 -0
- package/dist/components/declared-reads.js +131 -0
- package/dist/components/deploy-units.d.ts.map +1 -1
- package/dist/components/deploy-units.js +4 -0
- package/dist/components/driver.d.ts +42 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +74 -3
- package/dist/components/fan-out-run.d.ts +23 -1
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +82 -14
- package/dist/components/fan-out-support.d.ts +6 -0
- package/dist/components/fan-out-support.d.ts.map +1 -1
- package/dist/components/fan-out-support.js +4 -1
- package/dist/components/fan-out.d.ts +28 -0
- package/dist/components/fan-out.d.ts.map +1 -1
- package/dist/components/groups.d.ts +229 -0
- package/dist/components/groups.d.ts.map +1 -0
- package/dist/components/groups.js +760 -0
- package/dist/components/index.d.ts +7 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +5 -0
- package/dist/components/plan-shape-conformance.d.ts +48 -0
- package/dist/components/plan-shape-conformance.d.ts.map +1 -0
- package/dist/components/plan-shape-conformance.js +122 -0
- package/dist/components/pr-run.d.ts +62 -7
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +354 -53
- package/dist/components/promote-rounds.d.ts +9 -0
- package/dist/components/promote-rounds.d.ts.map +1 -1
- package/dist/components/promote-rounds.js +23 -2
- package/dist/components/promote.d.ts +17 -0
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/promote.js +36 -6
- package/dist/components/release-gate.d.ts +23 -1
- package/dist/components/release-gate.d.ts.map +1 -1
- package/dist/components/release-gate.js +97 -33
- package/dist/components/run-groups.d.ts +58 -0
- package/dist/components/run-groups.d.ts.map +1 -0
- package/dist/components/run-groups.js +78 -0
- package/dist/components/starter-plugin.d.ts +1 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/starter-plugin.js +6 -1
- package/dist/components/verbs/index.d.ts +1 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/index.js +1 -0
- package/dist/components/verbs/member-outputs.d.ts +187 -0
- package/dist/components/verbs/member-outputs.d.ts.map +1 -0
- package/dist/components/verbs/member-outputs.js +218 -0
- package/dist/components/wave-plan.d.ts +2 -0
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +4 -1
- package/dist/config.d.ts +60 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +27 -0
- package/dist/deep-observation.d.ts +1 -13
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/deep-observation.js +5 -22
- package/dist/drift-record.d.ts +542 -0
- package/dist/drift-record.d.ts.map +1 -0
- package/dist/drift-record.js +886 -0
- package/dist/engine-facts.d.ts +144 -0
- package/dist/engine-facts.d.ts.map +1 -0
- package/dist/engine-facts.js +14 -0
- package/dist/graph-ir.d.ts +153 -2
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ir.js +224 -6
- package/dist/graph-refs.d.ts +145 -0
- package/dist/graph-refs.d.ts.map +1 -1
- package/dist/graph-refs.js +305 -0
- package/dist/graph-schema/fixtures/adopt.json +15 -0
- package/dist/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
- package/dist/graph-schema/fixtures/index.json +68 -0
- package/dist/graph-schema/fixtures/plunge.engine-graph.json +158 -0
- package/dist/graph-schema/fixtures.d.ts +23 -0
- package/dist/graph-schema/fixtures.d.ts.map +1 -0
- package/dist/graph-schema/fixtures.js +246 -0
- package/dist/graph-schema/generate.d.ts +15 -0
- package/dist/graph-schema/generate.d.ts.map +1 -0
- package/dist/graph-schema/generate.js +267 -0
- package/dist/graph-schema/graph.schema.json +1387 -0
- package/dist/health.schema.json +2 -1
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/instance-nodes.d.ts +159 -0
- package/dist/instance-nodes.d.ts.map +1 -0
- package/dist/instance-nodes.js +231 -0
- package/dist/lexicon.d.ts +72 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/adoption-file.d.ts +56 -0
- package/dist/lifecycle/adoption-file.d.ts.map +1 -0
- package/dist/lifecycle/adoption-file.js +123 -0
- package/dist/lifecycle/cloud-index.d.ts +186 -0
- package/dist/lifecycle/cloud-index.d.ts.map +1 -0
- package/dist/lifecycle/cloud-index.js +618 -0
- package/dist/lifecycle/derive-edges.d.ts +59 -0
- package/dist/lifecycle/derive-edges.d.ts.map +1 -0
- package/dist/lifecycle/derive-edges.js +126 -0
- package/dist/lifecycle/git.d.ts +10 -9
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/git.js +7 -1
- package/dist/lifecycle/identity-match.d.ts +133 -0
- package/dist/lifecycle/identity-match.d.ts.map +1 -0
- package/dist/lifecycle/identity-match.js +147 -0
- package/dist/lifecycle/index-diff.d.ts +224 -0
- package/dist/lifecycle/index-diff.d.ts.map +1 -0
- package/dist/lifecycle/index-diff.js +327 -0
- package/dist/lifecycle/index-document.d.ts +130 -0
- package/dist/lifecycle/index-document.d.ts.map +1 -0
- package/dist/lifecycle/index-document.js +189 -0
- package/dist/lifecycle/index-drift.d.ts +121 -0
- package/dist/lifecycle/index-drift.d.ts.map +1 -0
- package/dist/lifecycle/index-drift.js +178 -0
- package/dist/lifecycle/index-import.d.ts +168 -0
- package/dist/lifecycle/index-import.d.ts.map +1 -0
- package/dist/lifecycle/index-import.js +515 -0
- package/dist/lifecycle/index-join.d.ts +129 -0
- package/dist/lifecycle/index-join.d.ts.map +1 -0
- package/dist/lifecycle/index-join.js +248 -0
- package/dist/lifecycle/index.d.ts +4 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +4 -0
- package/dist/lifecycle/observation-baseline.d.ts +61 -21
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
- package/dist/lifecycle/observation-baseline.js +144 -19
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/observe.js +6 -1
- package/dist/lifecycle/query.d.ts +45 -4
- package/dist/lifecycle/query.d.ts.map +1 -1
- package/dist/lifecycle/query.js +89 -7
- package/dist/lifecycle/replay.d.ts +10 -3
- package/dist/lifecycle/replay.d.ts.map +1 -1
- package/dist/lifecycle/replay.js +10 -6
- package/dist/lifecycle/scenario.d.ts +4 -3
- package/dist/lifecycle/scenario.d.ts.map +1 -1
- package/dist/lifecycle/snapshot-store-fake.d.ts +9 -0
- package/dist/lifecycle/snapshot-store-fake.d.ts.map +1 -0
- package/dist/lifecycle/snapshot-store-fake.js +21 -0
- package/dist/lifecycle/snapshot-store-s3.d.ts +102 -0
- package/dist/lifecycle/snapshot-store-s3.d.ts.map +1 -0
- package/dist/lifecycle/snapshot-store-s3.js +254 -0
- package/dist/lifecycle/snapshot-store.d.ts +170 -0
- package/dist/lifecycle/snapshot-store.d.ts.map +1 -0
- package/dist/lifecycle/snapshot-store.js +832 -0
- package/dist/lifecycle/snapshot.d.ts +11 -0
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.js +18 -5
- package/dist/lifecycle/type-listing.d.ts +148 -0
- package/dist/lifecycle/type-listing.d.ts.map +1 -0
- package/dist/lifecycle/type-listing.js +166 -0
- package/dist/lint/component-checks.d.ts +15 -1
- package/dist/lint/component-checks.d.ts.map +1 -1
- package/dist/lint/component-checks.js +1 -0
- package/dist/lint/presets/relaxed.json +2 -1
- package/dist/lint/presets/strict.json +2 -1
- package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts +4 -0
- package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts.map +1 -0
- package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.js +13 -0
- package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts +4 -0
- package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts.map +1 -0
- package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.js +8 -0
- package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts +33 -0
- package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts.map +1 -0
- package/dist/lint/rules/comp/comp008-applies-signal-only-root.js +123 -0
- package/dist/lint/rules/comp/index.d.ts +2 -1
- package/dist/lint/rules/comp/index.d.ts.map +1 -1
- package/dist/lint/rules/comp/index.js +4 -1
- package/dist/observation.d.ts +19 -2
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +18 -1
- package/dist/op/activities/activity-contracts.d.ts +1 -1
- package/dist/op/activities/activity-contracts.js +1 -1
- package/dist/op/activities/change-set.d.ts +6 -4
- package/dist/op/activities/change-set.d.ts.map +1 -1
- package/dist/op/activities/change-set.js +4 -2
- package/dist/op/activity-failure.d.ts +27 -0
- package/dist/op/activity-failure.d.ts.map +1 -1
- package/dist/op/activity-failure.js +31 -0
- package/dist/op/index.d.ts +2 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +1 -1
- package/dist/op/local-executor.d.ts +5 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +22 -8
- package/dist/pr-loop.d.ts +92 -3
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +62 -3
- package/dist/pulumi-urn.d.ts +101 -0
- package/dist/pulumi-urn.d.ts.map +1 -0
- package/dist/pulumi-urn.js +131 -0
- package/dist/release-floors.d.ts +23 -0
- package/dist/release-floors.d.ts.map +1 -0
- package/dist/release-floors.js +22 -0
- package/dist/sensitive-key.d.ts +23 -0
- package/dist/sensitive-key.d.ts.map +1 -0
- package/dist/sensitive-key.js +31 -0
- package/dist/wave-runtime.mjs +234 -234
- package/dist/workspace/adoption-report.d.ts +53 -0
- package/dist/workspace/adoption-report.d.ts.map +1 -0
- package/dist/workspace/adoption-report.js +76 -0
- package/dist/workspace/capabilities-contract.d.ts +7 -0
- package/dist/workspace/capabilities-contract.d.ts.map +1 -1
- package/dist/workspace/capabilities-contract.js +2 -1
- package/dist/workspace/capabilities.schema.json +98 -0
- package/dist/workspace/change-set.schema.json +42 -3
- package/dist/workspace/ci-cloud-identity.d.ts +13 -1
- package/dist/workspace/ci-cloud-identity.d.ts.map +1 -1
- package/dist/workspace/ci-cloud-identity.js +30 -1
- package/dist/workspace/compose-graph.d.ts +10 -1
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/compose-graph.js +6 -1
- package/dist/workspace/conformance/index.d.ts +1 -1
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/conformance/index.js +6 -1
- package/dist/workspace/drift-cli.d.ts +81 -0
- package/dist/workspace/drift-cli.d.ts.map +1 -0
- package/dist/workspace/drift-cli.js +317 -0
- package/dist/workspace/drift.schema.json +672 -0
- package/dist/workspace/graph-cli.d.ts +2 -0
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/graph-cli.js +3 -0
- package/dist/workspace/graph.schema.json +72 -0
- package/dist/workspace/init.d.ts +5 -1
- package/dist/workspace/init.d.ts.map +1 -1
- package/dist/workspace/init.js +55 -9
- package/dist/workspace/ledger.schema.json +1 -1
- package/dist/workspace/nested-graph.d.ts.map +1 -1
- package/dist/workspace/nested-graph.js +2 -0
- package/dist/workspace/pr-report.schema.json +76 -3
- package/dist/workspace/promote-waves.d.ts +112 -0
- package/dist/workspace/promote-waves.d.ts.map +1 -0
- package/dist/workspace/promote-waves.js +290 -0
- package/dist/workspace/promote.d.ts +84 -2
- package/dist/workspace/promote.d.ts.map +1 -1
- package/dist/workspace/promote.js +347 -61
- package/dist/workspace/reason-codes.d.ts +12 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +14 -0
- package/dist/workspace/search-contract.d.ts +3 -3
- package/dist/workspace/search-contract.d.ts.map +1 -1
- package/dist/workspace/search-contract.js +2 -0
- package/dist/workspace/search.schema.json +103 -5
- package/package.json +14 -1
- package/src/__fixtures__/drift-record/README.md +58 -0
- package/src/__fixtures__/drift-record/choudoufu-cone.ndjson +4 -0
- package/src/__fixtures__/drift-record/choudoufu-resource.ndjson +3 -0
- package/src/__fixtures__/drift-record/component-status-ledger.json +65 -0
- package/src/__fixtures__/drift-record/component-status.json +91 -0
- package/src/__fixtures__/drift-record/deep-diff.json +259 -0
- package/src/__fixtures__/drift-record/lifecycle-diff-live.json +825 -0
- package/src/__fixtures__/drift-record/live-diff.json +125 -0
- package/src/audit/discover.e2e.test.ts +14 -0
- package/src/audit/discover.ts +3 -1
- package/src/behaviour.ts +1 -1
- package/src/capabilities/templates.json +145 -0
- package/src/capabilities/templates.test.ts +212 -0
- package/src/capabilities/templates.ts +204 -0
- package/src/change-set-bundle.test.ts +83 -0
- package/src/change-set-graph.ts +80 -9
- package/src/change-set.test.ts +24 -0
- package/src/change-set.ts +77 -14
- package/src/ci-identity-targets.test.ts +23 -0
- package/src/ci-identity.ts +10 -4
- package/src/cli/commands/import-index.test.ts +117 -0
- package/src/cli/commands/import-index.ts +265 -0
- package/src/cli/commands/lint.ts +4 -2
- package/src/cli/handlers/components.test.ts +9 -0
- package/src/cli/handlers/components.ts +12 -2
- package/src/cli/handlers/fan-out.ts +17 -3
- package/src/cli/handlers/graph.test.ts +42 -0
- package/src/cli/handlers/graph.ts +26 -4
- package/src/cli/handlers/index-cloud.ts +32 -0
- package/src/cli/handlers/index-diff.test.ts +68 -0
- package/src/cli/handlers/index-diff.ts +146 -0
- package/src/cli/handlers/ledger-root-member.test.ts +161 -0
- package/src/cli/handlers/lifecycle-drift.ts +68 -0
- package/src/cli/handlers/lifecycle.test.ts +33 -11
- package/src/cli/handlers/lifecycle.ts +205 -30
- package/src/cli/handlers/misc.ts +73 -0
- package/src/cli/handlers/pr-waves-blocked.test.ts +37 -2
- package/src/cli/handlers/pr.ts +16 -3
- package/src/cli/handlers/promote.test.ts +64 -0
- package/src/cli/handlers/promote.ts +81 -6
- package/src/cli/handlers/query.ts +27 -4
- package/src/cli/handlers/run-declared-reads-cli.test.ts +187 -0
- package/src/cli/handlers/run-groups-cli.test.ts +229 -0
- package/src/cli/handlers/scenario.test.ts +13 -0
- package/src/cli/handlers/scenario.ts +8 -7
- package/src/cli/handlers/search-index.test.ts +255 -0
- package/src/cli/handlers/search-json.test.ts +1 -1
- package/src/cli/handlers/search-ownership.test.ts +194 -0
- package/src/cli/handlers/search.ts +271 -38
- package/src/cli/main.test.ts +7 -0
- package/src/cli/main.ts +56 -1
- package/src/cli/mcp/lifecycle-tools.ts +3 -2
- package/src/cli/mcp/resource-handlers.ts +4 -3
- package/src/cli/mcp/workspace-tools.ts +1 -1
- package/src/cli/parse-args.ts +40 -2
- package/src/cli/registry.ts +19 -1
- package/src/cloud-lister/aws.test.ts +202 -0
- package/src/cloud-lister/aws.ts +226 -0
- package/src/cloud-lister/azure.test.ts +132 -0
- package/src/cloud-lister/azure.ts +142 -0
- package/src/cloud-lister/common.ts +115 -0
- package/src/cloud-lister/credentials.ts +110 -0
- package/src/cloud-lister/fakes.ts +66 -0
- package/src/cloud-lister/gcp.test.ts +121 -0
- package/src/cloud-lister/gcp.ts +159 -0
- package/src/cloud-lister/guard.test.ts +198 -0
- package/src/cloud-lister/guard.ts +111 -0
- package/src/cloud-lister/ids.ts +54 -0
- package/src/cloud-lister/index.ts +134 -0
- package/src/cloud-lister/kubernetes.test.ts +160 -0
- package/src/cloud-lister/kubernetes.ts +284 -0
- package/src/cloud-lister/markers.ts +74 -0
- package/src/cloud-lister/mask.ts +77 -0
- package/src/cloud-lister/shape.test.ts +81 -0
- package/src/cloud-lister/sigv4.ts +70 -0
- package/src/cloud-lister/transport.ts +118 -0
- package/src/cloud-lister/types.ts +135 -0
- package/src/components/capability-plugin-loader.ts +4 -0
- package/src/components/capability-plugin.ts +10 -0
- package/src/components/capability.ts +238 -0
- package/src/components/cli-support.ts +70 -14
- package/src/components/covered-round.test.ts +166 -0
- package/src/components/covered-round.ts +293 -0
- package/src/components/declared-reads.test.ts +103 -0
- package/src/components/declared-reads.ts +134 -0
- package/src/components/deploy-units.ts +4 -0
- package/src/components/driver.ts +101 -4
- package/src/components/fan-out-run.ts +95 -15
- package/src/components/fan-out-support.ts +10 -1
- package/src/components/fan-out.ts +30 -0
- package/src/components/groups.test.ts +320 -0
- package/src/components/groups.ts +880 -0
- package/src/components/index.ts +59 -0
- package/src/components/plan-shape-conformance.ts +130 -0
- package/src/components/pr-env-run-bundle.test.ts +2 -0
- package/src/components/pr-run.test.ts +728 -4
- package/src/components/pr-run.ts +395 -55
- package/src/components/promote-rounds.ts +25 -2
- package/src/components/promote.ts +55 -5
- package/src/components/registry.test.ts +1 -1
- package/src/components/release-gate.ts +115 -30
- package/src/components/release-groups.test.ts +362 -0
- package/src/components/run-groups.ts +113 -0
- package/src/components/starter-plugin.ts +6 -0
- package/src/components/verbs/index.ts +1 -0
- package/src/components/verbs/member-outputs.test.ts +205 -0
- package/src/components/verbs/member-outputs.ts +308 -0
- package/src/components/wave-plan.ts +5 -2
- package/src/config.ts +44 -0
- package/src/deep-observation.ts +5 -23
- package/src/drift-record.test.ts +596 -0
- package/src/drift-record.ts +1206 -0
- package/src/engine-facts.guard.test.ts +111 -0
- package/src/engine-facts.test.ts +178 -0
- package/src/engine-facts.ts +154 -0
- package/src/gated-waves-bundle.test.ts +2 -0
- package/src/graph-ir-live.test.ts +92 -0
- package/src/graph-ir.ts +317 -7
- package/src/graph-refs.ts +416 -0
- package/src/graph-schema/fixtures/adopt.json +15 -0
- package/src/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
- package/src/graph-schema/fixtures/index.json +68 -0
- package/src/graph-schema/fixtures/plunge.engine-graph.json +158 -0
- package/src/graph-schema/fixtures.ts +267 -0
- package/src/graph-schema/generate.ts +263 -0
- package/src/graph-schema/graph-schema.test.ts +153 -0
- package/src/graph-schema/graph.schema.json +1387 -0
- package/src/health.schema.json +2 -1
- package/src/identity.ts +2 -0
- package/src/index.ts +2 -0
- package/src/instance-nodes.ts +288 -0
- package/src/lexicon.ts +76 -0
- package/src/lifecycle/__fixtures__/index-diff/20261026T120000Z-aaaaaaaaaaaa.json +149 -0
- package/src/lifecycle/__fixtures__/index-diff/20261026T130000Z-bbbbbbbbbbbb.json +127 -0
- package/src/lifecycle/adoption-file.test.ts +100 -0
- package/src/lifecycle/adoption-file.ts +154 -0
- package/src/lifecycle/cloud-index.test.ts +517 -0
- package/src/lifecycle/cloud-index.ts +736 -0
- package/src/lifecycle/derive-edges.test.ts +193 -0
- package/src/lifecycle/derive-edges.ts +182 -0
- package/src/lifecycle/git.ts +10 -4
- package/src/lifecycle/identity-match.test.ts +308 -0
- package/src/lifecycle/identity-match.ts +257 -0
- package/src/lifecycle/index-diff.test.ts +198 -0
- package/src/lifecycle/index-diff.ts +476 -0
- package/src/lifecycle/index-document.test.ts +64 -0
- package/src/lifecycle/index-document.ts +299 -0
- package/src/lifecycle/index-drift.test.ts +191 -0
- package/src/lifecycle/index-drift.ts +304 -0
- package/src/lifecycle/index-import.test.ts +255 -0
- package/src/lifecycle/index-import.ts +634 -0
- package/src/lifecycle/index-join.test.ts +250 -0
- package/src/lifecycle/index-join.ts +304 -0
- package/src/lifecycle/index-store-load-bearing.test.ts +187 -0
- package/src/lifecycle/index.ts +4 -0
- package/src/lifecycle/member-ledger.test.ts +25 -7
- package/src/lifecycle/observation-baseline.test.ts +72 -1
- package/src/lifecycle/observation-baseline.ts +190 -27
- package/src/lifecycle/observe.ts +6 -1
- package/src/lifecycle/query.test.ts +69 -1
- package/src/lifecycle/query.ts +103 -10
- package/src/lifecycle/replay.test.ts +9 -5
- package/src/lifecycle/replay.ts +9 -5
- package/src/lifecycle/scenario.ts +4 -3
- package/src/lifecycle/snapshot-store-fake.ts +29 -0
- package/src/lifecycle/snapshot-store-s3.ts +316 -0
- package/src/lifecycle/snapshot-store.test.ts +385 -0
- package/src/lifecycle/snapshot-store.ts +981 -0
- package/src/lifecycle/snapshot.test.ts +13 -0
- package/src/lifecycle/snapshot.ts +23 -6
- package/src/lifecycle/type-listing.test.ts +166 -0
- package/src/lifecycle/type-listing.ts +309 -0
- package/src/lint/component-checks.ts +17 -1
- package/src/lint/config.test.ts +3 -0
- package/src/lint/presets/relaxed.json +2 -1
- package/src/lint/presets/strict.json +2 -1
- package/src/lint/rules/__fixtures__/comp/comp008/fail/dns.component.ts +15 -0
- package/src/lint/rules/__fixtures__/comp/comp008/pass/dns.component.ts +10 -0
- package/src/lint/rules/comp/comp.test.ts +2 -1
- package/src/lint/rules/comp/comp008-applies-signal-only-root.ts +121 -0
- package/src/lint/rules/comp/comp008.test.ts +77 -0
- package/src/lint/rules/comp/index.ts +4 -1
- package/src/observation-edges.test.ts +33 -0
- package/src/observation.ts +36 -1
- package/src/op/activities/activity-contracts.ts +1 -1
- package/src/op/activities/change-set.ts +8 -5
- package/src/op/activity-failure.ts +36 -0
- package/src/op/index.ts +2 -1
- package/src/op/local-executor.test.ts +80 -0
- package/src/op/local-executor.ts +27 -9
- package/src/pr-loop-bundle.test.ts +5 -2
- package/src/pr-loop.test.ts +57 -1
- package/src/pr-loop.ts +154 -6
- package/src/pulumi-urn.ts +151 -0
- package/src/release-floors.ts +22 -0
- package/src/sensitive-key.ts +34 -0
- package/src/workspace/adoption-report.test.ts +61 -0
- package/src/workspace/adoption-report.ts +103 -0
- package/src/workspace/capabilities-contract.test.ts +4 -0
- package/src/workspace/capabilities-contract.ts +9 -1
- package/src/workspace/capabilities.schema.json +98 -0
- package/src/workspace/change-set.schema.json +42 -3
- package/src/workspace/ci-cloud-identity.ts +31 -1
- package/src/workspace/compose-graph.test.ts +12 -0
- package/src/workspace/compose-graph.ts +12 -1
- package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
- package/src/workspace/conformance/index.ts +6 -1
- package/src/workspace/drift-cli.test.ts +65 -0
- package/src/workspace/drift-cli.ts +365 -0
- package/src/workspace/drift.schema.json +672 -0
- package/src/workspace/graph-cli.ts +5 -0
- package/src/workspace/graph.schema.json +72 -0
- package/src/workspace/init.test.ts +38 -1
- package/src/workspace/init.ts +50 -8
- package/src/workspace/intent-gaps.test.ts +18 -2
- package/src/workspace/ledger.schema.json +1 -1
- package/src/workspace/nested-graph.ts +1 -0
- package/src/workspace/pr-report.schema.json +76 -3
- package/src/workspace/promote-waves.test.ts +296 -0
- package/src/workspace/promote-waves.ts +340 -0
- package/src/workspace/promote.test.ts +158 -4
- package/src/workspace/promote.ts +398 -57
- package/src/workspace/reason-codes.test.ts +4 -1
- package/src/workspace/reason-codes.ts +14 -0
- package/src/workspace/search-contract.ts +2 -0
- package/src/workspace/search.schema.json +103 -5
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* behold#1029: where the lister's credentials come from.
|
|
3
|
+
*
|
|
4
|
+
* The goal's hard rule: against an emulator the lister takes explicit
|
|
5
|
+
* endpoints and static credentials only, and never the AWS SDK's default
|
|
6
|
+
* provider chain, Azure's DefaultAzureCredential or google-auth's
|
|
7
|
+
* application-default credentials. So:
|
|
8
|
+
*
|
|
9
|
+
* - a target that names an `endpoint` must carry its credentials (a static
|
|
10
|
+
* AWS key pair, an Azure bearer token, a GCP token or an explicit `null`
|
|
11
|
+
* for an anonymous emulator). Without them the run is refused with
|
|
12
|
+
* {@link ListerConfigError}, and no chain is loaded;
|
|
13
|
+
* - only a target with no endpoint (a real cloud, which the maintainer runs)
|
|
14
|
+
* may fall back to a cloud SDK's chain. Those SDKs are optional peers,
|
|
15
|
+
* imported lazily by name, so chant's install and bundle do not grow for
|
|
16
|
+
* people who never index; their absence is a clear error naming the
|
|
17
|
+
* package.
|
|
18
|
+
*
|
|
19
|
+
* The lister itself reads no environment variable. `chant index` (chant#4532)
|
|
20
|
+
* turns its flags into these targets.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export class ListerConfigError extends Error {
|
|
24
|
+
constructor(message: string) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "ListerConfigError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AwsStaticCredentials {
|
|
31
|
+
accessKeyId: string;
|
|
32
|
+
secretAccessKey: string;
|
|
33
|
+
sessionToken?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The SDK chains a real-cloud run may use; replaced in tests so a test can prove none was touched. */
|
|
37
|
+
export interface CredentialChains {
|
|
38
|
+
aws(): Promise<AwsStaticCredentials>;
|
|
39
|
+
azure(): Promise<string>;
|
|
40
|
+
gcp(): Promise<string>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Imports a module by a name the bundler and the type checker do not follow. */
|
|
44
|
+
async function optionalPeer(name: string): Promise<Record<string, unknown>> {
|
|
45
|
+
const specifier = name;
|
|
46
|
+
try {
|
|
47
|
+
return (await import(specifier)) as Record<string, unknown>;
|
|
48
|
+
} catch {
|
|
49
|
+
throw new ListerConfigError(`listing a real cloud without explicit credentials needs the optional package ${name}; install it beside chant, or pass credentials`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type AnyFn = (...args: unknown[]) => unknown;
|
|
54
|
+
|
|
55
|
+
export const sdkCredentialChains: CredentialChains = {
|
|
56
|
+
async aws() {
|
|
57
|
+
const mod = await optionalPeer("@aws-sdk/credential-providers");
|
|
58
|
+
const chain = (mod.fromNodeProviderChain as AnyFn)() as () => Promise<AwsStaticCredentials>;
|
|
59
|
+
const c = await chain();
|
|
60
|
+
return { accessKeyId: c.accessKeyId, secretAccessKey: c.secretAccessKey, ...(c.sessionToken ? { sessionToken: c.sessionToken } : {}) };
|
|
61
|
+
},
|
|
62
|
+
async azure() {
|
|
63
|
+
const mod = await optionalPeer("@azure/identity");
|
|
64
|
+
const Ctor = mod.DefaultAzureCredential as new () => { getToken(scope: string): Promise<{ token: string } | null> };
|
|
65
|
+
const t = await new Ctor().getToken("https://management.azure.com/.default");
|
|
66
|
+
if (!t) throw new ListerConfigError("DefaultAzureCredential returned no token");
|
|
67
|
+
return t.token;
|
|
68
|
+
},
|
|
69
|
+
async gcp() {
|
|
70
|
+
const mod = await optionalPeer("google-auth-library");
|
|
71
|
+
const Ctor = mod.GoogleAuth as new (o: { scopes: string[] }) => { getAccessToken(): Promise<string | null | undefined> };
|
|
72
|
+
const t = await new Ctor({ scopes: ["https://www.googleapis.com/auth/cloud-platform.read-only"] }).getAccessToken();
|
|
73
|
+
if (!t) throw new ListerConfigError("google-auth returned no access token");
|
|
74
|
+
return t;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export async function resolveAwsCredentials(
|
|
79
|
+
target: { endpoint?: string; credentials?: AwsStaticCredentials },
|
|
80
|
+
chains: CredentialChains,
|
|
81
|
+
): Promise<AwsStaticCredentials> {
|
|
82
|
+
if (target.credentials) return target.credentials;
|
|
83
|
+
if (target.endpoint !== undefined) {
|
|
84
|
+
throw new ListerConfigError(`aws endpoint ${target.endpoint} needs static credentials (accessKeyId and secretAccessKey); the default provider chain is never used with an endpoint`);
|
|
85
|
+
}
|
|
86
|
+
return chains.aws();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function resolveAzureToken(
|
|
90
|
+
target: { endpoint?: string; token?: string },
|
|
91
|
+
chains: CredentialChains,
|
|
92
|
+
): Promise<string> {
|
|
93
|
+
if (target.token !== undefined) return target.token;
|
|
94
|
+
if (target.endpoint !== undefined) {
|
|
95
|
+
throw new ListerConfigError(`azure endpoint ${target.endpoint} needs a static bearer token; DefaultAzureCredential is never used with an endpoint`);
|
|
96
|
+
}
|
|
97
|
+
return chains.azure();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** `null` is an explicit anonymous call (floci-gcp with IAM enforcement off). */
|
|
101
|
+
export async function resolveGcpToken(
|
|
102
|
+
target: { endpoint?: string; token?: string | null },
|
|
103
|
+
chains: CredentialChains,
|
|
104
|
+
): Promise<string | null> {
|
|
105
|
+
if (target.token !== undefined) return target.token;
|
|
106
|
+
if (target.endpoint !== undefined) {
|
|
107
|
+
throw new ListerConfigError(`gcp endpoint ${target.endpoint} needs a static token, or token: null for an anonymous emulator; application-default credentials are never used with an endpoint`);
|
|
108
|
+
}
|
|
109
|
+
return chains.gcp();
|
|
110
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* behold#1029: test doubles for the lister. Nothing here opens a socket: a
|
|
3
|
+
* fake fetcher answers from a route function and throws on any call it was
|
|
4
|
+
* not given an answer for, and fake credential chains count their calls so a
|
|
5
|
+
* test can show none was touched.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { WireCall } from "./guard";
|
|
9
|
+
import type { CredentialChains } from "./credentials";
|
|
10
|
+
import type { Fetcher, WireResponse } from "./transport";
|
|
11
|
+
|
|
12
|
+
export function json(status: number, body: unknown, headers: Record<string, string> = {}): WireResponse {
|
|
13
|
+
return { status, headers: { "content-type": "application/json", ...headers }, body: JSON.stringify(body) };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FakeFetcher {
|
|
17
|
+
fetcher: Fetcher;
|
|
18
|
+
calls: WireCall[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function fakeFetcher(route: (call: WireCall) => WireResponse | undefined): FakeFetcher {
|
|
22
|
+
const calls: WireCall[] = [];
|
|
23
|
+
return {
|
|
24
|
+
calls,
|
|
25
|
+
fetcher: async (call) => {
|
|
26
|
+
calls.push(call);
|
|
27
|
+
const res = route(call);
|
|
28
|
+
if (!res) throw new Error(`fake fetcher has no answer for ${call.method} ${call.url}`);
|
|
29
|
+
return res;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CountingChains extends CredentialChains {
|
|
35
|
+
count: { aws: number; azure: number; gcp: number };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Chains that fail if used; `count` says how often each was asked. */
|
|
39
|
+
export function refusingChains(): CountingChains {
|
|
40
|
+
const count = { aws: 0, azure: 0, gcp: 0 };
|
|
41
|
+
return {
|
|
42
|
+
count,
|
|
43
|
+
async aws() {
|
|
44
|
+
count.aws++;
|
|
45
|
+
throw new Error("the AWS default provider chain was asked for credentials");
|
|
46
|
+
},
|
|
47
|
+
async azure() {
|
|
48
|
+
count.azure++;
|
|
49
|
+
throw new Error("DefaultAzureCredential was asked for a token");
|
|
50
|
+
},
|
|
51
|
+
async gcp() {
|
|
52
|
+
count.gcp++;
|
|
53
|
+
throw new Error("application-default credentials were asked for a token");
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A fixed clock that advances one second per reading, so timestamps are predictable. */
|
|
59
|
+
export function steppingClock(start = "2026-10-26T12:00:00Z"): () => Date {
|
|
60
|
+
let t = Date.parse(start);
|
|
61
|
+
return () => {
|
|
62
|
+
const d = new Date(t);
|
|
63
|
+
t += 1000;
|
|
64
|
+
return d;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* behold#1029: the GCP lister against a fake Cloud Asset Inventory. Shapes
|
|
3
|
+
* follow the pinned floci-gcp (source 641e6e8): `GET
|
|
4
|
+
* /cloudasset/v1/projects/{p}:searchAllResources`, `{ results, nextPageToken }`,
|
|
5
|
+
* and SERVICE_DISABLED as ServiceUsageGate writes it.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { listCloud } from "./index";
|
|
9
|
+
import { enableCommand, labelQuery } from "./gcp";
|
|
10
|
+
import { fakeFetcher, json, refusingChains, steppingClock } from "./fakes";
|
|
11
|
+
|
|
12
|
+
const PROJECT = "plc-corpus";
|
|
13
|
+
const ENDPOINT = "http://localhost:4588/cloudasset";
|
|
14
|
+
const BUCKET = "//storage.googleapis.com/projects/_/buckets/plc-assets";
|
|
15
|
+
const TOPIC = `//pubsub.googleapis.com/projects/${PROJECT}/topics/events`;
|
|
16
|
+
|
|
17
|
+
const serviceDisabled = json(403, {
|
|
18
|
+
error: {
|
|
19
|
+
code: 403,
|
|
20
|
+
message: `Cloud Asset API has not been used in project ${PROJECT} before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/cloudasset.googleapis.com/overview?project=${PROJECT} then retry.`,
|
|
21
|
+
status: "PERMISSION_DENIED",
|
|
22
|
+
errors: [{ message: "x", domain: "usageLimits", reason: "accessNotConfigured" }],
|
|
23
|
+
details: [{ "@type": "type.googleapis.com/google.rpc.ErrorInfo", reason: "SERVICE_DISABLED", domain: "googleapis.com", metadata: { service: "cloudasset.googleapis.com", consumer: `projects/${PROJECT}` } }],
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("listing gcp", () => {
|
|
28
|
+
it("searches the project, pages, and fills the listing-result shape", async () => {
|
|
29
|
+
const fake = fakeFetcher((call) => {
|
|
30
|
+
const url = new URL(call.url);
|
|
31
|
+
if (!url.searchParams.get("pageToken")) {
|
|
32
|
+
return json(200, {
|
|
33
|
+
results: [
|
|
34
|
+
{ name: BUCKET, assetType: "storage.googleapis.com/Bucket", project: "projects/123", location: "US", labels: { "plunge-stack": "0123456789abcdef", "plunge-urn": "fedcba9876543210fedcba9876543210" } },
|
|
35
|
+
],
|
|
36
|
+
nextPageToken: "t2",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return json(200, { results: [{ name: TOPIC, assetType: "pubsub.googleapis.com/Topic", project: "projects/123", additionalAttributes: { secretValue: "s" } }] });
|
|
40
|
+
});
|
|
41
|
+
const result = await listCloud(
|
|
42
|
+
{ substrate: "gcp", scopeId: PROJECT, types: "*", locations: "*" },
|
|
43
|
+
{ substrate: "gcp", gcp: { endpoint: ENDPOINT, token: null } },
|
|
44
|
+
{ fetcher: fake.fetcher, chains: refusingChains(), now: steppingClock(), retryDelays: [] },
|
|
45
|
+
);
|
|
46
|
+
expect(fake.calls.length).toBe(2);
|
|
47
|
+
const first = new URL(fake.calls[0].url);
|
|
48
|
+
expect(first.origin + first.pathname).toBe(`${ENDPOINT}/v1/projects/${PROJECT}:searchAllResources`);
|
|
49
|
+
expect(first.searchParams.get("pageSize")).toBe("500");
|
|
50
|
+
expect(fake.calls[0].headers.authorization).toBeUndefined();
|
|
51
|
+
expect(new URL(fake.calls[1].url).searchParams.get("pageToken")).toBe("t2");
|
|
52
|
+
|
|
53
|
+
const bucket = result.resources.find((r) => r.nativeId === BUCKET)!;
|
|
54
|
+
expect(bucket.id).toBe(`gcp/${PROJECT}/us/storage.googleapis.com%2FBucket/${BUCKET}`);
|
|
55
|
+
expect(bucket.location).toBe("us");
|
|
56
|
+
expect(bucket.cloudIds).toEqual([{ kind: "provider-id", value: BUCKET }]);
|
|
57
|
+
expect(bucket.facts.markers).toEqual({ owner: "0123456789abcdef", address: "fedcba9876543210fedcba9876543210", encoding: "digest", source: "live" });
|
|
58
|
+
|
|
59
|
+
const topic = result.resources.find((r) => r.nativeId === TOPIC)!;
|
|
60
|
+
expect(topic.location).toBe("global");
|
|
61
|
+
expect((topic.properties.additionalAttributes as Record<string, unknown>).secretValue).toBe("<masked>");
|
|
62
|
+
expect(result.coverage).toEqual([{ listedType: "*", location: "*", verdict: "listed" }]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("filters locations after the search and puts types and labels in the request", async () => {
|
|
66
|
+
const fake = fakeFetcher(() =>
|
|
67
|
+
json(200, {
|
|
68
|
+
results: [
|
|
69
|
+
{ name: "//compute.googleapis.com/projects/p/zones/us-central1-a/instances/a", assetType: "compute.googleapis.com/Instance", location: "us-central1-a" },
|
|
70
|
+
{ name: "//compute.googleapis.com/projects/p/zones/europe-west1-b/instances/b", assetType: "compute.googleapis.com/Instance", location: "europe-west1-b" },
|
|
71
|
+
],
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
const result = await listCloud(
|
|
75
|
+
{ substrate: "gcp", scopeId: PROJECT, types: ["compute.googleapis.com/Instance"], locations: ["us-central1-a"], tagFilter: { env: "prod" } },
|
|
76
|
+
{ substrate: "gcp", gcp: { endpoint: ENDPOINT, token: "static" } },
|
|
77
|
+
{ fetcher: fake.fetcher, chains: refusingChains(), now: steppingClock(), retryDelays: [] },
|
|
78
|
+
);
|
|
79
|
+
const url = new URL(fake.calls[0].url);
|
|
80
|
+
expect(url.searchParams.getAll("assetTypes")).toEqual(["compute.googleapis.com/Instance"]);
|
|
81
|
+
expect(url.searchParams.get("query")).toBe("labels.env=prod");
|
|
82
|
+
expect(fake.calls[0].headers.authorization).toBe("Bearer static");
|
|
83
|
+
expect(result.resources.map((r) => r.location)).toEqual(["us-central1-a"]);
|
|
84
|
+
expect(result.coverage).toEqual([{ listedType: "compute.googleapis.com/Instance", location: "us-central1-a", verdict: "filtered", detail: 'tag filter {"env":"prod"}' }]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("never enables the Cloud Asset API: says so, prints the command, lists nothing", async () => {
|
|
88
|
+
const fake = fakeFetcher(() => serviceDisabled);
|
|
89
|
+
const result = await listCloud(
|
|
90
|
+
{ substrate: "gcp", scopeId: PROJECT, types: ["storage.googleapis.com/Bucket"], locations: "*" },
|
|
91
|
+
{ substrate: "gcp", gcp: { endpoint: ENDPOINT, token: null } },
|
|
92
|
+
{ fetcher: fake.fetcher, chains: refusingChains(), now: steppingClock(), retryDelays: [] },
|
|
93
|
+
);
|
|
94
|
+
expect(result.resources).toEqual([]);
|
|
95
|
+
expect(result.coverage).toEqual([{ listedType: "storage.googleapis.com/Bucket", location: "*", verdict: "read-failed", detail: `Cloud Asset API is disabled on project ${PROJECT}` }]);
|
|
96
|
+
expect(result.notes).toHaveLength(1);
|
|
97
|
+
expect(result.notes[0].code).toBe("api-disabled");
|
|
98
|
+
expect(result.notes[0].command).toBe(enableCommand(PROJECT));
|
|
99
|
+
expect(result.notes[0].command).toBe("gcloud services enable cloudasset.googleapis.com --project=plc-corpus");
|
|
100
|
+
// One call, the search; nothing went to Service Usage.
|
|
101
|
+
expect(fake.calls).toHaveLength(1);
|
|
102
|
+
expect(fake.calls[0].method).toBe("GET");
|
|
103
|
+
expect(fake.calls[0].url).toContain(":searchAllResources");
|
|
104
|
+
expect(fake.calls.some((c) => /serviceusage|:enable|batchEnable/.test(c.url))).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("never asks application-default credentials when an endpoint is given", async () => {
|
|
108
|
+
const chains = refusingChains();
|
|
109
|
+
const fake = fakeFetcher(() => undefined);
|
|
110
|
+
await expect(
|
|
111
|
+
listCloud({ substrate: "gcp", scopeId: PROJECT, types: "*", locations: "*" }, { substrate: "gcp", gcp: { endpoint: ENDPOINT } }, { fetcher: fake.fetcher, chains }),
|
|
112
|
+
).rejects.toThrow(/application-default credentials are never used/);
|
|
113
|
+
expect(chains.count.gcp).toBe(0);
|
|
114
|
+
expect(fake.calls).toEqual([]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("joins label terms with AND and refuses anything else", () => {
|
|
118
|
+
expect(labelQuery({ a: "1", b: "2" })).toBe("labels.a=1 AND labels.b=2");
|
|
119
|
+
expect(() => labelQuery({ "a OR x": "1" })).toThrow();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* behold#1029: GCP, through Cloud Asset Inventory's `searchAllResources` on
|
|
3
|
+
* one project, paged.
|
|
4
|
+
*
|
|
5
|
+
* Nothing is enabled for the person. The Cloud Asset API has to be on before
|
|
6
|
+
* a project can be searched, and turning it on changes the project. When the
|
|
7
|
+
* search answers SERVICE_DISABLED, the lister lists nothing for the project,
|
|
8
|
+
* marks every coverage entry `read-failed`, and puts the enable command in a
|
|
9
|
+
* note for the person to run. The guard (packages/core/src/cloud-lister/guard.ts) admits no
|
|
10
|
+
* Service Usage call at all, so the lister could not enable it if it tried.
|
|
11
|
+
*
|
|
12
|
+
* Types go in `assetTypes`, a tag filter in the query as `labels.KEY=VALUE`
|
|
13
|
+
* terms. Locations are filtered here, after the search, because floci-gcp's
|
|
14
|
+
* query subset has no location term.
|
|
15
|
+
*
|
|
16
|
+
* The pinned floci-gcp (ghcr.io/lex00/floci-gcp, source 641e6e8) serves this
|
|
17
|
+
* call under the path prefix `/cloudasset` (pass `http://localhost:4588/cloudasset`
|
|
18
|
+
* as the endpoint), for project scopes only, over the assets of four
|
|
19
|
+
* services (Compute, Cloud Storage, IAM, Pub/Sub), with SERVICE_DISABLED
|
|
20
|
+
* answered as GCP answers it when its Service Usage enforcement is on.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { buildResource, describeFailure, listedVerdict, noteTagFilter, withThrottleRetry, type ListerContext } from "./common";
|
|
24
|
+
import { resolveGcpToken, ListerConfigError, type CredentialChains } from "./credentials";
|
|
25
|
+
import { jsonBody, type TlsOptions, type WireResponse } from "./transport";
|
|
26
|
+
import type { CloudId } from "./types";
|
|
27
|
+
|
|
28
|
+
export interface GcpTarget {
|
|
29
|
+
/** An emulator's Cloud Asset base, e.g. `http://localhost:4588/cloudasset`. Absent means `https://cloudasset.googleapis.com`. */
|
|
30
|
+
endpoint?: string;
|
|
31
|
+
/** An OAuth access token; `null` sends none (an emulator with IAM enforcement off). Required with an endpoint. */
|
|
32
|
+
token?: string | null;
|
|
33
|
+
tls?: TlsOptions;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const PAGE_SIZE = 500;
|
|
37
|
+
const MAX_PAGES = 10_000;
|
|
38
|
+
const PROJECT = /^[A-Za-z0-9._:-]+$/;
|
|
39
|
+
const LABEL_PART = /^[A-Za-z0-9_\-]+$/;
|
|
40
|
+
|
|
41
|
+
export function enableCommand(project: string): string {
|
|
42
|
+
return `gcloud services enable cloudasset.googleapis.com --project=${project}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function searchUrl(target: GcpTarget, project: string, params: { assetTypes?: readonly string[]; query?: string; pageToken?: string }): string {
|
|
46
|
+
const base = (target.endpoint ?? "https://cloudasset.googleapis.com").replace(/\/+$/, "");
|
|
47
|
+
const url = new URL(`${base}/v1/projects/${encodeURIComponent(project)}:searchAllResources`);
|
|
48
|
+
for (const t of params.assetTypes ?? []) url.searchParams.append("assetTypes", t);
|
|
49
|
+
if (params.query) url.searchParams.set("query", params.query);
|
|
50
|
+
url.searchParams.set("pageSize", String(PAGE_SIZE));
|
|
51
|
+
if (params.pageToken) url.searchParams.set("pageToken", params.pageToken);
|
|
52
|
+
return url.toString();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The `query` for a tag filter, or undefined. Exported for tests. */
|
|
56
|
+
export function labelQuery(filter: Readonly<Record<string, string>> | undefined): string | undefined {
|
|
57
|
+
const entries = Object.entries(filter ?? {});
|
|
58
|
+
if (entries.length === 0) return undefined;
|
|
59
|
+
return entries
|
|
60
|
+
.map(([k, v]) => {
|
|
61
|
+
if (!LABEL_PART.test(k) || !LABEL_PART.test(v)) throw new ListerConfigError(`gcp label filter ${k}=${v} is not a plain label key and value`);
|
|
62
|
+
return `labels.${k}=${v}`;
|
|
63
|
+
})
|
|
64
|
+
.join(" AND ");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface GcpError {
|
|
68
|
+
status: string;
|
|
69
|
+
message: string;
|
|
70
|
+
reasons: string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function gcpError(res: WireResponse): GcpError {
|
|
74
|
+
const body = jsonBody(res) as
|
|
75
|
+
| { error?: { status?: string; message?: string; details?: Array<{ reason?: string }>; errors?: Array<{ reason?: string }> } }
|
|
76
|
+
| Array<{ error?: { status?: string; message?: string; details?: Array<{ reason?: string }> } }>
|
|
77
|
+
| undefined;
|
|
78
|
+
const e = Array.isArray(body) ? body[0]?.error : body?.error;
|
|
79
|
+
const reasons = [...(e?.details ?? []), ...((e as { errors?: Array<{ reason?: string }> } | undefined)?.errors ?? [])]
|
|
80
|
+
.map((d) => d.reason ?? "")
|
|
81
|
+
.filter(Boolean);
|
|
82
|
+
return { status: e?.status ?? "", message: e?.message ?? "", reasons };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function isServiceDisabled(res: WireResponse): boolean {
|
|
86
|
+
if (res.status !== 403) return false;
|
|
87
|
+
const err = gcpError(res);
|
|
88
|
+
return err.reasons.includes("SERVICE_DISABLED") || err.reasons.includes("accessNotConfigured") || /has not been used in project|is disabled/i.test(err.message);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function listGcp(
|
|
92
|
+
ctx: ListerContext,
|
|
93
|
+
target: GcpTarget,
|
|
94
|
+
chains: CredentialChains,
|
|
95
|
+
retryDelays?: readonly number[],
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
const { scope } = ctx;
|
|
98
|
+
if (!PROJECT.test(scope.scopeId)) throw new ListerConfigError(`gcp project ${JSON.stringify(scope.scopeId)} is not a project id`);
|
|
99
|
+
const token = await resolveGcpToken(target, chains);
|
|
100
|
+
const query = labelQuery(scope.tagFilter);
|
|
101
|
+
const assetTypes = scope.types === "*" ? undefined : scope.types;
|
|
102
|
+
const wantLocations = scope.locations === "*" ? undefined : new Set(scope.locations.map((l) => l.toLowerCase()));
|
|
103
|
+
const typesCovered = scope.types === "*" ? ["*"] : [...scope.types];
|
|
104
|
+
const locationsCovered = scope.locations === "*" ? ["*"] : [...scope.locations].map((l) => l.toLowerCase());
|
|
105
|
+
noteTagFilter(ctx, "as labels terms in the Cloud Asset query");
|
|
106
|
+
|
|
107
|
+
const found: Parameters<typeof buildResource>[1][] = [];
|
|
108
|
+
let pageToken: string | undefined;
|
|
109
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
110
|
+
const url = searchUrl(target, scope.scopeId, { assetTypes, query, pageToken });
|
|
111
|
+
const headers: Record<string, string> = { accept: "application/json" };
|
|
112
|
+
if (token !== null) headers.authorization = `Bearer ${token}`;
|
|
113
|
+
const res = await withThrottleRetry(
|
|
114
|
+
() => ctx.transport.send({ substrate: "gcp", method: "GET", url, headers }),
|
|
115
|
+
(r) => r.status === 429,
|
|
116
|
+
retryDelays,
|
|
117
|
+
);
|
|
118
|
+
const observedAt = ctx.now().toISOString();
|
|
119
|
+
if (res.status >= 400) {
|
|
120
|
+
const err = gcpError(res);
|
|
121
|
+
const detail = describeFailure(res, err.status, err.message);
|
|
122
|
+
if (isServiceDisabled(res)) {
|
|
123
|
+
const why = `Cloud Asset API is disabled on project ${scope.scopeId}`;
|
|
124
|
+
for (const t of typesCovered) for (const l of locationsCovered) ctx.coverage.push({ listedType: t, location: l, verdict: "read-failed", detail: why });
|
|
125
|
+
ctx.notes.push({
|
|
126
|
+
code: "api-disabled",
|
|
127
|
+
message: `${why}, so nothing was listed for it. chant never enables an API; enabling it changes the project. To list it, run the command yourself and index again.`,
|
|
128
|
+
command: enableCommand(scope.scopeId),
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const verdict = res.status === 401 || res.status === 403 ? "no-credentials" : "read-failed";
|
|
133
|
+
for (const t of typesCovered) for (const l of locationsCovered) ctx.coverage.push({ listedType: t, location: l, verdict, detail });
|
|
134
|
+
ctx.notes.push({ code: verdict === "no-credentials" ? "no-credentials" : "read-failed", message: `searchAllResources on project ${scope.scopeId} failed: ${detail}` });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const parsed = jsonBody(res) as { results?: Array<Record<string, unknown>>; nextPageToken?: string } | undefined;
|
|
138
|
+
for (const r of parsed?.results ?? []) {
|
|
139
|
+
const name = typeof r.name === "string" ? r.name : undefined;
|
|
140
|
+
const assetType = typeof r.assetType === "string" ? r.assetType : undefined;
|
|
141
|
+
if (!name || !assetType) continue;
|
|
142
|
+
const location = (typeof r.location === "string" && r.location ? r.location : "global").toLowerCase();
|
|
143
|
+
if (wantLocations && !wantLocations.has(location)) continue;
|
|
144
|
+
const tags: Record<string, string> = {};
|
|
145
|
+
if (r.labels && typeof r.labels === "object") {
|
|
146
|
+
for (const [k, v] of Object.entries(r.labels as Record<string, unknown>)) tags[k] = String(v ?? "");
|
|
147
|
+
}
|
|
148
|
+
const cloudIds: CloudId[] = [{ kind: "provider-id", value: name }];
|
|
149
|
+
const attrs = r.additionalAttributes as Record<string, unknown> | undefined;
|
|
150
|
+
if (attrs && typeof attrs.selfLink === "string") cloudIds.push({ kind: "self-link", value: attrs.selfLink });
|
|
151
|
+
const { labels: _labels, ...properties } = r;
|
|
152
|
+
found.push({ listedType: assetType, location, nativeId: name, cloudIds, tags, properties, observedAt, labels: true });
|
|
153
|
+
}
|
|
154
|
+
pageToken = parsed?.nextPageToken || undefined;
|
|
155
|
+
if (!pageToken) break;
|
|
156
|
+
}
|
|
157
|
+
for (const r of found) ctx.resources.push(buildResource(ctx, r));
|
|
158
|
+
for (const t of typesCovered) for (const l of locationsCovered) ctx.coverage.push({ listedType: t, location: l, ...listedVerdict(scope) });
|
|
159
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* behold#1029 done-when: "A test fails if any mutating API is reachable from
|
|
3
|
+
* the lister."
|
|
4
|
+
*
|
|
5
|
+
* Three layers, each of which goes red when a mutating call is added:
|
|
6
|
+
* 1. the allowlist is pinned to exactly the five read operations, and no
|
|
7
|
+
* entry may name a write;
|
|
8
|
+
* 2. a mutating call of every kind each substrate has is refused by the
|
|
9
|
+
* guard, and never reaches the fetcher;
|
|
10
|
+
* 3. every call each lister makes while listing (against fakes) classifies
|
|
11
|
+
* into the allowlist, and the lister sources name no mutating operation.
|
|
12
|
+
*
|
|
13
|
+
* Red first: add `"cloudcontrol:CreateResource"` to READ_OPERATIONS.aws, or a
|
|
14
|
+
* `method: "PUT"` call to any lister, and layer 1 or 3 fails.
|
|
15
|
+
*/
|
|
16
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { describe, expect, it } from "vitest";
|
|
20
|
+
import { assertReadCall, classifyCall, MutatingCallRefused, READ_OPERATIONS, type WireCall } from "./guard";
|
|
21
|
+
import { Transport, OriginRefused } from "./transport";
|
|
22
|
+
import { listCloud } from "./index";
|
|
23
|
+
import { fakeFetcher, json, refusingChains, steppingClock } from "./fakes";
|
|
24
|
+
|
|
25
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
|
|
27
|
+
describe("the read allowlist", () => {
|
|
28
|
+
it("is exactly the five read operations", () => {
|
|
29
|
+
expect(READ_OPERATIONS).toEqual({
|
|
30
|
+
aws: ["cloudcontrol:ListResources"],
|
|
31
|
+
azure: ["resourcegraph:resources"],
|
|
32
|
+
gcp: ["cloudasset:searchAllResources"],
|
|
33
|
+
kubernetes: ["k8s:discovery", "k8s:list"],
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("names no write", () => {
|
|
38
|
+
for (const ops of Object.values(READ_OPERATIONS)) {
|
|
39
|
+
for (const op of ops) expect(op).not.toMatch(/create|update|delete|put|patch|enable|disable|cancel|watch|tag|apply|import|export/i);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const aws = (target: string, method: WireCall["method"] = "POST"): WireCall => ({
|
|
45
|
+
substrate: "aws",
|
|
46
|
+
method,
|
|
47
|
+
url: "http://localhost:4566/",
|
|
48
|
+
headers: { "x-amz-target": `CloudApiService.${target}` },
|
|
49
|
+
body: "{}",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const MUTATING: Array<[string, WireCall]> = [
|
|
53
|
+
["aws CreateResource", aws("CreateResource")],
|
|
54
|
+
["aws UpdateResource", aws("UpdateResource")],
|
|
55
|
+
["aws DeleteResource", aws("DeleteResource")],
|
|
56
|
+
["aws CancelResourceRequest", aws("CancelResourceRequest")],
|
|
57
|
+
["aws GetResource (not on the list either)", aws("GetResource")],
|
|
58
|
+
["aws a call with no target", { substrate: "aws", method: "POST", url: "http://localhost:4566/", headers: {}, body: "Action=CreateBucket" }],
|
|
59
|
+
["aws a PUT", { substrate: "aws", method: "PUT", url: "http://localhost:4566/bucket", headers: {} }],
|
|
60
|
+
["azure PUT on an ARM resource", { substrate: "azure", method: "PUT", url: "https://localhost:4577/subscriptions/s/resourceGroups/rg?api-version=2021-04-01", headers: {} }],
|
|
61
|
+
["azure DELETE on an ARM resource", { substrate: "azure", method: "DELETE", url: "https://localhost:4577/subscriptions/s/resourceGroups/rg?api-version=2021-04-01", headers: {} }],
|
|
62
|
+
["azure POST to another provider action", { substrate: "azure", method: "POST", url: "https://localhost:4577/subscriptions/s/providers/Microsoft.Storage/register?api-version=2021-04-01", headers: {} }],
|
|
63
|
+
["azure GET on Resource Graph's path", { substrate: "azure", method: "GET", url: "https://localhost:4577/providers/Microsoft.ResourceGraph/resources", headers: {} }],
|
|
64
|
+
["gcp enabling the Cloud Asset API", { substrate: "gcp", method: "POST", url: "https://serviceusage.googleapis.com/v1/projects/p/services/cloudasset.googleapis.com:enable", headers: {} }],
|
|
65
|
+
["gcp batchEnable", { substrate: "gcp", method: "POST", url: "https://serviceusage.googleapis.com/v1/projects/p/services:batchEnable", headers: {} }],
|
|
66
|
+
["gcp a searchAllResources on the serviceusage host", { substrate: "gcp", method: "GET", url: "https://serviceusage.googleapis.com/v1/projects/p:searchAllResources", headers: {} }],
|
|
67
|
+
["gcp POST searchAllResources", { substrate: "gcp", method: "POST", url: "http://localhost:4588/cloudasset/v1/projects/p:searchAllResources", headers: {} }],
|
|
68
|
+
["gcp creating a feed", { substrate: "gcp", method: "POST", url: "https://cloudasset.googleapis.com/v1/projects/p/feeds", headers: {} }],
|
|
69
|
+
["gcp exportAssets", { substrate: "gcp", method: "GET", url: "https://cloudasset.googleapis.com/v1/projects/p:exportAssets", headers: {} }],
|
|
70
|
+
["k8s POST", { substrate: "kubernetes", method: "POST", url: "https://127.0.0.1:6443/api/v1/namespaces/default/pods", headers: {} }],
|
|
71
|
+
["k8s PUT", { substrate: "kubernetes", method: "PUT", url: "https://127.0.0.1:6443/api/v1/namespaces/default/configmaps/x", headers: {} }],
|
|
72
|
+
["k8s PATCH", { substrate: "kubernetes", method: "PATCH", url: "https://127.0.0.1:6443/apis/apps/v1/namespaces/default/deployments/x", headers: {} }],
|
|
73
|
+
["k8s DELETE", { substrate: "kubernetes", method: "DELETE", url: "https://127.0.0.1:6443/api/v1/namespaces/default/pods/x", headers: {} }],
|
|
74
|
+
["k8s a watch", { substrate: "kubernetes", method: "GET", url: "https://127.0.0.1:6443/api/v1/pods?watch=1", headers: {} }],
|
|
75
|
+
["k8s a pod's exec", { substrate: "kubernetes", method: "GET", url: "https://127.0.0.1:6443/api/v1/namespaces/default/pods/x/exec?command=sh", headers: {} }],
|
|
76
|
+
["k8s a service proxy", { substrate: "kubernetes", method: "GET", url: "https://127.0.0.1:6443/api/v1/namespaces/default/services/x/proxy/", headers: {} }],
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
describe("the guard refuses every mutating call", () => {
|
|
80
|
+
for (const [name, call] of MUTATING) {
|
|
81
|
+
it(name, async () => {
|
|
82
|
+
expect(() => assertReadCall(call)).toThrow(MutatingCallRefused);
|
|
83
|
+
const fake = fakeFetcher(() => json(200, {}));
|
|
84
|
+
const transport = new Transport({ fetcher: fake.fetcher });
|
|
85
|
+
await expect(transport.send(call)).rejects.toThrow(MutatingCallRefused);
|
|
86
|
+
expect(fake.calls).toEqual([]);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
it("admits the four reads", () => {
|
|
91
|
+
expect(classifyCall(aws("ListResources"))).toBe("cloudcontrol:ListResources");
|
|
92
|
+
expect(classifyCall({ substrate: "azure", method: "POST", url: "https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2022-10-01", headers: {} })).toBe("resourcegraph:resources");
|
|
93
|
+
expect(classifyCall({ substrate: "gcp", method: "GET", url: "http://localhost:4588/cloudasset/v1/projects/p:searchAllResources?pageSize=500", headers: {} })).toBe("cloudasset:searchAllResources");
|
|
94
|
+
expect(classifyCall({ substrate: "kubernetes", method: "GET", url: "https://h/apis/apps/v1", headers: {} })).toBe("k8s:discovery");
|
|
95
|
+
expect(classifyCall({ substrate: "kubernetes", method: "GET", url: "https://h/apis/apps/v1/deployments?limit=500", headers: {} })).toBe("k8s:list");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("the origin pin", () => {
|
|
100
|
+
it("refuses a call to any host but the run's endpoint", async () => {
|
|
101
|
+
const fake = fakeFetcher(() => json(200, {}));
|
|
102
|
+
const transport = new Transport({ fetcher: fake.fetcher, pinnedOrigin: "http://localhost:4566" });
|
|
103
|
+
await expect(transport.send({ ...aws("ListResources"), url: "https://cloudcontrolapi.us-east-1.amazonaws.com/" })).rejects.toThrow(OriginRefused);
|
|
104
|
+
expect(fake.calls).toEqual([]);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("every call a lister makes is on the allowlist", () => {
|
|
109
|
+
const seen: Array<{ substrate: string; operation: string }> = [];
|
|
110
|
+
const onSend = (call: WireCall, operation: string) => seen.push({ substrate: call.substrate, operation });
|
|
111
|
+
|
|
112
|
+
it("aws", async () => {
|
|
113
|
+
const fake = fakeFetcher(() => json(200, { TypeName: "AWS::S3::Bucket", ResourceDescriptions: [] }));
|
|
114
|
+
await listCloud(
|
|
115
|
+
{ substrate: "aws", scopeId: "000000000000", locations: ["us-east-1"], types: ["AWS::S3::Bucket", "AWS::IAM::Role"] },
|
|
116
|
+
{ substrate: "aws", aws: { endpoint: "http://localhost:4566", credentials: { accessKeyId: "test", secretAccessKey: "test" } } },
|
|
117
|
+
{ fetcher: fake.fetcher, chains: refusingChains(), now: steppingClock(), retryDelays: [], onSend },
|
|
118
|
+
);
|
|
119
|
+
expect(fake.calls.length).toBe(2);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("azure", async () => {
|
|
123
|
+
const fake = fakeFetcher(() => json(200, { data: [], totalRecords: 0, count: 0 }));
|
|
124
|
+
await listCloud(
|
|
125
|
+
{ substrate: "azure", scopeId: "00000000-0000-0000-0000-000000000000", locations: "*", types: "*" },
|
|
126
|
+
{ substrate: "azure", azure: { endpoint: "https://localhost:4577", token: "static" } },
|
|
127
|
+
{ fetcher: fake.fetcher, chains: refusingChains(), now: steppingClock(), retryDelays: [], onSend },
|
|
128
|
+
);
|
|
129
|
+
expect(fake.calls.length).toBe(1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("gcp", async () => {
|
|
133
|
+
const fake = fakeFetcher(() => json(200, {}));
|
|
134
|
+
await listCloud(
|
|
135
|
+
{ substrate: "gcp", scopeId: "plc-corpus", locations: "*", types: "*" },
|
|
136
|
+
{ substrate: "gcp", gcp: { endpoint: "http://localhost:4588/cloudasset", token: null } },
|
|
137
|
+
{ fetcher: fake.fetcher, chains: refusingChains(), now: steppingClock(), retryDelays: [], onSend },
|
|
138
|
+
);
|
|
139
|
+
expect(fake.calls.length).toBe(1);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("kubernetes", async () => {
|
|
143
|
+
const fake = fakeFetcher((call) => {
|
|
144
|
+
const path = new URL(call.url).pathname;
|
|
145
|
+
if (path === "/api/v1") return json(200, { resources: [{ name: "configmaps", kind: "ConfigMap", namespaced: true, verbs: ["get", "list", "create"] }] });
|
|
146
|
+
if (path === "/apis") return json(200, { groups: [] });
|
|
147
|
+
if (path === "/api/v1/configmaps") return json(200, { items: [] });
|
|
148
|
+
return undefined;
|
|
149
|
+
});
|
|
150
|
+
await listCloud(
|
|
151
|
+
{ substrate: "kubernetes", scopeId: "kind-floci", locations: "*", types: "*" },
|
|
152
|
+
{ substrate: "kubernetes", kubernetes: { server: "https://127.0.0.1:6443", token: "static" } },
|
|
153
|
+
{ fetcher: fake.fetcher, chains: refusingChains(), now: steppingClock(), retryDelays: [], onSend },
|
|
154
|
+
);
|
|
155
|
+
expect(fake.calls.length).toBe(3);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("and nothing else", () => {
|
|
159
|
+
for (const { substrate, operation } of seen) {
|
|
160
|
+
expect(READ_OPERATIONS[substrate as keyof typeof READ_OPERATIONS]).toContain(operation);
|
|
161
|
+
}
|
|
162
|
+
expect(new Set(seen.map((s) => s.operation))).toEqual(
|
|
163
|
+
new Set(["cloudcontrol:ListResources", "resourcegraph:resources", "cloudasset:searchAllResources", "k8s:discovery", "k8s:list"]),
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("the lister sources", () => {
|
|
169
|
+
const sources = readdirSync(here).filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && f !== "guard.ts" && f !== "fakes.ts");
|
|
170
|
+
|
|
171
|
+
it("include every substrate's lister", () => {
|
|
172
|
+
expect(sources).toEqual(expect.arrayContaining(["aws.ts", "azure.ts", "gcp.ts", "kubernetes.ts", "transport.ts"]));
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("send only GET and POST", () => {
|
|
176
|
+
for (const f of sources) {
|
|
177
|
+
const text = readFileSync(join(here, f), "utf8");
|
|
178
|
+
expect(text, f).not.toMatch(/method:\s*"(PUT|PATCH|DELETE)"/);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("name no mutating Cloud Control, Service Usage or Cloud Asset operation", () => {
|
|
183
|
+
for (const f of sources) {
|
|
184
|
+
const text = readFileSync(join(here, f), "utf8");
|
|
185
|
+
const targets = [...text.matchAll(/CloudApiService\.([A-Za-z]+)/g)].map((m) => m[1]);
|
|
186
|
+
for (const t of targets) expect(t, f).toBe("ListResources");
|
|
187
|
+
expect(text, f).not.toMatch(/:(enable|disable|batchEnable|exportAssets)\b|\/feeds\b/);
|
|
188
|
+
expect(text, f).not.toMatch(/serviceusage\.googleapis\.com/);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("import no cloud SDK client", () => {
|
|
193
|
+
for (const f of sources) {
|
|
194
|
+
const text = readFileSync(join(here, f), "utf8");
|
|
195
|
+
expect(text, f).not.toMatch(/from\s+"@aws-sdk\/client-|from\s+"@azure\/arm-|from\s+"@google-cloud\/|from\s+"@kubernetes\/client-node"|from\s+"@intentius\/chant-k8s-client"/);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
});
|