Skip to main content

Terraform drift intelligence: detect, attribute, score, and fix IaC drift

Project description

๐Ÿ” drifty

Terraform drift intelligence โ€” detect what changed, who changed it, how dangerous it is, and how to fix it.

PyPI version Python CI License: MIT

pip install drifty

The Problem

terraform plan tells you what drifted. It does not tell you who changed it, how dangerous the change is, or what to do about it.

Manual changes in the AWS console during incidents, auto-scaling events, and ad-hoc CLI commands silently diverge your infrastructure from Terraform state. By the time the drift is noticed, it is often unclear whether it came from a teammate, automation, or a security issue.

Enterprise platforms like Spacelift and HCP Terraform Cloud can detect drift on a schedule, but they are full IaC platforms that are heavier, more expensive, and still do not provide attribution or severity intelligence.

drifty fills this gap.


Demo

$ drifty scan --workspace ./infra --attribute

๐Ÿ” drifty โ€” Terraform Drift Intelligence
Scanning workspace: ./infra  |  2026-06-05 14:00 UTC

โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚  3 drifts detected  -   1 Critical  -   1 High  -   1 Low   โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

๐Ÿ”ด CRITICAL  aws_security_group.main  (sg-0abc1234)
   Changed:  ingress.0.cidr_blocks  โ†’  ["0.0.0.0/0"]  (was: ["10.0.0.0/8"])
   Who:      arn:aws:iam::123456789:user/john.doe
   When:     2026-06-03 14:22:11 UTC
   Action:   ModifySecurityGroupRules
   Fix:      terraform import aws_security_group.main sg-0abc1234

๐ŸŸ  HIGH  aws_instance.api_server  (i-0def5678)
   Changed:  instance_type  โ†’  t3.large  (was: t3.medium)
   Who:      arn:aws:iam::123456789:role/ops-automation
   When:     2026-06-02 09:15:44 UTC
   Action:   ModifyInstanceAttribute
   Fix:      terraform import aws_instance.api_server i-0def5678

๐ŸŸข LOW  aws_s3_bucket.assets  (assets-bucket-prod)
   Changed:  tags.LastModified  โ†’  "2026-06-01"  (was: "2026-05-15")
   Who:      attribution unavailable (event outside 90-day CloudTrail window)
   Fix:      Add tag to Terraform config or run terraform apply to reconcile

โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Run `drifty report --format markdown` to export this as a report.

Install

Requirements: Python 3.10+, AWS credentials configured, Terraform available in your workspace

pip install drifty

Quick Start

# 1. Initialize config in your Terraform workspace
cd ./infra
drifty init

# 2. Scan for drift
drifty scan

# 3. Scan with CloudTrail attribution
drifty scan --attribute

# 4. Filter to critical and high only
drifty scan --severity high

# 5. Output as JSON for CI/CD piping
drifty scan --output json | jq '.findings[] | select(.severity=="critical")'

# 6. Export a markdown report
drifty report --format markdown --out ./drift-report.md

# 7. Send drift summary to Slack
drifty config set slack_webhook=https://hooks.slack.com/services/xxx/yyy/zzz
drifty scan --notify slack

# 8. Watch for new drift continuously
drifty watch --interval 300 --threshold high --notify slack

# 9. Watch with CloudTrail attribution on every cycle
drifty watch --interval 300 --attribute --notify slack

# 10. Post drift report as a GitHub PR comment
drifty report-pr --attribute --severity high

# 11. View drift history across previous scans
drifty history

# 12. Show last 30 scans, high severity and above
drifty history --last 30 --severity high

# 13. Suppress a known or accepted drift
drifty ignore aws_instance.api_server --reason "approved by security team"

# 14. List all ignored resources
drifty ignore --list

# 15. Remove an ignore entry
drifty ignore aws_instance.api_server --remove

How It Works

drifty scan
    โ”‚
    โ”œโ”€ 1. runs: terraform plan -refresh-only -json
    โ”‚
    โ”œโ”€ 2. parses JSON Lines output โ†’ extracts resource_drift entries
    โ”‚
    โ”œโ”€ 3. scores each finding
    โ”‚       critical โ†’ IAM, security groups, S3 policies
    โ”‚       high     โ†’ EC2 instances, RDS, load balancers
    โ”‚       medium   โ†’ Lambda, Auto Scaling, CloudWatch
    โ”‚       low      โ†’ tag-only changes
    โ”‚
    โ”œโ”€ 4. attributes each finding via CloudTrail (if --attribute)
    โ”‚       boto3 โ†’ LookupEvents by resource ID
    โ”‚       returns: IAM principal, timestamp, API action
    โ”‚
    โ””โ”€ 5. renders output
            terminal โ†’ Rich color-coded output
            json     โ†’ structured output for CI/CD
            markdown โ†’ report for docs and collaboration

