Terraform provider — cmdash
terraform-provider-cmdash manages CMDash configuration as code through the
public API v2 (see public-api-versioning.md).
It authenticates with a CMDash API key and only uses routes the public API
defines.
What the provider manages
| Terraform object | Public API v2 routes | Kind |
|---|---|---|
cmdash_webhook_registration | GET/POST/PUT/DELETE /v2/webhooks and POST /v2/webhooks/:id/rotate-secret — full create, read, update, delete | Resource, with import |
cmdash_compliance_frameworks, cmdash_framework_controls | GET /v2/compliance/frameworks, .../frameworks/:slug/controls, .../progress | Data sources |
cmdash_integration_connections | GET /v2/integrations, GET /v2/integrations/:id/health | Data source |
Outbound webhook registrations are therefore the resource you manage as code;
frameworks and integration connections are read as data. Integration connections
are created and removed in the dashboard, and POST /v2/integrations/:id/sync
triggers a sync.
Notes on the API behavior the provider relies on:
- There is no single-item
GET /v2/webhooks/:id. Only:id/logsand:id/rotate-secrettake an id, so the provider reads one registration's currenturl,eventsandisActiveby listing and filtering client-side. POST /v2/webhooksandPOST /v2/webhooks/:id/rotate-secretare the only two responses that ever carry the plaintext HMAC secret. If it is lost from Terraform state (a corrupted or hand-edited state file,terraform state rm), no API call returns it again;terraform apply -replacedestroys and recreates the registration, which also invalidates the old secret and requires updating the receiving webhook consumer.- The framework and integration lists return every row for the tenant, with no
limit/offset.
Provider configuration
terraform {
required_providers {
cmdash = {
source = "cmdash/cmdash"
}
}
}
provider "cmdash" {
# Both can come from the environment instead — CMDASH_API_KEY,
# CMDASH_BASE_URL. base_url defaults to https://app.cmdash.net/api, the
# product API this provider targets (api.cmdash.com is the unrelated
# control plane and has no v1/v2 surface).
api_key = var.cmdash_api_key
base_url = "https://app.cmdash.net/api"
}
The API key must be cmdash_ek_..., minted from the tenant dashboard
(Settings > API Keys — requires the Enterprise PUBLIC_API entitlement and the
Owner or Admin role), sent as Authorization: Bearer cmdash_ek_.... Grant it at
least the webhooks:read/webhooks:write scopes to use
cmdash_webhook_registration; the read-only data sources need
compliance:read/integrations:read respectively.
Rate limits are per key and depend on your plan (500 to 20,000 requests per
hour — see public-api-versioning.md,
"Rate limits"). The provider retries a 429 with backoff derived from
X-RateLimit-Reset/Retry-After; it does not increase your effective limit, it
just avoids a hard failure on a burst terraform apply across many resources.
Writes carry an Idempotency-Key, so a retried request never double-applies
(see idempotency.md).
Example: webhook registration
resource "cmdash_webhook_registration" "posture_alerts" {
url = "https://example.com/hooks/cmdash"
events = ["posture.changed", "risk.created"]
}
output "posture_alerts_secret" {
value = cmdash_webhook_registration.posture_alerts.secret
sensitive = true
}
Import an existing registration by UUID:
terraform import cmdash_webhook_registration.posture_alerts <webhook-uuid>
An imported resource's secret attribute stays unset, because the API never
returns the secret again after creation.
Example: reading frameworks and integrations
data "cmdash_compliance_frameworks" "all" {}
data "cmdash_framework_controls" "cmmc" {
slug = "cmmc-l2"
}
data "cmdash_integration_connections" "all" {}
output "unhealthy_integrations" {
value = [for c in data.cmdash_integration_connections.all.connections : c.provider_name if c.status == "UNHEALTHY"]
}
Installing from source
cd terraform-provider-cmdash
go build -o terraform-provider-cmdash .
Point Terraform at the local build with a ~/.terraformrc dev_overrides
block:
provider_installation {
dev_overrides {
"cmdash/cmdash" = "/path/to/terraform-provider-cmdash"
}
direct {}
}
With that in place, terraform plan and terraform apply in any configuration
referencing the cmdash provider use the local binary directly — no
terraform init, no registry, no version pin. Terraform prints a warning that
dev_overrides is active, which is expected.
Testing
- Unit tests cover the auth header,
429retry/backoff fromX-RateLimit-Reset, idempotency-key attachment on writes only, 404 as terminal (not retried), and the idempotency key format. Run:cd terraform-provider-cmdash && go test ./internal/client/... - Acceptance tests drive the real provider against an in-process HTTP fake of
/v2/webhooks(no live API, no network beyond localhost). They cover plan-empty after apply, import, and in-place update. Gated byTF_ACC=1; unset, they skip, so plaingo test ./...needs neither aterraformbinary nor network access. Run:cd terraform-provider-cmdash && TF_ACC=1 go test ./internal/provider/...(downloads aterraformbinary on first run).