Metadata-Version: 2.4
Name: opsight
Version: 0.8.0
Summary: Infrastructure intelligence engine — Terraform audit CLI
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: typer[all]>=0.12
Requires-Dist: rich>=13
Requires-Dist: jinja2>=3.1
Requires-Dist: pyyaml>=6
Requires-Dist: python-hcl2>=4
Requires-Dist: pydantic>=2
Provides-Extra: scanners
Requires-Dist: checkov>=3; extra == "scanners"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-cov>=5; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"

# ⚡ Opsight

[![CI](https://github.com/Trifel-guy/Opsight/actions/workflows/ci.yml/badge.svg)](https://github.com/Trifel-guy/Opsight/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/opsight)](https://pypi.org/project/opsight/)
[![Release](https://img.shields.io/github/v/release/Trifel-guy/Opsight?sort=semver)](https://github.com/Trifel-guy/Opsight/releases/latest)
![Python](https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12-blue)
![Type checked: mypy strict](https://img.shields.io/badge/mypy-strict-blue)
![Lint: ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)
![License: MIT](https://img.shields.io/badge/license-MIT-green)

> **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](#features)
- [How it works](#how-it-works)
- [Installation](#installation)
- [Prerequisites](#prerequisites)
- [Usage](#usage)
- [Scaffolding](#scaffolding)
- [Configuration](#configuration)
- [Scoring model](#scoring-model)
- [Output reports](#output-reports)
- [CI integration (GitHub Action & SARIF)](#ci-integration-github-action--sarif)
- [Architecture](#architecture)
- [Development](#development)
- [Roadmap](#roadmap)
- [License](#license)

---

## Features

- 🔒 **Security** auditing via [Checkov](https://www.checkov.io/)
- 🔧 **Quality / linting** via [tflint](https://github.com/terraform-linters/tflint)
- 💰 **Cost** estimation via [Infracost](https://www.infracost.io/) *(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
- 🪄 **`op` alias** — every command is also available under the shorter `op`

---

## How it works

```
Terraform dir
     │
     ▼
┌─────────────┐   raw dicts   ┌───────────────┐   Finding[]   ┌────────────────┐
│  Scanners   │ ────────────▶ │ Normalization │ ────────────▶ │ Prioritization │
│ tflint      │               │   service     │               │    service     │
│ checkov     │               └───────────────┘               └────────┬───────┘
│ infracost   │                                                        │
└─────────────┘                                                        ▼
                                                              ┌─────────────────┐
                                                              │  Scoring service │
                                                              └────────┬────────┘
                                                                       ▼
                                                        ScanReport → JSON / HTML / terminal
```

1. **Scan** — every enabled scanner runs against the target path and returns raw,
   tool-specific dicts.
2. **Normalize** — raw results are coerced into a canonical `Finding` model with a
   unified `Severity` and `Category`.
3. **Prioritize** — findings are sorted (security first, critical first).
4. **Score** — per-category and weighted global scores are computed and clamped to `[0, 100]`.
5. **Report** — results are rendered to the terminal and written to JSON / HTML.

---

## Installation

From [PyPI](https://pypi.org/project/opsight/):

```bash
pip install opsight
```

Or with [pipx](https://pipx.pypa.io/) to get an isolated, always-available CLI:

```bash
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:

```bash
pip install "opsight[scanners]"
```

The other scanners (`tflint`, `terraform`, `infracost`) are external binaries —
see [Installing the scanners](#installing-the-scanners).

<details>
<summary>From source (for development)</summary>

```bash
# 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)
```

</details>

---

## 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:

```bash
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](https://github.com/terraform-linters/tflint#installation) |
| `terraform` | `brew install terraform`| `choco install terraform` | [HashiCorp repo](https://developer.hashicorp.com/terraform/install)  |
| `infracost` | `brew install infracost`| `choco install infracost` | [install script](https://www.infracost.io/docs/#1-install-infracost) |

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):

```bash
export INFRACOST_API_KEY="ico-xxxxxxxxxxxx"      # bash / Git Bash
```
```powershell
$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):

```bash
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](https://dashboard.infracost.io) (Org Settings → API keys).

Then **enable the scanner** (it is off by default) in `opsight.yaml`:

```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 **`op`** alias —
> e.g. `op scan ./infra`, `op module new vpc`, `op provider add aws`.

```bash
# Generate a starter opsight.yaml (optional)
opsight init

# 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

# Lint sub-directories / modules too (tflint --recursive)
opsight scan ./infra --recursive

# 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`, `sarif`, `both`  |
| `--no-cost`       |       | `false`       | Skip the Infracost scanner                       |
| `--no-security`   |       | `false`       | Skip the Checkov scanner                         |
| `--recursive`     |       | *(config)*    | Run tflint over sub-directories/modules          |
| `--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>/`:

```bash
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):

```bash
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 fmt` is run on the generated files when `terraform` is on your `PATH`;
> otherwise it is skipped silently.

---

## Configuration

Generate a commented starter file with every default spelled out:

```bash
opsight init            # writes ./opsight.yaml (use --dir / --force as needed)
```

The file is **validated** against a schema on load (Pydantic): unknown keys
(typos) and wrong types fail fast with a clear error rather than being silently
ignored. Opsight reads an `opsight.yaml` file (override with `--config`). Defaults:

```yaml
# Opsight default configuration
scanners:
  tflint:
    enabled: true
    recursive: false        # also lint sub-directories/modules (tflint --recursive)
    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.
- **SARIF** — `opsight.sarif` (`--format sarif`): SARIF 2.1.0 for GitHub code
  scanning. Findings land in the repo's **Security** tab and as PR annotations,
  with severity buckets driven by `security-severity`.

---

## CI integration (GitHub Action & SARIF)

Use the bundled composite action to scan Terraform and upload findings to GitHub
code scanning on every push/PR:

```yaml
# .github/workflows/opsight.yml
name: Opsight
on: [push, pull_request]

permissions:
  contents: read
  security-events: write        # required to upload SARIF

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: Trifel-guy/Opsight@v1
        with:
          path: ./infra          # Terraform directory (default ".")
          fail-under: 70          # optional: fail the job below this score
```

Prefer to wire it yourself? Run the CLI and upload the SARIF directly:

```yaml
      - run: pip install "opsight[scanners]"
      - run: opsight scan ./infra --format sarif
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: opsight.sarif
```

> **Note:** uploading SARIF to code scanning is free on public repositories; on
> **private** repos it requires GitHub Advanced Security. The `tflint` /
> `terraform` binaries aren't installed by the action — add their setup steps
> first if you want those scanners too (checkov ships via `opsight[scanners]`).

---

## 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

```bash
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

- [x] **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`.