Commands

drifty scan

Options:
  --workspace PATH    Terraform directory (default: current dir)
  --profile TEXT      AWS CLI profile (default: "default")
  --attribute         Enable CloudTrail attribution
  --severity TEXT     Minimum severity: critical | high | medium | low
  --output TEXT       Output format: terminal | json | markdown
  --notify TEXT       Send results to: slack

drifty init

Initializes .drifty/config.yaml in the workspace with default settings.

drifty config set KEY=VALUE

drifty config set default_severity=high
drifty config set default_profile=prod
drifty config set slack_webhook=https://hooks.slack.com/...
drifty config set cloudtrail_lookback_days=30

drifty report

drifty report --format markdown --out ./reports/drift-$(date +%F).md
drifty report --format json

drifty scan --notify slack

Sends a Slack Block Kit message when drift is found. Configure slack_webhook in .drifty/config.yaml first.

# One-time setup
drifty config set slack_webhook=https://hooks.slack.com/services/T.../B.../xxx

# Then run with notifications
drifty scan --notify slack
drifty scan --attribute --notify slack --severity high

The Slack message includes:

  • Severity summary
  • Per-finding blocks with changed attributes, attribution, and remediation hints
  • A cap of 10 findings per message to stay within Slack block limits

drifty watch

Continuously monitors a Terraform workspace for drift and alerts when new findings appear.

Options:
  --workspace PATH    Terraform directory (default: current dir)
  --interval INT      Polling interval in seconds (default: 300)
  --threshold TEXT    Minimum severity to trigger alert: critical | high | medium | low
  --notify TEXT       Notifier to use when new drift is detected: slack
  --attribute         Enable CloudTrail attribution on each cycle
# Poll every 5 minutes, alert on high+ drift via Slack
drifty watch --interval 300 --threshold high --notify slack --attribute

# Run locally with a faster interval for testing
drifty watch --interval 60 --threshold low

drifty watch tracks state between cycles using a finding hash stored in .drifty/state.json. It only alerts on new drift so previously seen findings do not create repeated noise.

drifty report-pr

Scans for drift and posts a formatted report as a comment on a GitHub Pull Request.

Options:
  --workspace PATH    Terraform directory (default: current dir)
  --profile TEXT      AWS CLI profile
  --attribute         Enable CloudTrail attribution
  --severity TEXT     Minimum severity filter: critical | high | medium | low
  --token TEXT        GitHub token (defaults to GITHUB_TOKEN env var)
  --repo TEXT         Repository in owner/repo format (defaults to GITHUB_REPOSITORY env var)
  --pr INT            Pull request number (defaults to PR_NUMBER env var)
# In GitHub Actions
drifty report-pr --attribute --severity high

# Locally against a specific PR
drifty report-pr --repo acme/infra --pr 42 --token ghp_xxx

Each finding renders as a collapsible <details> block with changed attributes, attribution details, and a remediation hint.

Example GitHub Actions step:

- name: Drift Report
  run: drifty report-pr --attribute --severity high
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    GITHUB_REPOSITORY: ${{ github.repository }}
    PR_NUMBER: ${{ github.event.pull_request.number }}

drifty history

Shows drift trends from previous scans. Findings are automatically persisted to .drifty/history.json after every drifty scan.

Options:
  --workspace PATH    Terraform directory (default: current dir)
  --last INT          Number of recent scans to show (default: 10)
  --severity TEXT     Minimum severity filter: critical | high | medium | low
  --output TEXT       terminal | json
drifty history
drifty history --last 30 --severity high
drifty history --output json

drifty ignore

Manages the ignore list for suppressing known or accepted drift. Suppressed resources still appear in scan output under a dimmed Suppressed label rather than being silently hidden.

Options:
  RESOURCE            Resource address to ignore, for example aws_instance.api_server
  --workspace PATH    Terraform directory (default: current dir)
  --reason TEXT       Reason for ignoring this resource
  --remove            Remove a resource from the ignore list
  --list              List all currently ignored resources
