Documentation

From zero to your first drift report in five minutes

Fortify Core runs refresh-only Terraform plans against your live cloud, classifies every difference, and opens the pull request that puts reality back in line with your code.

Quickstart

Install the Fortify Core GitHub app on the repositories holding your Terraform configuration, then register an environment. Scans begin on the next interval boundary.

npm i -g @fortifycore/cli
fortify login

fortify env add production \
  --repo acme/infra-prod \
  --workdir envs/prod \
  --backend s3 \
  --interval 15m

fortify scan production --wait

The first scan establishes a baseline. Subsequent scans report only what changed since the previous run, so the initial noise burst happens once.

Cloud credentials

Fortify Core assumes a read-only role per account. Nothing is ever applied to your cloud from our infrastructure. The AWS trust policy looks like this:

resource "aws_iam_role" "fortify_reader" {
  name = "fortify-core-reader"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { AWS = "arn:aws:iam::905512338841:root" }
      Action    = "sts:AssumeRole"
      Condition = {
        StringEquals = { "sts:ExternalId" = var.fortify_external_id }
      }
    }]
  })

  managed_policy_arns = [
    "arn:aws:iam::aws:policy/ReadOnlyAccess"
  ]
}

Google Cloud uses service account impersonation with the Viewer role; Azure uses a managed identity with the Reader role at the management group or subscription scope.

Configuration file

Environments can be declared in code and committed alongside your Terraform. Changes to this file are picked up on the next scan.

# .fortify.yml
version: 1

defaults:
  interval: 15m
  notify:
    slack: "#platform-alerts"

environments:
  - name: production
    workdir: envs/prod
    severity_floor: MEDIUM
    notify:
      slack: "#prod-drift"
      pagerduty: critical-only
    self_heal:
      enabled: true
      classes: [tags, descriptions, log_retention]
      delay: 15m
      max_resources_per_run: 5

  - name: staging
    workdir: envs/staging
    interval: 1h
    severity_floor: LOW

ignore:
  - resource: aws_autoscaling_group.web
    attributes: [desired_capacity]
  - attribute_pattern: "tags.LastScanned"

Severity model

Every difference is scored by resource type, attribute sensitivity and downstream blast radius.

CRITICAL — network exposure, IAM permission expansion, encryption disabled, deletion protection removed, public access unblocked.

HIGH — instance classes, capacity, database parameter groups, load balancer targets.

MEDIUM — log retention, backup windows, non-security parameters.

LOW — tags, descriptions and cosmetic metadata.

Self-healing

Self-healing is opt-in per environment and per class of change. Four guardrails apply and cannot be disabled entirely: a minimum delay before action, suppression during declared incidents and change freezes, a ceiling on resources touched per run, and a reversibility check that requires a recorded previous value.

fortify heal enable production \
  --classes tags,descriptions \
  --delay 15m \
  --max-resources 5

fortify heal status production

Alert routing

Route by environment and severity. A common pattern is CRITICAL to PagerDuty, HIGH to the owning team's Slack channel derived from CODEOWNERS, MEDIUM into a daily digest, and LOW silently self-healed.

fortify notify set production \
  --slack "#prod-drift" \
  --pagerduty-service PXXXXXX \
  --min-severity HIGH \
  --digest MEDIUM,LOW@daily

CLI reference

fortify login                      Authenticate this machine
fortify env list                   List connected environments
fortify env add <name> [flags]     Connect a new environment
fortify env rm <name>              Disconnect an environment
fortify scan <env> [--wait]        Trigger an out-of-band scan
fortify drift list [--severity]    List open drift findings
fortify drift show <id>            Show the full diff and audit trail
fortify pr open <id>               Generate a remediation pull request
fortify drift ignore <id> --reason Suppress a finding with justification
fortify heal enable|disable <env>  Manage self-healing
fortify export --since 30d --csv   Export drift history for evidence

REST API

Bearer-token authenticated, JSON in and out, rate limited to 600 requests per minute.

curl https://api.fortifycore.io/v1/drift?severity=CRITICAL \
  -H "Authorization: Bearer $FORTIFY_TOKEN"

{
  "data": [
    {
      "id": "drf_8f21c",
      "environment": "production",
      "resource": "aws_security_group.api",
      "attribute": "ingress",
      "severity": "CRITICAL",
      "detected_at": "2026-08-17T02:41:19Z",
      "actor": "arn:aws:iam::9055:user/oncall",
      "status": "pr_open",
      "pull_request": "https://github.com/acme/infra-prod/pull/812"
    }
  ],
  "next": null
}

Webhook payloads carry the same shape plus an event field and an HMAC-SHA256 signature in the X-Fortify-Signature header.

Troubleshooting

Scan skipped: state locked. Your pipeline held the lock. The scan retries on the next interval; no action needed.

Repeated LOW findings on the same attribute. A cost or tagging tool is writing on a schedule. Add it to the ignore list or enable self-healing for the tags class.

Pull request conflicts. Open remediation pull requests rebase automatically on each scan and close themselves if the drift is resolved elsewhere.

Access denied on assume role. Check the external ID matches the value shown on the environment page in the dashboard.

Ready to try it? Start a 14-day trial or browse the FAQ.