Skip to main content
CMDashCybersecurity Maturity Dashboard
DocsDevelopersTerraform provider — cmdash

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 objectPublic API v2 routesKind
cmdash_webhook_registrationGET/POST/PUT/DELETE /v2/webhooks and POST /v2/webhooks/:id/rotate-secret — full create, read, update, deleteResource, with import
cmdash_compliance_frameworks, cmdash_framework_controlsGET /v2/compliance/frameworks, .../frameworks/:slug/controls, .../progressData sources
cmdash_integration_connectionsGET /v2/integrations, GET /v2/integrations/:id/healthData 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/logs and :id/rotate-secret take an id, so the provider reads one registration's current url, events and isActive by listing and filtering client-side.
  • POST /v2/webhooks and POST /v2/webhooks/:id/rotate-secret are 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 -replace destroys 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

code
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

code
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:

bash
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

code
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

bash
cd terraform-provider-cmdash
go build -o terraform-provider-cmdash .

Point Terraform at the local build with a ~/.terraformrc dev_overrides block:

code
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, 429 retry/backoff from X-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 by TF_ACC=1; unset, they skip, so plain go test ./... needs neither a terraform binary nor network access. Run: cd terraform-provider-cmdash && TF_ACC=1 go test ./internal/provider/... (downloads a terraform binary on first run).

This page is the product manual, published from the repository at docs/terraform-provider.md. Customers read the same text inside CMDash under Help. Last updated October 9, 2026.

Terraform provider — cmdash — CMDash Docs