tfscout 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tfscout-0.1.0/.github/workflows/ci.yml +48 -0
- tfscout-0.1.0/.github/workflows/publish.yml +47 -0
- tfscout-0.1.0/.gitignore +11 -0
- tfscout-0.1.0/LICENSE +21 -0
- tfscout-0.1.0/PKG-INFO +194 -0
- tfscout-0.1.0/README.md +175 -0
- tfscout-0.1.0/action.yml +115 -0
- tfscout-0.1.0/docs/setup.md +183 -0
- tfscout-0.1.0/examples/aws-oidc/main.tf +102 -0
- tfscout-0.1.0/examples/drift.yml +44 -0
- tfscout-0.1.0/examples/review.yml +43 -0
- tfscout-0.1.0/pyproject.toml +46 -0
- tfscout-0.1.0/src/tfscout/__init__.py +3 -0
- tfscout-0.1.0/src/tfscout/__main__.py +3 -0
- tfscout-0.1.0/src/tfscout/analyze.py +239 -0
- tfscout-0.1.0/src/tfscout/cli.py +182 -0
- tfscout-0.1.0/src/tfscout/diff.py +113 -0
- tfscout-0.1.0/src/tfscout/github.py +130 -0
- tfscout-0.1.0/src/tfscout/llm.py +207 -0
- tfscout-0.1.0/src/tfscout/plan.py +142 -0
- tfscout-0.1.0/src/tfscout/redact.py +68 -0
- tfscout-0.1.0/src/tfscout/render.py +149 -0
- tfscout-0.1.0/src/tfscout/rules.py +444 -0
- tfscout-0.1.0/tests/conftest.py +38 -0
- tfscout-0.1.0/tests/fixtures/plan_drift.json +51 -0
- tfscout-0.1.0/tests/fixtures/plan_review.json +134 -0
- tfscout-0.1.0/tests/test_analyze.py +133 -0
- tfscout-0.1.0/tests/test_cli_github.py +102 -0
- tfscout-0.1.0/tests/test_llm.py +154 -0
- tfscout-0.1.0/tests/test_plan_redact.py +77 -0
- tfscout-0.1.0/tests/test_rules.py +134 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
python: ["3.10", "3.11", "3.12", "3.13"]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v7
|
|
19
|
+
- uses: actions/setup-python@v7
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python }}
|
|
22
|
+
- run: pip install -e '.[dev]'
|
|
23
|
+
- run: ruff check . && ruff format --check .
|
|
24
|
+
- run: pytest -q
|
|
25
|
+
|
|
26
|
+
action:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v7
|
|
30
|
+
- id: review
|
|
31
|
+
uses: ./
|
|
32
|
+
with:
|
|
33
|
+
mode: review
|
|
34
|
+
plan: tests/fixtures/plan_review.json
|
|
35
|
+
provider: none
|
|
36
|
+
comment: "false"
|
|
37
|
+
- id: drift
|
|
38
|
+
uses: ./
|
|
39
|
+
with:
|
|
40
|
+
mode: drift
|
|
41
|
+
plan: tests/fixtures/plan_drift.json
|
|
42
|
+
provider: none
|
|
43
|
+
comment: "false"
|
|
44
|
+
- name: Check outputs
|
|
45
|
+
run: |
|
|
46
|
+
test "${{ steps.review.outputs.max-severity }}" = critical
|
|
47
|
+
test "${{ steps.drift.outputs.drift-detected }}" = true
|
|
48
|
+
test -s "${{ steps.review.outputs.report-file }}"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Runs when a GitHub release is published. Uses PyPI trusted publishing (OIDC), so no API token is stored.
|
|
4
|
+
# One-time setup on https://pypi.org/manage/account/publishing/ ("Add a new pending publisher"):
|
|
5
|
+
# PyPI project name: tfscout · Owner: mikonoid · Repository: tfscout
|
|
6
|
+
# Workflow: publish.yml · Environment: pypi
|
|
7
|
+
on:
|
|
8
|
+
release:
|
|
9
|
+
types: [published]
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
build:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v7
|
|
19
|
+
- uses: actions/setup-python@v7
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.12"
|
|
22
|
+
- name: Check that the tag matches the package version
|
|
23
|
+
run: |
|
|
24
|
+
version=$(python -c "import re; print(re.search(r'__version__ = \"(.+)\"', open('src/tfscout/__init__.py').read())[1])")
|
|
25
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
26
|
+
if [ "$version" != "$tag" ]; then
|
|
27
|
+
echo "::error::release tag v$tag does not match __version__ $version"
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
- run: pip install build && python -m build
|
|
31
|
+
- uses: actions/upload-artifact@v7
|
|
32
|
+
with:
|
|
33
|
+
name: dist
|
|
34
|
+
path: dist/
|
|
35
|
+
|
|
36
|
+
publish:
|
|
37
|
+
needs: build
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
environment: pypi
|
|
40
|
+
permissions:
|
|
41
|
+
id-token: write
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/download-artifact@v8
|
|
44
|
+
with:
|
|
45
|
+
name: dist
|
|
46
|
+
path: dist/
|
|
47
|
+
- uses: pypa/gh-action-pypi-publish@v1.14.3
|
tfscout-0.1.0/.gitignore
ADDED
tfscout-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mikonoid
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
tfscout-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tfscout
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI-assisted Terraform plan review and drift detection
|
|
5
|
+
Project-URL: Homepage, https://github.com/mikonoid/tfscout
|
|
6
|
+
Project-URL: Issues, https://github.com/mikonoid/tfscout/issues
|
|
7
|
+
Author: mikonoid
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: devsecops,drift,github-action,iac,llm,opentofu,terraform
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
13
|
+
Classifier: Topic :: System :: Systems Administration
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
17
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# tfscout
|
|
21
|
+
|
|
22
|
+
**AI-assisted plan review and drift detection for Terraform / OpenTofu.**
|
|
23
|
+
Runs entirely in your CI: no SaaS, no server, and your plans never leave the runner unredacted.
|
|
24
|
+
**tfscout never touches your cloud.** It only reads the plan file your pipeline already produces.
|
|
25
|
+
|
|
26
|
+
🧪 **[Live demo](https://github.com/mikonoid/tfscout-demo)**: see real PR comments and drift issues, no AWS account needed.
|
|
27
|
+
|
|
28
|
+
📘 **[Setup guide](https://github.com/mikonoid/tfscout/blob/main/docs/setup.md)**: what you need, AWS OIDC in 10 minutes, Jenkins/GitLab, troubleshooting.
|
|
29
|
+
|
|
30
|
+
- 🔎 **`review`** reads `terraform plan` in a pull request and posts a single, auto-updated comment with what
|
|
31
|
+
changes, and what can break: data loss, open security groups, IAM wildcards, disabled encryption, forced
|
|
32
|
+
replacements, plus anything the AI notices.
|
|
33
|
+
- 🧭 **`drift`** reads `terraform plan -refresh-only` on a schedule, classifies every drifted resource as
|
|
34
|
+
`noise`, `manual-change` or `risky`, explains it and suggests how to codify or revert it. Results go to one
|
|
35
|
+
GitHub issue per root module, which is updated when drift changes and closed when it is resolved.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
plan ──► redact secrets ──► deterministic rules ──► LLM (optional) ──► PR comment / issue / JSON
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Deterministic rules are the baseline you can trust. The LLM adds a plain-English summary and extra findings,
|
|
42
|
+
and can escalate but **never downgrade** a risky item. With `provider: none` tfscout works without any AI.
|
|
43
|
+
|
|
44
|
+
## Example PR comment
|
|
45
|
+
|
|
46
|
+
> ### 🔎 tfscout plan review — `prod`
|
|
47
|
+
> ➕ create **2** · ✏️ update **2** · ♻️ replace **1**
|
|
48
|
+
>
|
|
49
|
+
> - Recreates `orders-db` in another AZ
|
|
50
|
+
> - Grants CI full admin
|
|
51
|
+
>
|
|
52
|
+
> | | Severity | Resource | Finding |
|
|
53
|
+
> |---|---|---|---|
|
|
54
|
+
> | 🔴 | critical | `aws_db_instance.main` | Stateful resource will be REPLACED |
|
|
55
|
+
> | 🔴 | critical | `aws_iam_policy.ci` | IAM policy allows Action "*" |
|
|
56
|
+
> | 🔴 | critical | `aws_security_group.web` | Ingress opened to the internet (22/tcp) |
|
|
57
|
+
> | 🟠 | high | `aws_db_instance.main` | Encryption disabled (`storage_encrypted`) |
|
|
58
|
+
> | 🟠 | high | `aws_db_instance.main` | AZ move forces DB recreation 🤖 |
|
|
59
|
+
|
|
60
|
+
## Quick start (GitHub Action)
|
|
61
|
+
|
|
62
|
+
Already running `terraform plan` in GitHub Actions? Add one step. No new cloud credentials are needed.
|
|
63
|
+
Otherwise see the [setup guide](https://github.com/mikonoid/tfscout/blob/main/docs/setup.md).
|
|
64
|
+
|
|
65
|
+
### Pull request review
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
permissions:
|
|
69
|
+
contents: read
|
|
70
|
+
pull-requests: write
|
|
71
|
+
id-token: write # for cloud OIDC
|
|
72
|
+
|
|
73
|
+
steps:
|
|
74
|
+
- uses: actions/checkout@v7
|
|
75
|
+
- uses: hashicorp/setup-terraform@v4
|
|
76
|
+
with: { terraform_wrapper: false }
|
|
77
|
+
# ... configure cloud credentials ...
|
|
78
|
+
- run: terraform init -input=false && terraform plan -input=false -lock=false -out=tfplan
|
|
79
|
+
working-directory: envs/prod
|
|
80
|
+
- uses: mikonoid/tfscout@v0
|
|
81
|
+
with:
|
|
82
|
+
mode: review
|
|
83
|
+
working-directory: envs/prod
|
|
84
|
+
plan: tfplan
|
|
85
|
+
provider: gemini
|
|
86
|
+
api-key: ${{ secrets.GEMINI_API_KEY }}
|
|
87
|
+
fail-on: critical # optional merge gate
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Scheduled drift detection
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
on:
|
|
94
|
+
schedule: [{ cron: "0 6 * * 1-5" }]
|
|
95
|
+
permissions:
|
|
96
|
+
contents: read
|
|
97
|
+
issues: write
|
|
98
|
+
id-token: write
|
|
99
|
+
|
|
100
|
+
steps:
|
|
101
|
+
# ... checkout, setup-terraform, credentials, init ...
|
|
102
|
+
- run: terraform plan -refresh-only -input=false -lock=false -out=drift.tfplan
|
|
103
|
+
working-directory: envs/prod
|
|
104
|
+
- uses: mikonoid/tfscout@v0
|
|
105
|
+
with:
|
|
106
|
+
mode: drift
|
|
107
|
+
working-directory: envs/prod
|
|
108
|
+
plan: drift.tfplan
|
|
109
|
+
provider: gemini
|
|
110
|
+
api-key: ${{ secrets.GEMINI_API_KEY }}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Full workflows with a matrix over several directories: [examples/review.yml](https://github.com/mikonoid/tfscout/blob/main/examples/review.yml),
|
|
114
|
+
[examples/drift.yml](https://github.com/mikonoid/tfscout/blob/main/examples/drift.yml).
|
|
115
|
+
One-time AWS OIDC role for CI without stored keys: [examples/aws-oidc](https://github.com/mikonoid/tfscout/blob/main/examples/aws-oidc/main.tf).
|
|
116
|
+
|
|
117
|
+
### Inputs
|
|
118
|
+
|
|
119
|
+
| Input | Default | Description |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `mode` | — | `review` or `drift` |
|
|
122
|
+
| `plan` | — | Binary plan (`-out`) or `terraform show -json` output, relative to `working-directory` |
|
|
123
|
+
| `working-directory` | `.` | Root module directory |
|
|
124
|
+
| `name` | working-directory | Label used in titles; keeps comments/issues per module separate |
|
|
125
|
+
| `provider` | `none` | `none`, `gemini`, `groq`, `openrouter`, `openai`, `ollama`, `custom` |
|
|
126
|
+
| `model` / `base-url` | preset | Override model (comma-separated = fallback chain on 503/429/timeout/404) / endpoint |
|
|
127
|
+
| `timeout` | `90` | Seconds to wait for the LLM per attempt |
|
|
128
|
+
| `reasoning-effort` | preset | `minimal`/`low`/`medium`/`high` for thinking models (gemini: `low`) |
|
|
129
|
+
| `api-key` | — | LLM API key (use a secret) |
|
|
130
|
+
| `comment` | `true` | Post PR comment (review) or sync the drift issue (drift) |
|
|
131
|
+
| `fail-on` | `none` | review: fail when a finding is at least this severity |
|
|
132
|
+
| `fail-on-drift` | `false` | drift: fail when actionable drift exists |
|
|
133
|
+
| `terraform-bin` | `terraform` | Use `tofu` for OpenTofu |
|
|
134
|
+
|
|
135
|
+
Outputs: `max-severity`, `findings-count`, `drift-detected`, `fingerprint`, `report-file`.
|
|
136
|
+
The report is also added to the job summary.
|
|
137
|
+
|
|
138
|
+
## LLM providers
|
|
139
|
+
|
|
140
|
+
tfscout speaks the OpenAI-compatible `/chat/completions` API, so you can use any of these:
|
|
141
|
+
|
|
142
|
+
| Provider | Cost | Key |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| `gemini` (fallback chain `gemini-flash-lite-latest` → `gemini-3.5-flash` → `gemini-flash-latest`) | free tier | [AI Studio](https://aistudio.google.com/apikey) → `GEMINI_API_KEY` |
|
|
145
|
+
| `groq` | free tier | `GROQ_API_KEY` |
|
|
146
|
+
| `openrouter` | free models available | `OPENROUTER_API_KEY` |
|
|
147
|
+
| `openai` | paid | `OPENAI_API_KEY` |
|
|
148
|
+
| `ollama` | free, local/self-hosted runner | none |
|
|
149
|
+
| `custom` | — | any OpenAI-compatible endpoint (Azure AI Foundry, vLLM, LM Studio…) |
|
|
150
|
+
|
|
151
|
+
## Security & privacy
|
|
152
|
+
|
|
153
|
+
- `terraform show -json` prints sensitive values **in clear text**. Before anything is sent to an LLM, tfscout
|
|
154
|
+
applies Terraform's own `*_sensitive` masks and also redacts values by key name (`password`, `token`,
|
|
155
|
+
`private_key`…) and by pattern (AWS keys, PEM keys, GitHub/Slack/OpenAI tokens, JWTs, DB URLs with
|
|
156
|
+
credentials).
|
|
157
|
+
- Only redacted attribute diffs are sent, never the full state.
|
|
158
|
+
- `provider: none` sends nothing anywhere.
|
|
159
|
+
- Use read-only cloud credentials for planning. tfscout never runs `apply`.
|
|
160
|
+
|
|
161
|
+
## CLI
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pip install git+https://github.com/mikonoid/tfscout # PyPI package coming soon
|
|
165
|
+
|
|
166
|
+
terraform plan -out=tfplan
|
|
167
|
+
tfscout review tfplan # rules only
|
|
168
|
+
GEMINI_API_KEY=... tfscout review tfplan --provider gemini
|
|
169
|
+
tfscout review plan.json --format json --fail-on high # exit 3 on high/critical
|
|
170
|
+
|
|
171
|
+
terraform plan -refresh-only -out=drift.tfplan
|
|
172
|
+
tfscout drift drift.tfplan --provider ollama --exit-code # exit 2 on actionable drift
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Works with GitLab CI, Jenkins and locally too. Requires Python ≥ 3.10 and has no third-party dependencies.
|
|
176
|
+
|
|
177
|
+
## Roadmap
|
|
178
|
+
|
|
179
|
+
- [ ] CloudTrail attribution for drift ("who changed it and when")
|
|
180
|
+
- [ ] Cost delta via Infracost
|
|
181
|
+
- [ ] Auto-generated PR that codifies a manual change
|
|
182
|
+
- [ ] Custom org rules (`.tfscout.yml`: required tags, allowed regions, naming)
|
|
183
|
+
- [ ] GCP/Azure rule coverage
|
|
184
|
+
|
|
185
|
+
## Development
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
|
|
189
|
+
.venv/bin/pytest -q && .venv/bin/ruff check . && .venv/bin/ruff format --check .
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT
|
tfscout-0.1.0/README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# tfscout
|
|
2
|
+
|
|
3
|
+
**AI-assisted plan review and drift detection for Terraform / OpenTofu.**
|
|
4
|
+
Runs entirely in your CI: no SaaS, no server, and your plans never leave the runner unredacted.
|
|
5
|
+
**tfscout never touches your cloud.** It only reads the plan file your pipeline already produces.
|
|
6
|
+
|
|
7
|
+
🧪 **[Live demo](https://github.com/mikonoid/tfscout-demo)**: see real PR comments and drift issues, no AWS account needed.
|
|
8
|
+
|
|
9
|
+
📘 **[Setup guide](https://github.com/mikonoid/tfscout/blob/main/docs/setup.md)**: what you need, AWS OIDC in 10 minutes, Jenkins/GitLab, troubleshooting.
|
|
10
|
+
|
|
11
|
+
- 🔎 **`review`** reads `terraform plan` in a pull request and posts a single, auto-updated comment with what
|
|
12
|
+
changes, and what can break: data loss, open security groups, IAM wildcards, disabled encryption, forced
|
|
13
|
+
replacements, plus anything the AI notices.
|
|
14
|
+
- 🧭 **`drift`** reads `terraform plan -refresh-only` on a schedule, classifies every drifted resource as
|
|
15
|
+
`noise`, `manual-change` or `risky`, explains it and suggests how to codify or revert it. Results go to one
|
|
16
|
+
GitHub issue per root module, which is updated when drift changes and closed when it is resolved.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
plan ──► redact secrets ──► deterministic rules ──► LLM (optional) ──► PR comment / issue / JSON
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Deterministic rules are the baseline you can trust. The LLM adds a plain-English summary and extra findings,
|
|
23
|
+
and can escalate but **never downgrade** a risky item. With `provider: none` tfscout works without any AI.
|
|
24
|
+
|
|
25
|
+
## Example PR comment
|
|
26
|
+
|
|
27
|
+
> ### 🔎 tfscout plan review — `prod`
|
|
28
|
+
> ➕ create **2** · ✏️ update **2** · ♻️ replace **1**
|
|
29
|
+
>
|
|
30
|
+
> - Recreates `orders-db` in another AZ
|
|
31
|
+
> - Grants CI full admin
|
|
32
|
+
>
|
|
33
|
+
> | | Severity | Resource | Finding |
|
|
34
|
+
> |---|---|---|---|
|
|
35
|
+
> | 🔴 | critical | `aws_db_instance.main` | Stateful resource will be REPLACED |
|
|
36
|
+
> | 🔴 | critical | `aws_iam_policy.ci` | IAM policy allows Action "*" |
|
|
37
|
+
> | 🔴 | critical | `aws_security_group.web` | Ingress opened to the internet (22/tcp) |
|
|
38
|
+
> | 🟠 | high | `aws_db_instance.main` | Encryption disabled (`storage_encrypted`) |
|
|
39
|
+
> | 🟠 | high | `aws_db_instance.main` | AZ move forces DB recreation 🤖 |
|
|
40
|
+
|
|
41
|
+
## Quick start (GitHub Action)
|
|
42
|
+
|
|
43
|
+
Already running `terraform plan` in GitHub Actions? Add one step. No new cloud credentials are needed.
|
|
44
|
+
Otherwise see the [setup guide](https://github.com/mikonoid/tfscout/blob/main/docs/setup.md).
|
|
45
|
+
|
|
46
|
+
### Pull request review
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
permissions:
|
|
50
|
+
contents: read
|
|
51
|
+
pull-requests: write
|
|
52
|
+
id-token: write # for cloud OIDC
|
|
53
|
+
|
|
54
|
+
steps:
|
|
55
|
+
- uses: actions/checkout@v7
|
|
56
|
+
- uses: hashicorp/setup-terraform@v4
|
|
57
|
+
with: { terraform_wrapper: false }
|
|
58
|
+
# ... configure cloud credentials ...
|
|
59
|
+
- run: terraform init -input=false && terraform plan -input=false -lock=false -out=tfplan
|
|
60
|
+
working-directory: envs/prod
|
|
61
|
+
- uses: mikonoid/tfscout@v0
|
|
62
|
+
with:
|
|
63
|
+
mode: review
|
|
64
|
+
working-directory: envs/prod
|
|
65
|
+
plan: tfplan
|
|
66
|
+
provider: gemini
|
|
67
|
+
api-key: ${{ secrets.GEMINI_API_KEY }}
|
|
68
|
+
fail-on: critical # optional merge gate
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Scheduled drift detection
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
on:
|
|
75
|
+
schedule: [{ cron: "0 6 * * 1-5" }]
|
|
76
|
+
permissions:
|
|
77
|
+
contents: read
|
|
78
|
+
issues: write
|
|
79
|
+
id-token: write
|
|
80
|
+
|
|
81
|
+
steps:
|
|
82
|
+
# ... checkout, setup-terraform, credentials, init ...
|
|
83
|
+
- run: terraform plan -refresh-only -input=false -lock=false -out=drift.tfplan
|
|
84
|
+
working-directory: envs/prod
|
|
85
|
+
- uses: mikonoid/tfscout@v0
|
|
86
|
+
with:
|
|
87
|
+
mode: drift
|
|
88
|
+
working-directory: envs/prod
|
|
89
|
+
plan: drift.tfplan
|
|
90
|
+
provider: gemini
|
|
91
|
+
api-key: ${{ secrets.GEMINI_API_KEY }}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Full workflows with a matrix over several directories: [examples/review.yml](https://github.com/mikonoid/tfscout/blob/main/examples/review.yml),
|
|
95
|
+
[examples/drift.yml](https://github.com/mikonoid/tfscout/blob/main/examples/drift.yml).
|
|
96
|
+
One-time AWS OIDC role for CI without stored keys: [examples/aws-oidc](https://github.com/mikonoid/tfscout/blob/main/examples/aws-oidc/main.tf).
|
|
97
|
+
|
|
98
|
+
### Inputs
|
|
99
|
+
|
|
100
|
+
| Input | Default | Description |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `mode` | — | `review` or `drift` |
|
|
103
|
+
| `plan` | — | Binary plan (`-out`) or `terraform show -json` output, relative to `working-directory` |
|
|
104
|
+
| `working-directory` | `.` | Root module directory |
|
|
105
|
+
| `name` | working-directory | Label used in titles; keeps comments/issues per module separate |
|
|
106
|
+
| `provider` | `none` | `none`, `gemini`, `groq`, `openrouter`, `openai`, `ollama`, `custom` |
|
|
107
|
+
| `model` / `base-url` | preset | Override model (comma-separated = fallback chain on 503/429/timeout/404) / endpoint |
|
|
108
|
+
| `timeout` | `90` | Seconds to wait for the LLM per attempt |
|
|
109
|
+
| `reasoning-effort` | preset | `minimal`/`low`/`medium`/`high` for thinking models (gemini: `low`) |
|
|
110
|
+
| `api-key` | — | LLM API key (use a secret) |
|
|
111
|
+
| `comment` | `true` | Post PR comment (review) or sync the drift issue (drift) |
|
|
112
|
+
| `fail-on` | `none` | review: fail when a finding is at least this severity |
|
|
113
|
+
| `fail-on-drift` | `false` | drift: fail when actionable drift exists |
|
|
114
|
+
| `terraform-bin` | `terraform` | Use `tofu` for OpenTofu |
|
|
115
|
+
|
|
116
|
+
Outputs: `max-severity`, `findings-count`, `drift-detected`, `fingerprint`, `report-file`.
|
|
117
|
+
The report is also added to the job summary.
|
|
118
|
+
|
|
119
|
+
## LLM providers
|
|
120
|
+
|
|
121
|
+
tfscout speaks the OpenAI-compatible `/chat/completions` API, so you can use any of these:
|
|
122
|
+
|
|
123
|
+
| Provider | Cost | Key |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| `gemini` (fallback chain `gemini-flash-lite-latest` → `gemini-3.5-flash` → `gemini-flash-latest`) | free tier | [AI Studio](https://aistudio.google.com/apikey) → `GEMINI_API_KEY` |
|
|
126
|
+
| `groq` | free tier | `GROQ_API_KEY` |
|
|
127
|
+
| `openrouter` | free models available | `OPENROUTER_API_KEY` |
|
|
128
|
+
| `openai` | paid | `OPENAI_API_KEY` |
|
|
129
|
+
| `ollama` | free, local/self-hosted runner | none |
|
|
130
|
+
| `custom` | — | any OpenAI-compatible endpoint (Azure AI Foundry, vLLM, LM Studio…) |
|
|
131
|
+
|
|
132
|
+
## Security & privacy
|
|
133
|
+
|
|
134
|
+
- `terraform show -json` prints sensitive values **in clear text**. Before anything is sent to an LLM, tfscout
|
|
135
|
+
applies Terraform's own `*_sensitive` masks and also redacts values by key name (`password`, `token`,
|
|
136
|
+
`private_key`…) and by pattern (AWS keys, PEM keys, GitHub/Slack/OpenAI tokens, JWTs, DB URLs with
|
|
137
|
+
credentials).
|
|
138
|
+
- Only redacted attribute diffs are sent, never the full state.
|
|
139
|
+
- `provider: none` sends nothing anywhere.
|
|
140
|
+
- Use read-only cloud credentials for planning. tfscout never runs `apply`.
|
|
141
|
+
|
|
142
|
+
## CLI
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
pip install git+https://github.com/mikonoid/tfscout # PyPI package coming soon
|
|
146
|
+
|
|
147
|
+
terraform plan -out=tfplan
|
|
148
|
+
tfscout review tfplan # rules only
|
|
149
|
+
GEMINI_API_KEY=... tfscout review tfplan --provider gemini
|
|
150
|
+
tfscout review plan.json --format json --fail-on high # exit 3 on high/critical
|
|
151
|
+
|
|
152
|
+
terraform plan -refresh-only -out=drift.tfplan
|
|
153
|
+
tfscout drift drift.tfplan --provider ollama --exit-code # exit 2 on actionable drift
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Works with GitLab CI, Jenkins and locally too. Requires Python ≥ 3.10 and has no third-party dependencies.
|
|
157
|
+
|
|
158
|
+
## Roadmap
|
|
159
|
+
|
|
160
|
+
- [ ] CloudTrail attribution for drift ("who changed it and when")
|
|
161
|
+
- [ ] Cost delta via Infracost
|
|
162
|
+
- [ ] Auto-generated PR that codifies a manual change
|
|
163
|
+
- [ ] Custom org rules (`.tfscout.yml`: required tags, allowed regions, naming)
|
|
164
|
+
- [ ] GCP/Azure rule coverage
|
|
165
|
+
|
|
166
|
+
## Development
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
|
|
170
|
+
.venv/bin/pytest -q && .venv/bin/ruff check . && .venv/bin/ruff format --check .
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT
|
tfscout-0.1.0/action.yml
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
name: tfscout
|
|
2
|
+
description: AI-assisted Terraform/OpenTofu plan review and drift detection. Explains risks in PR comments and drift in issues.
|
|
3
|
+
author: mikonoid
|
|
4
|
+
branding:
|
|
5
|
+
icon: search
|
|
6
|
+
color: purple
|
|
7
|
+
|
|
8
|
+
inputs:
|
|
9
|
+
mode:
|
|
10
|
+
description: "`review` (comment on a PR with plan risks) or `drift` (analyse a `plan -refresh-only` and sync an issue)"
|
|
11
|
+
required: true
|
|
12
|
+
plan:
|
|
13
|
+
description: Path to the plan file (binary `-out` file or `terraform show -json` output), relative to working-directory
|
|
14
|
+
required: true
|
|
15
|
+
working-directory:
|
|
16
|
+
description: Terraform root module directory (needed to read binary plans)
|
|
17
|
+
default: "."
|
|
18
|
+
name:
|
|
19
|
+
description: Label for this root module, used in comment/issue titles and to keep them separate (defaults to working-directory)
|
|
20
|
+
default: ""
|
|
21
|
+
provider:
|
|
22
|
+
description: "LLM provider: none (rules only), gemini (free tier), groq, openrouter, openai, ollama, custom"
|
|
23
|
+
default: none
|
|
24
|
+
model:
|
|
25
|
+
description: Override the model name
|
|
26
|
+
default: ""
|
|
27
|
+
base-url:
|
|
28
|
+
description: Override the OpenAI-compatible endpoint (required for provider `custom`)
|
|
29
|
+
default: ""
|
|
30
|
+
api-key:
|
|
31
|
+
description: API key for the LLM provider (pass it from a secret)
|
|
32
|
+
default: ""
|
|
33
|
+
timeout:
|
|
34
|
+
description: Seconds to wait for the LLM per attempt
|
|
35
|
+
default: "90"
|
|
36
|
+
reasoning-effort:
|
|
37
|
+
description: "reasoning_effort for thinking models: minimal|low|medium|high (gemini default: low)"
|
|
38
|
+
default: ""
|
|
39
|
+
github-token:
|
|
40
|
+
description: Token used for PR comments and drift issues
|
|
41
|
+
default: ${{ github.token }}
|
|
42
|
+
comment:
|
|
43
|
+
description: "review: create/update a PR comment. drift: create/update/close a drift issue"
|
|
44
|
+
default: "true"
|
|
45
|
+
fail-on:
|
|
46
|
+
description: "review: fail the step when a finding has at least this severity (none|info|low|medium|high|critical)"
|
|
47
|
+
default: none
|
|
48
|
+
fail-on-drift:
|
|
49
|
+
description: "drift: fail the step when actionable drift is found"
|
|
50
|
+
default: "false"
|
|
51
|
+
terraform-bin:
|
|
52
|
+
description: terraform or tofu binary used to read binary plans
|
|
53
|
+
default: terraform
|
|
54
|
+
|
|
55
|
+
outputs:
|
|
56
|
+
max-severity:
|
|
57
|
+
description: "review: highest finding severity (or `none`)"
|
|
58
|
+
value: ${{ steps.run.outputs.max-severity }}
|
|
59
|
+
findings-count:
|
|
60
|
+
description: Number of findings (review) or actionable drift items (drift)
|
|
61
|
+
value: ${{ steps.run.outputs.findings-count }}
|
|
62
|
+
drift-detected:
|
|
63
|
+
description: "drift: `true` when actionable drift was found"
|
|
64
|
+
value: ${{ steps.run.outputs.drift-detected }}
|
|
65
|
+
fingerprint:
|
|
66
|
+
description: "drift: stable id of the current drift"
|
|
67
|
+
value: ${{ steps.run.outputs.fingerprint }}
|
|
68
|
+
report-file:
|
|
69
|
+
description: Path to the rendered Markdown report
|
|
70
|
+
value: ${{ steps.run.outputs.report-file }}
|
|
71
|
+
|
|
72
|
+
runs:
|
|
73
|
+
using: composite
|
|
74
|
+
steps:
|
|
75
|
+
- id: run
|
|
76
|
+
shell: bash
|
|
77
|
+
working-directory: ${{ inputs.working-directory }}
|
|
78
|
+
env:
|
|
79
|
+
PYTHONPATH: ${{ github.action_path }}/src
|
|
80
|
+
IN_MODE: ${{ inputs.mode }}
|
|
81
|
+
IN_PLAN: ${{ inputs.plan }}
|
|
82
|
+
IN_NAME: ${{ inputs.name || inputs.working-directory }}
|
|
83
|
+
IN_PROVIDER: ${{ inputs.provider }}
|
|
84
|
+
IN_MODEL: ${{ inputs.model }}
|
|
85
|
+
IN_BASE_URL: ${{ inputs.base-url }}
|
|
86
|
+
IN_COMMENT: ${{ inputs.comment }}
|
|
87
|
+
IN_FAIL_ON: ${{ inputs.fail-on }}
|
|
88
|
+
IN_FAIL_ON_DRIFT: ${{ inputs.fail-on-drift }}
|
|
89
|
+
IN_TF_BIN: ${{ inputs.terraform-bin }}
|
|
90
|
+
TFSCOUT_API_KEY: ${{ inputs.api-key }}
|
|
91
|
+
TFSCOUT_TIMEOUT: ${{ inputs.timeout }}
|
|
92
|
+
TFSCOUT_REASONING_EFFORT: ${{ inputs.reasoning-effort }}
|
|
93
|
+
GITHUB_TOKEN: ${{ inputs.github-token }}
|
|
94
|
+
run: |
|
|
95
|
+
set -euo pipefail
|
|
96
|
+
if ! python3 -c 'import sys; sys.exit(sys.version_info < (3, 10))' 2>/dev/null; then
|
|
97
|
+
echo "::error::tfscout needs python3 >= 3.10 on the runner (add actions/setup-python)"
|
|
98
|
+
exit 1
|
|
99
|
+
fi
|
|
100
|
+
case "$IN_MODE" in review|drift) ;; *) echo "::error::mode must be 'review' or 'drift'"; exit 1 ;; esac
|
|
101
|
+
|
|
102
|
+
report="$RUNNER_TEMP/tfscout-${IN_MODE}-$(printf '%s' "$IN_NAME" | tr -c 'A-Za-z0-9._-' '_').md"
|
|
103
|
+
args=("$IN_MODE" "$IN_PLAN" --name "$IN_NAME" --provider "$IN_PROVIDER" --terraform-bin "$IN_TF_BIN" --output "$report")
|
|
104
|
+
[[ -n "$IN_MODEL" ]] && args+=(--model "$IN_MODEL")
|
|
105
|
+
[[ -n "$IN_BASE_URL" ]] && args+=(--base-url "$IN_BASE_URL")
|
|
106
|
+
if [[ "$IN_MODE" == review ]]; then
|
|
107
|
+
[[ "$IN_COMMENT" == true ]] && args+=(--github-comment)
|
|
108
|
+
args+=(--fail-on "$IN_FAIL_ON")
|
|
109
|
+
else
|
|
110
|
+
[[ "$IN_COMMENT" == true ]] && args+=(--github-issue)
|
|
111
|
+
[[ "$IN_FAIL_ON_DRIFT" == true ]] && args+=(--exit-code)
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
echo "report-file=$report" >> "$GITHUB_OUTPUT"
|
|
115
|
+
python3 -m tfscout "${args[@]}"
|