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.