drifty ignore aws_instance.api_server
drifty ignore aws_instance.api_server --reason "approved by security team"
drifty ignore aws_instance.api_server --remove
drifty ignore --list

Ignore entries are persisted to .drifty/ignore.yaml with timestamp and author.


Severity Rules

Resource Type Severity
aws_iam_role_policy, aws_iam_policy ๐Ÿ”ด Critical
aws_security_group, aws_security_group_rule ๐Ÿ”ด Critical
aws_s3_bucket_policy, aws_s3_bucket_public_access_block ๐Ÿ”ด Critical
aws_instance (type or AMI change) ๐ŸŸ  High
aws_rds_instance, aws_lb, aws_alb ๐ŸŸ  High
aws_lambda_function, aws_autoscaling_group ๐ŸŸก Medium
aws_cloudwatch_metric_alarm ๐ŸŸก Medium
aws_instance (tag-only change) ๐ŸŸข Low
aws_s3_bucket (tag-only change) ๐ŸŸข Low

Override any rule per workspace:

# .drifty/config.yaml
severity_overrides:
  aws_lambda_function: high
  aws_cloudwatch_metric_alarm: low

drifty vs. alternatives

Feature terraform plan Spacelift / HCP TF drifty
Detects drift โœ… โœ… โœ…
Who caused it โŒ โŒ โœ… CloudTrail
Severity score โŒ โŒ โœ…
Remediation hint โŒ โŒ โœ…
JSON / Markdown output โŒ Partial โœ…
Works locally / in CI โœ… โŒ SaaS only โœ…
Cost Free $$$ Free
Install N/A Platform setup pip install drifty
Slack alerts โŒ โŒ โœ…
Continuous drift watch โŒ โœ… Scheduled โœ…
GitHub PR comment โŒ โŒ โœ…
Drift history / trends โŒ โŒ โœ…
Ignore / suppress drift โŒ โŒ โœ…

Configuration Reference

# .drifty/config.yaml
default_profile: default
default_severity: null
default_output: terminal
slack_webhook: null
cloudtrail_lookback_days: 90
severity_overrides: {}

Release

drifty is packaged with Poetry and published to PyPI.

poetry version patch
poetry build
poetry publish

Use TestPyPI for a dry run before publishing to production:

poetry publish -r testpypi

Roadmap

  • Slack notifications via --notify slack
  • Continuous drift watch
  • GitHub PR comment integration
  • Drift history
  • Ignore / suppress drift
  • Azure and GCP provider support

Contributing

git clone https://github.com/satyajit-dey-17/drifty.git
cd drifty
poetry install
poetry run pytest -v
poetry run ruff check drifty/
poetry run black drifty/

Please open an issue before submitting a large PR. See .github/ISSUE_TEMPLATE/ for bug and feature templates.


License

MIT ยฉ Satyajit Dey

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

drifty-0.6.4.tar.gz (35.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

drifty-0.6.4-py3-none-any.whl (40.5 kB view details)

Uploaded Python 3

File details

Details for the file drifty-0.6.4.tar.gz.

File metadata

  • Download URL: drifty-0.6.4.tar.gz
  • Upload date:
  • Size: 35.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for drifty-0.6.4.tar.gz
Algorithm Hash digest
SHA256 ac3d47044f80c11ad81338dc40e9c6163b77b67a06404e4ec0f45b41ed2ff8c1
MD5 af07fb62a875e4b053c6a0e9482ccde8
BLAKE2b-256 ce30ff592a931e9a638afa3c778c5dc60b431727936a9e247d64a98b4611c913

See more details on using hashes here.

Provenance

The following attestation bundles were made for drifty-0.6.4.tar.gz:

Publisher: publish.yml on satyajit-dey-17/drifty

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file drifty-0.6.4-py3-none-any.whl.

File metadata

  • Download URL: drifty-0.6.4-py3-none-any.whl
  • Upload date:
  • Size: 40.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for drifty-0.6.4-py3-none-any.whl
Algorithm Hash digest
SHA256 d372fe3f2fd6d7aeea40cba3c50d098aa7e7e717977c52bfd64d23de571be1bf
MD5 fe7c1d4de26477694650b54a5f3b3834
BLAKE2b-256 af1141473ec3af1b3f85e38197353a26df638196a7b29b91142f519f10df6e86

See more details on using hashes here.

Provenance

The following attestation bundles were made for drifty-0.6.4-py3-none-any.whl:

Publisher: publish.yml on satyajit-dey-17/drifty

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page