Infrastructure intelligence engine — Terraform audit CLI
Project description
⚡ Opsight
Infrastructure Intelligence Engine — a Terraform audit & scaffolding CLI: aggregate security, cost and quality findings into a single graded health report, and scaffold modules and providers from best-practice templates.
Opsight runs a suite of best-in-class scanners against a Terraform directory, normalizes their heterogeneous output into a canonical model, computes a weighted A→F health score, and produces rich terminal, JSON and HTML reports. It also generates Terraform boilerplate — new modules and provider blocks with sane version constraints — straight from the command line.
Table of contents
- Features
- How it works
- Installation
- Prerequisites
- Usage
- Scaffolding
- Configuration
- Scoring model
- Output reports
- Architecture
- Development
- Roadmap
- License
Features
- 🔒 Security auditing via Checkov
- 🔧 Quality / linting via tflint
- 💰 Cost estimation via Infracost (optional, requires an API key)
- 📊 Weighted health score per category + a global grade (A → F)
- 🎯 Prioritized findings — sorted by severity then category importance
- 📄 Multi-format reports — terminal (Rich), JSON and a self-contained HTML page
- 🧩 Pluggable scanners — add a new tool by implementing a single
run()method - ⚙️ Fully configurable via
opsight.yaml(enable/disable scanners, weights, penalties) - 🚦 CI-friendly exit codes — non-zero when the global score drops below 60
- 🏗️ Scaffolding — generate modules (
module new) and add providers (provider add) with pessimistic~>constraints resolved from the Terraform Registry - 🪄
opalias — every command is also available under the shorterop
How it works
Terraform dir
│
▼
┌─────────────┐ raw dicts ┌───────────────┐ Finding[] ┌────────────────┐
│ Scanners │ ────────────▶ │ Normalization │ ────────────▶ │ Prioritization │
│ tflint │ │ service │ │ service │
│ checkov │ └───────────────┘ └────────┬───────┘
│ infracost │ │
└─────────────┘ ▼
┌─────────────────┐
│ Scoring service │
└────────┬────────┘
▼
ScanReport → JSON / HTML / terminal
- Scan — every enabled scanner runs against the target path and returns raw, tool-specific dicts.
- Normalize — raw results are coerced into a canonical
Findingmodel with a unifiedSeverityandCategory. - Prioritize — findings are sorted (security first, critical first).
- Score — per-category and weighted global scores are computed and clamped to
[0, 100]. - Report — results are rendered to the terminal and written to JSON / HTML.
Installation
From PyPI:
pip install opsight
Or with pipx to get an isolated, always-available CLI:
pipx install opsight
This exposes both the opsight command and its shorter op alias.
To pull in the checkov scanner at the same time, install the extra:
pip install "opsight[scanners]"
The other scanners (tflint, terraform, infracost) are external binaries —
see Installing the scanners.
From source (for development)
# From the package directory (contains pyproject.toml):
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -e . # runtime install
pip install -e ".[dev]" # with dev tooling (pytest, ruff, mypy)
Prerequisites
Opsight orchestrates external CLI tools — install the ones you need and make sure
they are on your PATH:
| Tool | Purpose | Required? |
|---|---|---|
terraform |
parse / init infra | recommended |
tflint |
quality / linting | for the quality scanner |
checkov |
security scanning | for the security scanner |
infracost |
cost estimation | optional — needs INFRACOST_API_KEY env var |
Installing the scanners
checkov is a Python tool, so it ships as an optional extra — install it together
with Opsight:
pip install "opsight[scanners]" # Opsight + checkov
tflint, terraform and infracost are standalone Go binaries (not on PyPI),
so pip cannot install them. Use your platform's package manager and make sure they
are on your PATH:
| Tool | macOS (brew) | Windows (scoop / choco) | Linux |
|---|---|---|---|
tflint |
brew install tflint |
scoop install tflint |
install script |
terraform |
brew install terraform |
choco install terraform |
HashiCorp repo |
infracost |
brew install infracost |
choco install infracost |
install script |
Opsight skips any scanner whose binary is missing (with a warning) and still runs with whatever is available.
Infracost API key
Opsight never reads the key itself — it just runs the infracost CLI as a
subprocess, which inherits the environment. So you provide the key exactly the
way infracost expects, in one of two ways:
Option 1 — environment variable (the subprocess inherits it):
export INFRACOST_API_KEY="ico-xxxxxxxxxxxx" # bash / Git Bash
$env:INFRACOST_API_KEY = "ico-xxxxxxxxxxxx" # PowerShell, current session
setx INFRACOST_API_KEY "ico-xxxxxxxxxxxx" # PowerShell, persistent (reopen shell)
Option 2 — Infracost's own credential store (no env var needed afterwards):
infracost auth login # opens the browser, fetches a free key
# or set it directly:
infracost configure set api_key ico-xxxxxxxxxxxx
Get a free key with infracost auth login or from the
Infracost dashboard (Org Settings → API keys).
Then enable the scanner (it is off by default) in opsight.yaml:
scanners:
infracost:
enabled: true
Finally, run a scan without --no-cost, with infracost on your PATH. A cost
result is only emitted as a finding when a resource's monthly cost exceeds $10.
Usage
Every command is also available under the shorter
opalias — e.g.op scan ./infra,op module new vpc,op provider add aws.
# Scan a Terraform directory (writes JSON + HTML by default)
opsight scan ./infra
# Verbose — show the full findings table in the terminal
opsight scan ./infra -v
# Pick output formats and directory
opsight scan ./infra --format json,html --output ./reports
# Skip specific scanners
opsight scan ./infra --no-cost --no-security
# Use a custom configuration file
opsight scan ./infra --config my-opsight.yaml
# Print the version
opsight version
scan options
| Option | Alias | Default | Description |
|---|---|---|---|
PATH |
(required) | Path to the Terraform directory | |
--config |
-c |
opsight.yaml |
Path to a configuration file |
--output |
-o |
. |
Output directory for reports |
--format |
-f |
json,html |
Report formats: json, html, both |
--no-cost |
false |
Skip the Infracost scanner | |
--no-security |
false |
Skip the Checkov scanner | |
--verbose |
-v |
false |
Show detailed findings in the terminal |
--fail-under |
60 |
Exit non-zero if the global score is below this |
Exit codes
| Code | Meaning |
|---|---|
0 |
Global score ≥ threshold |
1 |
Global score < threshold, or invalid path |
The threshold defaults to 60 and can be set with --fail-under (CLI) or
scoring.fail_under (config) — the flag wins. This makes Opsight easy to drop into
a CI pipeline as a quality gate.
Scaffolding
Beyond auditing, Opsight scaffolds Terraform boilerplate from versioned templates. All writes are explicit and safe — existing files are never silently merged.
module new <name>
Scaffold a reusable module under <dir>/modules/<name>/:
opsight module new networking # creates ./modules/networking/
opsight module new vpc --dir infra # creates ./infra/modules/vpc/
opsight module new vpc --force # overwrite an existing module
It generates main.tf, variables.tf, outputs.tf, versions.tf (with a
terraform { required_version = … } block) and a README.md. The command aborts
if the target already exists, unless --force is given. Names must be lowercase
kebab- or snake_case — no path separators or .. traversal.
| Option | Alias | Default | Description |
|---|---|---|---|
NAME |
(required) | Module name (kebab- or snake_case) | |
--dir |
-d |
. |
Base directory that contains modules/ |
--force |
false |
Overwrite an existing module directory |
provider add <name> [--version]
Declare a provider the right way — required_providers in versions.tf and the
provider "<name>" {} block in providers.tf (each file created if missing):
opsight provider add scaleway # resolve latest -> ~> X.Y
opsight provider add scaleway --version "~> 2.5" # pin a constraint yourself
opsight provider add hashicorp/aws # explicit namespace/type
opsight provider add scaleway --dir infra # target another directory
Version resolution. Without --version, Opsight queries the Terraform Registry
for the latest stable release and builds a pessimistic constraint — 2.76.1
becomes ~> 2.76 (i.e. >= 2.76.0, < 3.0.0), allowing minor/patch upgrades while
blocking breaking majors. A bare name is resolved by trying hashicorp/<name> then
<name>/<name>; pass an explicit namespace/type to be precise. If the registry is
unreachable, supply --version to work fully offline.
Idempotency. If the provider is already declared, Opsight warns and changes
nothing (exit code 3) — never a duplicate entry. When versions.tf already has a
required_providers block, the new entry is inserted safely; if no such block can
be located, Opsight refuses rather than attempt a fragile HCL merge.
| Option | Alias | Default | Description |
|---|---|---|---|
NAME |
(required) | Provider name or namespace/type |
|
--version |
(auto) | Version constraint, e.g. ~> 2.5 |
|
--dir |
-d |
. |
Target Terraform directory |
Exit codes
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Error (invalid name, module exists, registry down) |
3 |
Provider already declared — skipped, nothing changed |
terraform fmtis run on the generated files whenterraformis on yourPATH; otherwise it is skipped silently.
Configuration
Opsight reads an opsight.yaml file (override with --config). Defaults:
# Opsight default configuration
scanners:
tflint:
enabled: true
extra_args: []
checkov:
enabled: true
extra_args: ["--compact", "--quiet"]
infracost:
enabled: false # requires INFRACOST_API_KEY env var
extra_args: []
cost_threshold: 10.0 # flag resources costing at least this much (USD/month)
scoring:
base: 100
weights:
security: 1.5
cost: 1.2
quality: 1.0
maintainability: 1.0
penalties:
low: 1
medium: 3
high: 7
critical: 15
fail_under: 60 # exit non-zero below this global score
output:
formats: ["json", "html"]
directory: "."
json_filename: "opsight-report.json"
html_filename: "opsight-report.html"
codegen:
terraform_required_version: ">= 1.5.0" # injected into generated versions.tf
registry:
base_url: "https://registry.terraform.io"
cache_ttl: 86400 # cache resolved provider versions (seconds; 0 disables)
logging:
level: "INFO"
Scoring model
Each category starts at base (100) and loses points for every finding:
penalty(finding) = severity_penalty × category_weight
category_score = max(0, base − Σ penalties in that category)
global_score = round( Σ(category_score × weight) / Σ weights ), clamped to [0, 100]
Severity penalties (default): low = 1, medium = 3, high = 7, critical = 15.
Category weights (default): security = 1.5, cost = 1.2, quality = 1.0,
maintainability = 1.0.
Letter grades (from the global score):
| Grade | Score range |
|---|---|
| A | 90 – 100 |
| B | 75 – 89 |
| C | 60 – 74 |
| D | 40 – 59 |
| F | 0 – 39 |
Output reports
- Terminal — a Rich summary table (per-category scores, grade, finding counts)
plus an optional detailed findings table with
-v. - JSON —
opsight-report.json: machine-readable, full finding detail (id, title, category, severity, source, resource, file, line, description, impact, recommendation). - HTML —
opsight-report.html: a self-contained page rendered from a Jinja2 template, suitable for sharing or archiving as a CI artifact.
Architecture
Opsight follows a hexagonal (ports & adapters) layout:
opsight/
├── domain/ # Pure business core (no I/O)
│ ├── models/ # Finding, Score, ScanResult
│ ├── value_objects/ # Severity, Category
│ ├── services/ # normalization, prioritization, scoring
│ └── codegen/ # ABC ports, models, services, value objects
├── application/ # Orchestration
│ ├── use_cases/ # RunScanUseCase
│ ├── dto/ # ScanReportDTO
│ └── codegen/ # NewModuleUseCase, AddProviderUseCase
├── infrastructure/ # Adapters to the outside world
│ ├── scanners/ # tflint, checkov, infracost (+ base, factory)
│ ├── terraform/ # executor (init/plan/fmt), parser (python-hcl2)
│ └── codegen/ # jinja renderer, local FS, HTTP registry, tf runner
├── interfaces/
│ └── cli/ # Typer entry point (main.py) + codegen sub-apps
├── shared/ # Cross-cutting concerns
│ ├── config/ # YAML loading
│ ├── logging/ # logger setup
│ └── reporting/ # JSON + HTML reporters
└── templates/ # report.html.j2 + terraform/ HCL templates
Adding a new scanner: subclass BaseScanner, implement run(path) -> list[dict],
set name, and register it in ScannerFactory. The domain layer needs no changes —
normalization maps the raw output onto the canonical Finding model.
Codegen keeps the same discipline: business logic (templating decisions, version
resolution, idempotency) lives in domain/codegen, while all I/O — filesystem, HTTP
registry, the terraform process — sits behind ABC ports (TemplateRenderer,
FileSystemPort, RegistryClient, TerraformRunner) with concrete adapters in
infrastructure/codegen. HCL templates are versioned under templates/terraform/.
Development
pip install -e ".[dev]"
pytest # run the test suite (unit + integration)
pytest --cov=opsight # with coverage
ruff check . # lint
mypy opsight # type-check (strict mode)
Tooling configuration lives in pyproject.toml:
- ruff — line length 100, target
py311 - mypy —
strict = true - pytest —
testpaths = ["tests"], verbose with short tracebacks
Roadmap
- Scaffolding (Phase A) —
module new,provider add - Additional scanners (e.g. Trivy, OPA/Conftest)
- SARIF output for native GitHub code-scanning integration
- Baseline / diff mode (compare against a previous report)
- Per-finding severity overrides and suppression rules
License
MIT — see pyproject.toml.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file opsight-0.4.0.tar.gz.
File metadata
- Download URL: opsight-0.4.0.tar.gz
- Upload date:
- Size: 43.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ad4564c6c0edce2739f9b0aa35ab50a91fb6f3ec10befd50c3527cf4e448e7e0
|
|
| MD5 |
49fbd37162f06ce2d2d846d2eae10188
|
|
| BLAKE2b-256 |
cd055d429db804e6d218fe07be8c614a67da9bd1b061be2994a01619387047c5
|
Provenance
The following attestation bundles were made for opsight-0.4.0.tar.gz:
Publisher:
release.yml on Trifel-guy/Opsight
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
opsight-0.4.0.tar.gz -
Subject digest:
ad4564c6c0edce2739f9b0aa35ab50a91fb6f3ec10befd50c3527cf4e448e7e0 - Sigstore transparency entry: 1912336074
- Sigstore integration time:
-
Permalink:
Trifel-guy/Opsight@28ef506d4c6f1767105660272a6e2c3045869ce6 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/Trifel-guy
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@28ef506d4c6f1767105660272a6e2c3045869ce6 -
Trigger Event:
push
-
Statement type:
File details
Details for the file opsight-0.4.0-py3-none-any.whl.
File metadata
- Download URL: opsight-0.4.0-py3-none-any.whl
- Upload date:
- Size: 54.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
623a310241aeb447ce8fe965442ba52d500f3a2586859fcc096c5b9bbe51e802
|
|
| MD5 |
5c300cf7cae65897d9f8e526d93af75d
|
|
| BLAKE2b-256 |
7aeec991fbd8960d7b58593f734b74d70466998e131926476e02536fe5b4a5f5
|
Provenance
The following attestation bundles were made for opsight-0.4.0-py3-none-any.whl:
Publisher:
release.yml on Trifel-guy/Opsight
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
opsight-0.4.0-py3-none-any.whl -
Subject digest:
623a310241aeb447ce8fe965442ba52d500f3a2586859fcc096c5b9bbe51e802 - Sigstore transparency entry: 1912336175
- Sigstore integration time:
-
Permalink:
Trifel-guy/Opsight@28ef506d4c6f1767105660272a6e2c3045869ce6 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/Trifel-guy
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@28ef506d4c6f1767105660272a6e2c3045869ce6 -
Trigger Event:
push
-
Statement type: