Catch risky Terraform topology before it merges. Deterministic. Local. Free.
Quickstart · Live demo · Why ArchiteX? · Coverage · Roadmap
ArchiteX is a drop-in GitHub Action that reads the Terraform diff in a Pull Request and posts a deterministic architectural review back as a sticky PR comment. Every PR gets:
- A numeric risk score (0–10) with explainable reasons
- A plain-English summary of what changed and why it matters
- A focused delta diagram (Mermaid) showing only the changed nodes plus one layer of context
- An optional CI gate that can fail the build on critical violations
- A timestamped audit bundle with a self-contained HTML report and SHA-256 manifest
Everything runs on your own GitHub Actions runner. Raw Terraform never leaves the runner. No SaaS, no telemetry, no paid tier, no account.
v1.4 ships today — first-class Azure support (12
azurerm_*resources, 3 Azure-specific risk rules) on top of the existing 45-resource AWS coverage. The tool auto-detects which provider your.tffiles use; AWS-only repos see zero behavioral change. See the changelog →
Add this file to your Terraform repo at .github/workflows/architex.yml:
name: ArchiteX
on: pull_request
permissions:
contents: read
pull-requests: write
jobs:
architex:
runs-on: ubuntu-latest
steps:
- uses: danilotrix86/ArchiteX@v1.4.2
with:
terraform-dir: infraThat's it. Open a PR that touches infra/*.tf. ArchiteX will post a sticky comment with the score, the diagram, and the reasons. Nothing fails by default.
Want a CI gate that blocks merges on high-risk changes? Change one line:
with:
terraform-dir: infra
mode: blocking # advisory (default) | blockingFull GitHub Action reference →
A developer opens a PR that adds a public ALB and changes a security group from private (10.0.0.0/16) to public (0.0.0.0/0). ArchiteX posts:
Status: fail · Severity: high
This change introduces significant architectural risk. 1 node was added, 1 node was changed, and 2 edges were added. A new entry point (
aws_lb.web) was added, increasing the public attack surface. A security-related resource (aws_security_group.web) was modified, which may affect access control policies.
- Exposure (weight 4.0): A resource changed from private to public — confirm the CIDR allow-list was intended.
- Exposure (weight 3.0): A new entry point was added — confirm authentication and TLS are configured.
- Data exposure (weight 2.0): Public exposure was introduced alongside an access-control change.
Loadingflowchart LR classDef added stroke:#28a745,stroke-width:2px classDef changed stroke:#d39e00,stroke-width:2px classDef context stroke:#6c757d,stroke-width:1px aws_lb_web["+ entry_point: aws_lb.web"]:::added aws_security_group_web["~ access_control: aws_security_group.web"]:::changed aws_subnet_public["network: aws_subnet.public"]:::context aws_lb_web -->|attached_to| aws_security_group_web aws_lb_web -->|deployed_in| aws_subnet_public
- exposure
public_exposure_introduced(4.0)- exposure
new_entry_point(3.0)- data_exposure
potential_data_exposure(2.0)
Generated by ArchiteX (deterministic mode).
Every byte above is deterministic. The same diff produces byte-identical output across runs, machines, and contributors.
Static-analysis tools tell you which line has a problem. ArchiteX tells you what changed in your architecture and why a reviewer should care.
| ArchiteX | tfsec / Trivy | Checkov | Terraform Cloud | |
|---|---|---|---|---|
| Deterministic output (byte-identical re-runs) | ✅ | ❌ | ||
| Architectural delta (added/removed/changed) | ✅ | ❌ | ❌ | |
| Mermaid diagram in PR comment | ✅ | ❌ | ❌ | ❌ |
| Plain-English summary for non-experts | ✅ | ❌ | ❌ | ❌ |
Library-mode parsing (count = var.x ? 1 : 0) |
✅ | ❌ | ||
| Baseline anomaly detection | ✅ | ❌ | ❌ | ❌ |
| Self-contained HTML audit report | ✅ | ❌ | ❌ | ❌ |
| Air-gap friendly (no SaaS, no telemetry) | ✅ | ✅ | ✅ | ❌ |
| Free + open source | ✅ | ✅ | ✅ | ❌ |
| Single binary, single Action, zero config | ✅ | ✅ | ❌ |
ArchiteX complements rule-based scanners. Run them side-by-side: tfsec catches misconfigured-line issues; ArchiteX catches architectural drift (new entry points, novel resource types, conditional gates a reviewer might miss).
flowchart LR
classDef stage stroke:#0366d6,stroke-width:2px,fill:#f6f8fa
classDef output stroke:#28a745,stroke-width:2px,fill:#f6f8fa
A([base/*.tf]):::stage --> P1
B([head/*.tf]):::stage --> P1
P1["1. Fact-Finder<br/>HCL parse · graph build"]:::stage --> P2
P2["2. Delta Engine<br/>added · removed · changed"]:::stage --> P3
P3["3. Risk & Policy Engine<br/>18 rules · weighted score"]:::stage --> P4
P4["4. Interpreter<br/>Mermaid · Markdown · HTML"]:::stage --> O1([Sticky PR comment]):::output
P4 --> O2([Audit bundle<br/>summary.md · score.json<br/>egress.json · report.html<br/>manifest.json with SHA-256]):::output
Every stage is fully deterministic. The Interpreter is template-based — no inference, no model calls, no network. The same input produces byte-identical output across runs. See master.md for the full architecture specification.
- Azure support (Phase 9, tranche-0) — 12 new
azurerm_*resource types covering the canonical 3-tier Azure scope:azurerm_virtual_network,azurerm_subnet,azurerm_public_ip,azurerm_network_interface,azurerm_network_security_group,azurerm_network_security_rule,azurerm_linux_virtual_machine,azurerm_windows_virtual_machine,azurerm_lb,azurerm_storage_account,azurerm_mssql_server,azurerm_mssql_database. 57 resources total (45 AWS + 12 Azure). - 3 new Azure-specific risk rules —
nsg_allow_all_ingress(3.5),storage_account_public(4.0),mssql_database_public(3.5). Same per-resource signal pattern, samephase6CapPerRulecap, same library-modeisConditionalNodeguard as the AWS rules. 21 deterministic rules total. - Auto-detection — drop your Terraform into a repo and ArchiteX recognizes whether each resource is
aws_*orazurerm_*and applies the right rules accordingly. The PR comment now opens with a deterministic provider banner:_Detected providers: aws, azurerm — N resources analyzed._ - Cross-provider rules apply automatically. The five abstract-type rules (
new_entry_point,new_data_resource,public_exposure_introduced,potential_data_exposure,resource_removed) and the three baselinefirst_time_*rules already key off abstract type, so they fire on Azure as soon as the registry knows the new types — no rewrite needed. - AWS bit-identical promise. Every existing AWS regression test (
TestEvaluateWith_NilConfig_BehavesAsV11,TestEvaluate_HighRiskFixture_NoRegression, all sixexamples/01-public-alb…examples/06-lambda-public-urlselftest assertions) remains green. The three new Azure rules returnnilimmediately for any non-azurerm_*node (locked in byTestEvaluate_AzureRules_AWSNodes_NeverFire). - New example:
examples/07-azure-public-lb/— the canonical Azure "public LB + open NSG" anti-pattern, wired into the examples-gallery selftest. The cross-provider equivalent of example 01 (public ALB).
57 resource types across two providers (AWS + Azure), organized into a small set of abstract architectural roles:
| Family | Resources | Since |
|---|---|---|
| Network | aws_vpc, aws_subnet, aws_internet_gateway, aws_nat_gateway, aws_route53_zone, aws_route53_record |
v1.0 – v1.2 |
| Access control | aws_security_group, aws_security_group_rule, aws_network_acl, aws_network_acl_rule, aws_s3_bucket_public_access_block, aws_s3_bucket_policy, aws_sns_topic_policy, aws_sqs_queue_policy, aws_db_parameter_group, aws_db_option_group |
v1.0 – v1.3 |
| Compute | aws_instance, aws_lambda_function, aws_ecs_cluster, aws_ecs_service, aws_ecs_task_definition, aws_eks_cluster, aws_eks_node_group, aws_eks_addon, aws_eks_fargate_profile, aws_launch_template, aws_autoscaling_group, aws_autoscaling_policy |
v1.0 – v1.3 |
| Entry points | aws_lb, aws_lambda_function_url, aws_apigatewayv2_api, aws_cloudfront_distribution |
v1.0 – v1.2 |
| Data | aws_db_instance, aws_sns_topic, aws_sqs_queue, aws_secretsmanager_secret |
v1.0 – v1.2 |
| Storage | aws_s3_bucket, aws_ebs_volume |
v1.1 – v1.2 |
| Identity | aws_iam_role, aws_iam_policy, aws_iam_role_policy_attachment, aws_kms_key, aws_kms_alias, aws_eks_identity_provider_config |
v1.1 – v1.3 |
| Network (specialized) | aws_db_subnet_group |
v1.3 |
| Family | Resources | Since |
|---|---|---|
| Network | azurerm_virtual_network, azurerm_subnet, azurerm_public_ip, azurerm_network_interface |
v1.4 |
| Access control | azurerm_network_security_group, azurerm_network_security_rule |
v1.4 |
| Compute | azurerm_linux_virtual_machine, azurerm_windows_virtual_machine |
v1.4 |
| Entry points | azurerm_lb |
v1.4 |
| Storage | azurerm_storage_account |
v1.4 |
| Data | azurerm_mssql_server, azurerm_mssql_database |
v1.4 |
azurerm_resource_group is intentionally excluded — it is purely organizational, has no architectural-review value, and including it would clutter every Azure diagram with an inert root node. References to it from other resources simply do not produce edges (same warn-and-skip behavior as var.* / data.*).
Unsupported resource types emit a warning and slightly reduce the confidence score — they never cause failures.
The parser also expands local module recursion, literal count / for_each / dynamic blocks, data.aws_iam_policy.x.arn resolution, and jsonencode({...}) policy bodies — see docs/github-action.md for the full surface.
Each rule contributes a fixed weight (capped at 10.0). Reviewer-facing names are stable across versions.
| Rule | Weight | Triggers when… | Since |
|---|---|---|---|
public_exposure_introduced |
4.0 | A node's public attribute changes from false to true. |
v1.0 |
s3_bucket_public_exposure |
4.0 | An aws_s3_bucket_public_access_block is removed, OR a permissive bucket policy is added. |
v1.1 |
storage_account_public |
4.0 | An azurerm_storage_account is added with public_network_access_enabled = true or allow_nested_items_to_be_public = true. |
v1.4 |
eks_public_endpoint |
3.5 | An aws_eks_cluster is added with vpc_config.endpoint_public_access = true and no CIDR allow-list. |
v1.3 |
iam_admin_policy_attached |
3.5 | A role policy attachment binds AdministratorAccess or IAMFullAccess. |
v1.1 |
messaging_topic_public |
3.5 | An added SNS/SQS policy has Allow … Principal = "*". |
v1.2 |
nacl_allow_all_ingress |
3.5 | A NACL rule allows 0.0.0.0/0 ingress with literal allow. |
v1.2 |
nsg_allow_all_ingress |
3.5 | An azurerm_network_security_rule allows * / 0.0.0.0/0 inbound with literal Allow. |
v1.4 |
mssql_database_public |
3.5 | An azurerm_mssql_server is added with public_network_access_enabled = true. |
v1.4 |
new_entry_point |
3.0 | An added node has abstract type entry_point. |
v1.0 |
lambda_public_url_introduced |
3.0 | A lambda_function_url is added (layers on new_entry_point). |
v1.1 |
ebs_volume_unencrypted |
3.0 | An EBS volume has explicit literal encrypted = false. |
v1.2 |
new_data_resource |
2.5 | An added node has abstract type data. |
v1.0 |
cloudfront_no_waf |
2.5 | A CloudFront distribution is added with no web_acl_id. |
v1.2 |
potential_data_exposure |
2.0 | public_exposure_introduced fires alongside a data/access-control change. |
v1.0 |
eks_no_logging |
1.5 | An EKS cluster is added with no enabled_cluster_log_types. |
v1.3 |
first_time_abstract_type |
1.5 | A new abstract type appears in the repo for the first time. | v1.2 |
first_time_resource_type |
1.0 | A new Terraform type appears for the first time. | v1.2 |
asg_unrestricted_scaling |
1.0 | An ASG with max_size > 100 and no min_size floor. |
v1.3 |
resource_removed |
0.5 | A node was removed (capped at 2 reasons). | v1.0 |
first_time_edge_pair |
0.5 | A new (provider-type, provider-type) edge appears for the first time. | v1.2 |
Library-mode phantoms are never scored. A resource gated behind count = var.create ? 1 : 0 carries Attributes["conditional"] = true; every per-resource rule short-circuits on it. The diagram still shows the phantom (prefixed with ?) so reviewers can see the conditional intent without false positives.
Drop a .architex.yml at the root of your Terraform directory to tune the engine. Every field is optional; an absent file reproduces v1.2 behavior bit-for-bit. The configuration layer is provider-agnostic: the same rules:, thresholds:, ignore:, and suppressions: mechanisms apply identically to AWS rules, Azure rules, and any rule that lands in future minor releases.
parser:
mode: library # consumer (default) | library -- v1.3
rules:
# AWS rule overrides
iam_admin_policy_attached:
weight: 5.0 # bump default 3.5 -> 5.0
s3_bucket_public_exposure:
enabled: false # silence the rule entirely
# Azure rule overrides (v1.4) -- same shape, same semantics
storage_account_public:
weight: 6.0 # treat public storage accounts as fail-grade
nsg_allow_all_ingress:
enabled: false # accepted by design (e.g. all VMs are behind a managed bastion)
thresholds:
warn: 3.0 # >= warn -> medium / warn (default 3.0)
fail: 7.0 # >= fail -> high / fail (default 7.0)
ignore:
paths:
- "**/legacy/**" # parser skips matching .tf files
suppressions:
- rule: lambda_public_url_introduced
resource: aws_lambda_function_url.maintenance
reason: "Maintenance lambda; auth type is AWS_IAM"
expires: "2026-12-31" # expired entries still drop the rule
# but get an (EXPIRED) flag in the PR comment
# Azure suppressions look identical -- glob match works on Azure IDs too
- rule: mssql_database_public
resource: azurerm_mssql_server.public_reporting
reason: "Behind Azure firewall + Conditional Access; reviewed 2026-Q1"
expires: "2026-12-31"
- rule: storage_account_public
resource: azurerm_storage_account.legacy_*
reason: "Legacy hosting buckets; migration tracked in TICKET-1234"
expires: "2026-09-30"You can also place inline directives directly above a resource block:
# architex:ignore=s3_bucket_public_exposure reason="Static docs site, intentional"
resource "aws_s3_bucket_policy" "docs" {
...
}
# architex:ignore=storage_account_public reason="Public CDN origin, expires=2026-09-30"
resource "azurerm_storage_account" "public_assets" {
public_network_access_enabled = true
...
}Suppressed findings are never silent: they appear in a dedicated Suppressed Findings section in the PR comment with their reason and source.
The three Azure rule IDs configurable today (v1.4): nsg_allow_all_ingress (3.5), storage_account_public (4.0), mssql_database_public (3.5). Cross-provider rules — new_entry_point, new_data_resource, public_exposure_introduced, potential_data_exposure, resource_removed — are configured by the same rule IDs whether they fire on AWS or Azure resources.
The three first_time_* rules consume an optional .architex/baseline.json snapshot that records the kinds of resources, abstract types, and edge pairs your repo has ever contained — no raw HCL, no resource names. Generate or extend it with:
architex baseline ./infra # writes ./infra/.architex/baseline.json
architex baseline ./infra --merge # union with the existing snapshotCommit the baseline alongside your Terraform. From then on, novel additions surface as low-weight signals that layer on top of the existing rules — a brand-new public CloudFront in a repo that has never had an entry point scores new_entry_point (3.0) + first_time_abstract_type (1.5) + first_time_resource_type (1.0) = 5.5 instead of 3.0.
# Build once
go build -o architex ./cmd/architex
# Build the graph for a Terraform directory
./architex graph ./infra/
# Semantic delta between two snapshots
./architex diff ./base ./head
# Risk evaluation
./architex score ./base ./head
# Full PR-ready Markdown report (+ audit bundle with HTML report)
./architex report ./base ./head --out ./.architex/
# Sanitized egress payload (the only bytes that would leave a runner)
./architex sanitize ./base ./head --salt my-salt
# Post the report as a sticky PR comment (the ONLY network call)
GITHUB_TOKEN=ghp_xxx ./architex comment ./.architex/<bundle>/ \
--repo my-org/my-repo --pr 42 --mode advisory- Analysis runs locally on the runner. Parsing, graph construction, delta, risk evaluation, and Markdown rendering all happen in the
architexbinary. No.tfsource is transmitted anywhere. - The only network call is the PR comment.
architex commentPOSTs the rendered Markdown to the GitHub REST API usingGITHUB_TOKEN. That's the same token your other workflow steps already have. - The audit bundle is always uploaded. Every run produces a timestamped bundle (
diagram.mmd,summary.md,score.json,egress.json,report.html,manifest.jsonwith SHA-256 checksums) uploaded as a workflow artifact.report.htmlis a single self-contained page with no JavaScript, no CDN scripts, and no remote fonts — open it in an air-gapped browser and see the full report.
See master.md §6 for the full trust model, sanitization controls, and egress specification.
- v1.0 — Canonical 3-tier AWS scope (VPC, subnets, SGs, EC2, ALB, RDS).
- v1.1 — AWS Top 10 expansion (S3, IAM, Lambda, API Gateway v2). 17 resources, 8 rules.
- v1.2 — Depth & configurability: parser depth (modules, count, for_each, dynamic),
.architex.yml, baseline anomaly detection, self-contained HTML report. 34 resources, 15 rules. - v1.3 — EKS + Auto Scaling + RDS groups, library-mode parsing, conditional-phantom rendering, hardened selftest. 45 AWS resources, 18 rules.
- v1.4 — (this release) Multi-provider, auto-detected. First-class Azure tranche-0 (12
azurerm_*resources, 3 Azure-specific risk rules) layered on top of the AWS scope without touching it. 57 resources, 21 rules. - Future — Azure tranche-1 (Application Gateway, AKS, Cosmos DB, Key Vault), GCP, GitLab + Bitbucket adapters, non-Terraform IaC. See master.md §8.
See CONTRIBUTING.md for setup, what kinds of PRs are welcome, and the design principles that guide the project.
The single best ways to help:
- Open an Issue describing your use case, a Terraform pattern that didn't parse correctly, a rule you wished existed, or a false positive that hurt trust.
- Open a Discussion for broader design questions and proposals.
- Open a Pull Request with tests if you have a fix or an additive feature.
MIT — use, modify, and distribute freely, including in commercial settings.
Built for reviewers who'd rather see one diagram than scroll through a 600-line plan.
⭐ Star the repo if ArchiteX saved you a code review.