Metadata-Version: 2.1
Name: vulnscan-ai
Version: 0.4.14
Summary: RHEL vulnerability scanner with AI-assisted, approval-gated remediation (FIPS-aware)
Home-page: https://vulnscan-ai.techhack.nl
License: AGPL-3.0-or-later
Project-URL: Source, https://github.com/btdt1983/vulnscan-ai
Project-URL: Repository, https://repo.techhack.nl
Keywords: security,rhel,cve,vulnerability,fips,remediation
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Security
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: pdf
Requires-Dist: reportlab (>=3.6) ; extra == 'pdf'

# vulnscan-ai

[![CI](https://github.com/btdt1983/vulnscan-ai/actions/workflows/ci.yml/badge.svg)](https://github.com/btdt1983/vulnscan-ai/actions/workflows/ci.yml)
[![License: AGPL-3.0-or-later](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue)](LICENSE)
[![Release](https://img.shields.io/github/v/release/btdt1983/vulnscan-ai)](https://github.com/btdt1983/vulnscan-ai/releases)

**[Website](https://vulnscan-ai.techhack.nl)** · **[Install via dnf](https://repo.techhack.nl)** · **[Command reference](COMMANDS.md)** · **[Contributing](CONTRIBUTING.md)**

A FIPS-aware command-line tool for **RHEL-based distributions** (RHEL,
AlmaLinux, Rocky, CentOS Stream, Fedora) that:

1. **Scans** the host for known vulnerabilities using native and public sources,
2. **enriches** findings from online vulnerability databases,
3. uses an **LLM** (Claude by default; OpenAI / Gemini / Kimi / DeepSeek /
   Mistral / local optional) to **propose remediation**,
4. **applies fixes only after explicit approval** (or in `--dry-run`), screening
   every command against a safety deny-list,
5. exports a **PDF report** (or HTML when `reportlab` isn't installed).

It uses CLI tools under the hood (`dnf`/`yum`, `rpm`, optionally `oscap`) and
queries vulnerability websites (Red Hat Security Data API, NIST NVD).

---

## Why these choices

| Decision | Choice |
|---|---|
| Language | Python 3 (ships on RHEL; no mandatory 3rd-party deps) |
| Scanners | CVE: `dnf`/RHSA, OpenSCAP/OVAL, NVD/Red Hat feeds. Hardening/exposure: `ssh`, `systemd`, `ports`, `webroot`, `container`. Runtime posture: `effective` (reboot/restart still pending). Crypto posture: `fips` (FIPS-mode & crypto-policy gaps). Remote exposure: `network` (nmap against an authorized target allow-list) |
| Prioritisation | **CISA KEV** (actively exploited) + **EPSS** exploit-probability on every finding |
| Compliance | **CIS / STIG / PCI-DSS / HIPAA** benchmarks via OpenSCAP XCCDF (`scan --compliance`): score + failing rules + dashboard tab |
| Advisories | `news` command + dashboard tab: CISA KEV, NVD, distro errata (cached, offline-friendly) |
| Fix mode | **Suggest + approve** by default (safest for prod/FIPS) |
| AI backend | **Claude** default; pluggable adapters for the rest |

## FIPS posture

- No bundled cryptography. All TLS/hashing uses the **system OpenSSL**, which
  is the FIPS 140-validated module on RHEL.
- FIPS mode is auto-detected from `/proc/sys/crypto/fips_enabled`
  (`vulnscan-ai info` shows the status).
- Outbound HTTPS pins **TLS 1.2+** and honours the system crypto policy.
- Hashing uses **SHA-256** only; legacy digests (md5) are never used, so the
  tool keeps working when the FIPS policy disables them.
- For data-sensitive / air-gapped sites, use `--provider local` so no finding
  data leaves the host.

## Install

```bash
# core tool (no third-party deps required) — PDF works out of the box
pip install .

# optional: richer PDF layout via reportlab (otherwise the built-in
# dependency-free PDF writer is used automatically)
pip install '.[pdf]'        # pulls in reportlab
# or on RHEL:  dnf install python3-reportlab
```

Runs directly from a checkout too: `python -m vulnscanai ...`

## Configure the AI provider

The AI step proposes fixes (you approve them). Claude is the default. There are
two ways to set it up.

### Easiest: the setup wizard

Run **`vulnscan-ai setup`** (it also runs on the first interactive use) and pick:

- **A cloud provider** — `claude` / `openai` / `gemini` / `kimi` / `deepseek` /
  `mistral`. Paste the API key (hidden input), pick the model from a menu of known
  ids for that provider (a custom id is still allowed) and, for Claude, the
  reasoning effort. The key is stored in the per-user config (mode 0600) and used
  automatically — no environment variable to manage.
- **A local, offline model** — download an Ollama model sized to the host; no
  key, nothing leaves the machine.

> An API key is **not** a Claude Pro / ChatGPT Plus subscription — create a
> developer key (with billing) at the provider's console (`console.anthropic.com`,
> `platform.openai.com`, …). **Nothing to install for the cloud providers:**
> vulnscan-ai calls the plain REST API (no SDK, no "Claude Code") — you only need
> the key and network access to the provider.

### Manual: environment variables

Or set the key yourself — a real env var always wins over a wizard-stored one:

```bash
export ANTHROPIC_API_KEY=sk-ant-...      # claude (default)
export OPENAI_API_KEY=...                # openai
export GEMINI_API_KEY=...                # gemini
export MOONSHOT_API_KEY=...              # kimi
export DEEPSEEK_API_KEY=...              # deepseek (DeepSeek-Coder)
export MISTRAL_API_KEY=...               # mistral (Mixtral 8x7B)
export OLLAMA_HOST=http://127.0.0.1:11434  # local (no key)
export OLLAMA_MODEL=llama3.2:1b          # local model to use
export NVD_API_KEY=...                   # optional, higher NVD rate limit
```

DeepSeek and Mistral both speak the OpenAI Chat Completions API; override the
endpoint with `DEEPSEEK_BASE_URL` / `MISTRAL_BASE_URL` and the model with
`--model` (e.g. `deepseek-v4-pro`, `mistral-large-3`). **StarCoder 2** has no
hosted API — run it offline through the `local` provider:
`vulnscan-ai --provider local --model starcoder2 fix` (after `ollama pull starcoder2`).

**Model and reasoning effort.** Pick the model with `--model` (or the `model`
config), e.g. `--model claude-opus-5` for deeper reasoning on hard fixes. For Claude
you can also dial the **reasoning effort** with `--effort low|medium|high|xhigh|max`
(config `claude_effort`, env `VULNSCANAI_CLAUDE_EFFORT`) — it turns on adaptive
thinking, so the model reasons harder on tricky fixes. Use `max` when correctness
matters more than cost; other providers ignore the flag.

### Fully offline / air-gapped

**Package fixes need no AI at all.** For `dnf`/`oscap` (package/advisory)
findings, `fix` builds a deterministic plan — a scoped `dnf update -y
--advisory=<id>` — locally, with **no model and no network**. Run it fully
air-gapped with:

```bash
vulnscan-ai fix --offline        # deterministic dnf plan; never calls a provider
```

Config/service findings (ssh/systemd/ports/webroot/container) still need
reasoning; for those, either run a local model (below) or handle them manually
(`--offline` reports them as "no offline plan" and skips them). Disable the
catalog entirely with `fix --no-catalog` or `"offline_catalog": false`.

**Config-fix prompts are grounded with vetted SCAP Security Guide snippets, if
available.** When a host has `scap-security-guide` installed, `fix` looks up a
lexically-matching hardening rule from its SSG datastream (the same one `scan
--compliance` uses) and hands the model its peer-reviewed fix script as an
optional reference, adapted rather than copied verbatim — this cuts down on
hallucination for ssh/systemd/ports/webroot findings. No datastream installed?
It's a silent no-op, identical to before. Disable it with `fix
--no-scap-grounding` or `"scap_grounding": false`.

#### A local model for config-fix reasoning (Ollama)

No API key, no external calls — the AI step runs against a local model.

**Easiest: the setup wizard.** On the first interactive run the tool offers a
menu of offline models (sized to your host's RAM), can install Ollama, downloads
your pick, and saves it as the default. Run it any time with:

```bash
vulnscan-ai setup
```

```
 vulnscan-ai setup — offline AI model
 Detected RAM: 7.3 GB total, 2.0 GB available.
   #  model            download   needs RAM   notes
   1  qwen3:0.6b         0.5 GB        fits  tiny & fastest (recommended)
   2  llama3.2:1b        1.3 GB  tight/swap  good balance, CPU-friendly
   3  llama3.2:3b        2.0 GB  tight/swap  better quality
   ...
   0  skip for now
```

The auto-prompt only appears on an interactive terminal (never for the systemd
timer); suppress it with `VULNSCANAI_NO_SETUP=1`.

**GPU support.** Ollama runs the chosen model GPU-accelerated automatically when
an NVIDIA/AMD GPU with drivers is present — no different model or config needed.
The wizard detects the GPU, sizes the menu against **VRAM** (offering larger,
higher-quality models), and `vulnscan-ai info` reports the GPU. On a CPU-only
host it sizes against RAM and steers you to smaller models.

**Manual equivalent:**

```bash
curl -fsSL https://ollama.com/install.sh | sh
sudo systemctl enable --now ollama
ollama pull llama3.2:1b
vulnscan-ai providers            # 'local' shows 'ready' when the server answers
vulnscan-ai --provider local --model llama3.2:1b fix --min-severity important
```

**Keeping the local model current.** Ollama never refreshes a model on its own —
once pulled, it runs whatever sits on disk forever. Two different things can be
"newer", and only one of them is a re-pull:

```bash
vulnscan-ai setup --update       # re-pull what you have (picks up a moved tag)
vulnscan-ai setup                # switch generation, e.g. qwen2.5 -> qwen3
```

`--update` fetches only the changed layers and reports per model whether the tag
actually moved. It cannot cross generations: `qwen2.5` and `qwen3` are different
models, not two versions of one, so it also lists anything that has dropped off
the recommended list. `vulnscan-ai info` reports the same under AI providers.

The tool asks Ollama for JSON-constrained output, so even small models return
parseable remediation plans. Disabling enrichment (`--no-enrich`) makes a scan
fully offline too; the OVAL/RHSA data can be pre-staged with `update-oval` and
a mirrored repo.

## Usage

> See **[COMMANDS.md](COMMANDS.md)** for the full command reference (every
> command, all options, examples, exit codes, and config precedence).

```bash
# Prefer not to memorise flags? Run with no command for an interactive menu
# (arrow keys / numbered) that covers every command:
vulnscan-ai            # or: vulnscan-ai menu

# Show host / FIPS / scanner / provider status
vulnscan-ai info

# Scan, save findings, and write a PDF
vulnscan-ai scan --pdf report.pdf

# Only show important+ issues
vulnscan-ai scan --min-severity important

# Propose fixes with Claude and approve them interactively
vulnscan-ai fix

# Scan + fix in one go, dry-run (plan only, executes nothing), PDF out
vulnscan-ai fix --scan --dry-run --pdf plan.pdf

# Use a different provider/model
vulnscan-ai --provider openai --model gpt-5.6-sol fix
vulnscan-ai --provider claude --model claude-opus-5 fix

# Non-interactive (CI): auto-approve every screened fix
vulnscan-ai fix --yes

# Re-render a report from the last scan
vulnscan-ai report -o latest.pdf

# Scan with OpenSCAP/OVAL — the feed auto-refreshes when stale (>7 days);
# update-oval just pre-stages it (handy for air-gapped hosts)
vulnscan-ai update-oval
vulnscan-ai scan --scanner dnf --scanner oscap --pdf report.pdf

# Audit sshd hardening (root login, weak ciphers/MACs/KEX, ...)
vulnscan-ai scan --scanner ssh

# Audit systemd service sandboxing (systemd-analyze security)
vulnscan-ai scan --scanner systemd

# Audit network exposure (risky listening ports via ss)
vulnscan-ai scan --scanner ports

# Audit web document roots for exposed files (.sql dumps, .env, .git/, backups)
vulnscan-ai scan --scanner webroot

# Audit running Podman/Docker containers for unsafe runtime settings
vulnscan-ai scan --scanner container

# Audit the FIPS / crypto-policy posture (half-enabled FIPS, LEGACY policy, ...)
vulnscan-ai scan --scanner fips

# Audit remote hosts you are authorized to test (authorize targets first —
# vulnscan-ai network --add 10.0.0.0/24; see the `network` scanner below)
vulnscan-ai scan --scanner network

# Run every available scanner at once
vulnscan-ai scan --all

# Compliance benchmark (CIS / STIG / PCI-DSS / …) — score + failing rules
vulnscan-ai scan --list-profiles              # what this host offers
vulnscan-ai scan --compliance cis-l1 --pdf cis-l1.pdf
```

The `systemd` scanner is conservative by default (only `UNSAFE`, enabled
services, exposure ≥ 9.0, skipping un-hardenable units); widen or narrow it with
`VULNSCANAI_SYSTEMD_MIN_EXPOSURE`. Its fixes are systemd drop-ins applied through
the same transactional engine (`daemon-reload` → `systemd-analyze verify` →
restart → rollback).

### Keeping false positives low

The scanners are built to avoid noise:

- **OVAL (`oscap`)** reports only real *patch* advisories — `inventory`/compliance
  definitions (e.g. "the OS is installed") are dropped — with proper CVE ids and
  severities pulled from the feed metadata.
- **`ports`** suppresses ports the host firewall blocks (a socket on `0.0.0.0`
  isn't an exposure if the firewall drops it). It reads **firewalld** when
  running and falls back to raw **nftables** (`nft --json list ruleset`) on
  hosts without it — honouring a default-deny `input` policy, accept rules
  (single port, named sets, ranges) and explicit drop/reject rules. It only
  suppresses a port it can confidently prove blocked, so a parse miss never
  hides a real exposure.
- Findings that **`dnf` and `oscap` both report** (same advisory/CVE) are merged
  into one.
- **Already patched** (`dnf check-update`): a package finding whose fix is in the
  repo metadata but has *no installable update* is dropped — the host already has
  it. This clears the common **lingering-old-kernel** noise: old kernels stay
  installed, so the scanners keep listing historical kernel advisories that `dnf`
  reports as "Nothing to do" because the newest kernel is already on the system.
  Won't-fix advisories are never dropped this way. Disable with
  `"patched_filter": false`.
- **Vendor fix state** (during enrichment): Red Hat publishes, per CVE and per
  product, whether each package is actually affected. Findings Red Hat marks
  **"Not affected"** for this RHEL release are dropped as confirmed false
  positives; **"Will not fix" / "Out of support scope" / "Fix deferred"** are
  kept but annotated (a real issue with no dnf update coming — mitigate
  manually), so the AI won't propose a pointless `dnf update`. Disable with
  `"vendor_state_filter": false` in the config.
- **Runtime exposure** (local, `rpm` + `systemctl`): a vulnerable daemon package
  whose service units are **all stopped *and* disabled/masked** isn't exposed
  until someone starts it, so the finding is **downgraded to `low`** and
  annotated (it still shows in the full report, and resurfaces at full severity
  if you enable the unit). Conservative by design: packages shipping **no**
  service unit (libraries, CLI tools like `openssl`/`glibc`) are never touched,
  a unit that is enabled, `static` or has a listening socket counts as exposed,
  and an undetermined state keeps full severity. Disable with
  `"service_state_filter": false` in the config.
- **`container`** only inspects **running** containers and flags settings that are
  unambiguously dangerous (`--privileged`, the runtime control socket or sensitive
  host paths bind-mounted, host network/PID/IPC namespaces, dangerous added
  capabilities, disabled seccomp/SELinux). Benign bind mounts are ignored,
  read-only mounts are downgraded a step, and `--privileged` is reported once
  rather than as a flood of per-capability findings.
- **`effective`** catches what a patch on disk leaves running in RAM: a host
  still on an **older kernel** than the one installed (the package scanners
  report it patched the moment the RPM lands, but you keep executing the
  vulnerable kernel until you reboot), and services still mapping a **deleted/
  replaced library** (`libssl` updated but the running process holds the old
  code — restart to load the fix). Pure stdlib via `/proc` + `rpm`; uses
  `needs-restarting -r` (dnf-utils) as the authoritative reboot verdict when
  present. `fix` also **overwrites a package fix's `requires_reboot` with this
  ground truth** after applying, so the "reboot required" note is a fact, not a
  guess. Run it with `--scanner effective` or `--all`.
- **`fips`** audits whether the host's cryptography is actually hardened — the
  tool is FIPS-aware, so it checks its own ground. It flags the classic
  **half-enabled FIPS** trap (kernel in FIPS mode but the system crypto-policy is
  not, so OpenSSL/GnuTLS/OpenSSH still negotiate non-approved algorithms — or the
  reverse), an **inconsistent** state per `fips-mode-setup --check`, a **weakened
  crypto-policy** (`LEGACY`, or a SHA-1-restoring sub-policy — valuable on *any*
  host), and a **pending** policy change (configured ≠ applied). A consistent
  non-FIPS host is a legitimate configuration and produces **no findings**; set
  `"fips_required": true` to treat a non-FIPS host as a finding. Pure stdlib
  (reads `/proc` + the crypto-policies state files). Run it with `--scanner fips`
  or `--all`.
- **`network`** is the only scanner that inspects machines *other than* the one
  it runs on, so it stays genuinely unavailable — not just quiet — until you
  authorize at least one host/CIDR/hostname (IPv4 **and IPv6**) you are
  **permitted to test**: `vulnscan-ai network --add 10.0.0.0/24` (also
  `--remove`/`--list`, and the setup wizard offers it), or set
  `"network_targets": [...]` by hand. There is no per-scan CLI override —
  `scan` never takes a `--target` flag — only the persisted allow-list. It
  shells out to `nmap -sV` for host discovery, a scoped port scan and
  service/version detection (a separate `-6` invocation for any IPv6
  targets), then flags the same plaintext/legacy-protocol and
  sensitive-service exposures as `ports` — the same risk model, observed
  remotely instead of via local `ss` — **plus** a confidence-gated fallback
  that catches a known-risky service fingerprinted on a *non-standard* port
  (only when nmap's `-sV` match is a real confirmed probe, never its
  unconfirmed port-number guess). That fallback only has ports to look at
  when `"network_scan_ports"` is widened past the default `"known"` (fixed
  risky-port list) to `"top1000"`, `"all"`, or a literal `-p` spec —
  `vulnscan-ai network --ports top1000`. Still **no** CVE/version matching
  (parked; too high a false-positive risk without more validation). Findings
  carry the remote host in `target` and are detection-only: fixes must be
  applied on the flagged host itself, so `fix` never proposes or executes
  commands for them. Optional dependency (`Recommends: nmap`); run it with
  `--scanner network` or `--all` (a no-op without configured targets). Note
  `Recommends:` only auto-installs nmap on a fresh `dnf install` — an
  *upgrade* of an already-installed vulnscan-ai does not retroactively pull
  in a newly added weak dependency. `vulnscan-ai info` flags this: if nmap is
  missing it offers to install it now via `dnf` (interactively, TTY only) or
  prints the command to run yourself.
- A **baseline** silences accepted findings: `"ignore": [...]` in the config,
  one-per-line in `~/.config/vulnscan-ai/ignore`, `VULNSCANAI_IGNORE=a,b`, or
  `--ignore PATTERN`. Patterns match a finding id, CVE, advisory, package, or
  title (globs allowed); the scan reports how many it suppressed. During `fix`
  you can also press **`i`** to accept a reviewed finding and add it to the
  baseline on the spot — useful for hardening items you've accepted (e.g. SSH
  password auth on a LAN-only host; `--ignore "SSH*"` accepts the whole class).

### Exploitation-aware prioritisation

Cutting noise is half the job; the other half is surfacing what attackers are
*actually using*. During enrichment each CVE is checked against two public feeds:

- **CISA KEV** — the Known Exploited Vulnerabilities catalog. A match means the
  flaw is being exploited in the wild: the finding is tagged `[KEV]`, sorted to
  the top, and raised to at least `important` so it can't sit below your severity
  floor.
- **EPSS** — FIRST.org's exploit-probability score; a high value shows as
  `[EPSS xx%]`.

The same feeds power the **`news`** command and the dashboard **Advisories** tab
(CISA KEV, NVD, and your distribution's errata), cached locally so they work
offline. Disable the enrichment with `"exploit_enrich": false`; turn the news
tab off with `"news_enabled": false`.

```bash
# Recent advisories, actively-exploited first; [on-host] = matches your last scan
vulnscan-ai news --refresh
```

PDF output always produces a real PDF: it uses `reportlab` if installed,
otherwise a built-in dependency-free PDF writer. Use a `.html` extension to
get an HTML report instead.

### Safe fixes: transactional apply with auto-rollback

When a fix touches a **config file or service** (e.g. an sshd hardening finding),
`fix` applies it transactionally instead of blindly running commands:

1. **Backup** the affected file(s) under `<state-dir>/backups/<id>/`.
2. **Apply** the change — editing files (`sed`) and/or **writing new files** (e.g. a systemd drop-in) structurally, without a shell.
3. **Validate before restart** — e.g. `sshd -t`, `systemd-analyze verify` — so a broken config never reaches a restart.
4. **Reload** the service (preferring `reload` over `restart`) and confirm it stays active.
5. **Auto-rollback** — if *any* step fails, the backup is restored (a created file is removed) and the service brought back, so a bad sshd edit can't lock you out.

Every step is **streamed live** as it runs — backup, each command, the validate
step, the reload/health check (and any rollback) — along with the command's own
output, so you can see exactly what the fix is doing while it does it.

```bash
# AI proposes + applies, with the safety net above
vulnscan-ai fix --scanner ssh --scan

# Undo a fix later from its stored backup
vulnscan-ai rollback --list
vulnscan-ai rollback <finding-id>
```

Don't want to apply on the spot? Generate a reviewable artifact instead — a
self-contained bash script (with the same backup/validate/rollback logic) or an
Ansible playbook:

```bash
vulnscan-ai fix --export-script fix.sh --export-ansible fix.yml
```

### Machine-readable export (ticketing / code scanning)

Output format is chosen by file extension — `.pdf`, `.html`, `.json`, or
`.sarif` (SARIF 2.1.0):

```bash
vulnscan-ai scan --sarif findings.sarif --json findings.json
vulnscan-ai report -o findings.sarif        # from the last saved scan
```

- **SARIF 2.1.0** ingests into GitHub code scanning, DefectDojo, and most
  vuln-management pipelines. Severity maps to SARIF levels
  (critical/important→`error`, moderate→`warning`, low→`note`) and each result
  carries a numeric `security-severity` (CVSS when known) plus CVE/advisory/
  package/fix metadata and a stable `partialFingerprints` id.
- **JSON** is a flat document: tool metadata, a severity summary, and the full
  findings (including any AI remediation).

Typical operator loop: `scan` → review the table → `fix` (approve per-item) →
PDF is attached to your change ticket.

## Scheduled unattended scans (systemd timer)

The `scheduled` subcommand runs non-interactively: it scans, writes a dated
report to the reports directory, rotates old ones, and **never applies fixes**.

```bash
# what the timer runs
vulnscan-ai scheduled --keep 30
# with AI remediation proposals embedded (needs a provider key; no execution)
vulnscan-ai scheduled --plan
# CI/monitoring: non-zero exit when something serious is found
vulnscan-ai scheduled --fail-on important   # exit 3 if any >= important
```

The RPM installs a `vulnscan-ai.timer` (daily, 1h jitter, catch-up). Enable it:

```bash
sudo systemctl enable --now vulnscan-ai.timer
systemctl list-timers vulnscan-ai.timer
journalctl -u vulnscan-ai.service        # last run's output
ls /var/lib/vulnscan-ai/reports/         # generated PDFs
```

Scan-only runs need no API key and send nothing off the host. To enable
`--plan`, put a key in `/etc/vulnscan-ai/vulnscan-ai.env` (mode 0640) and add
`--plan` to `ExecStart` (override with `systemctl edit vulnscan-ai.service`).

### Drift between scans

Every `scan` compares against the previously saved findings and prints what is
**new** and what is **resolved** since the last run, so you can see a host's
posture move over time. `scheduled` reports the same drift counts.

### Email notifications

A scheduled scan can email a plain-text summary when it finds anything at or
above a severity, or anything new since the last scan. Configure it in the
wizard (`vulnscan-ai setup` → *Email notifications*) or in the config:

```json
{
  "notify_email": "ops@example.com",
  "notify_min_severity": "important",
  "smtp_host": "smtp.example.com",
  "smtp_port": 587,
  "smtp_from": "vulnscan-ai@example.com",
  "smtp_user": "vulnscan-ai",
  "smtp_starttls": true
}
```

The SMTP password is read from `VULNSCANAI_SMTP_PASSWORD` (preferred) or
`smtp_password` in the config. Sending never breaks a scan — a failed mail is
logged and the run continues. With no `notify_email` set, nothing is sent.

## Dashboard (HTTPS, login)

`vulnscan-ai dashboard` serves the saved findings — with their explanations,
CVEs and any AI fix plan — over a small HTTPS web UI behind a login. It is
stdlib-only (no extra packages), uses a self-signed certificate generated on
first run, and a single admin account — username **`admin`** by default
(change with `--user`), PBKDF2-SHA256 password hash. On start it prints the
login username, and a firewall hint if the port looks closed in firewalld.

```bash
# 1. Set the admin password (stored hashed; user 'admin' unless --user given)
sudo vulnscan-ai dashboard --set-password

# 2. Run it (foreground), or enable the service
sudo vulnscan-ai dashboard                 # https://<host>:65101/
sudo systemctl enable --now vulnscan-ai-dashboard
```

By default it **binds to localhost only** — reach it with an SSH tunnel
(`ssh -L 65101:localhost:65101 host`). To let specific machines in, add them to
the allow-list (from the CLI or the dashboard itself); the server then also
listens on the network, but **only** the allow-listed clients (and localhost)
are served:

```bash
sudo vulnscan-ai dashboard --allow 10.0.0.0/24 --allow 192.168.1.5
sudo vulnscan-ai dashboard --list            # show user/port/bind/allow-list
```

It refuses to start until a password is set, so findings are never exposed
unauthenticated. Port (`--port`, default 65101) and bind address (`--bind`) are
overridable.

### Scan and fix from the dashboard

- **Summary tiles** at the top show the total and per-severity counts, plus an
  **actively-exploited (CISA KEV)** tile and an **EPSS ≥50%** tile when any
  finding matches — the exploitation signals surface up front, not buried in the
  list.
- **Scan now** (header button) runs every available scanner (like `scan
  --all`) in the background and refreshes the page when done.
- **Preview fix** (per finding) asks the AI for a remediation and shows the
  plan — a dry-run, nothing is executed. Needs an AI provider configured.
- **Apply fix** actually runs the fix on the host (transactional, with
  auto-rollback). It is **off by default** — the dashboard stays read-only
  unless you opt in with `vulnscan-ai dashboard --enable-fix` (or
  `"dashboard_allow_fix": true` in the config, or the interactive menu's *Web
  dashboard → Enable applying fixes* entry). Only then does the Apply button
  appear; `--disable-fix` turns it back off. (Login + the allow-list still gate
  access — and enabling it grants those users root-equivalent remediation power.)
  Every applied fix — from the dashboard or the CLI — is written to an
  append-only audit log (`vulnscan-ai audit`, stored `0600` in the state dir):
  who changed what, when, from where, and how it turned out.

Opening it to the network is two layers: the app allow-list **and** the host
firewall. firewalld blocks the port by default — allow it only for your clients:

```bash
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" \
  source address="192.168.0.0/24" port port="65101" protocol="tcp" accept'
sudo firewall-cmd --reload
```

## Safety model

- The model **only proposes** commands; it never executes anything itself.
- Proposed commands pass through a deny-list (`rm -rf /`, `mkfs`, `curl|sh`,
  `setenforce 0`, `--nodeps`, package removal, crypto-policy downgrade, …)
  before they can run.
- Nothing runs without per-finding approval unless you pass `--yes`.
- `--dry-run` records the full plan in the report without touching the system.
- Fixes that require a reboot are flagged in the output and report.
- Config/service fixes are **transactional**: backup → apply → validate before
  restart → reload → **auto-rollback on failure**, so a bad edit can't strand a
  service. Restore later with `rollback`.

## Layout

```
vulnscanai/
  cli.py            # argparse CLI: info/scan/fix/rollback/report/providers/...
  config.py         # config file + env + flag precedence
  fips.py           # FIPS detection, approved hashing, hardened TLS context
  http.py           # stdlib HTTP over the hardened TLS context
  models.py         # Finding / Remediation dataclasses
  remediation.py    # AI prompt, JSON parse, screening, transactional apply+rollback
  export_fix.py     # render fixes as a bash script or Ansible playbook
  report.py         # block model + reportlab / native-PDF / HTML renderers
  pdfwriter.py      # dependency-free PDF writer (built-in fonts)
  scanners/         # dnf+RHSA, OpenSCAP/OVAL, sshd/systemd/ports hardening, CVE enrich
  ai/               # claude (default), openai, gemini, kimi, deepseek, mistral, local
```

## Disclaimer

Automated remediation changes a live system. Review proposals, prefer
`--dry-run` first, and test on non-production hosts. AI suggestions can be
wrong — the approval gate exists for a reason.

## License

Copyright (C) 2026 techhack. Licensed under the **GNU Affero General Public
License v3.0 or later** ([AGPL-3.0-or-later](LICENSE)). This keeps the project
open: anyone who runs a modified version — including over a network as a service
— must make their source available under the same terms.

The copyright holder retains the right to offer the software under separate
**commercial terms**. Contributions are accepted under the
[Contributor License Agreement](CLA.md) (see [CONTRIBUTING.md](CONTRIBUTING.md)),
which preserves that option. For a commercial license, contact
btdt1983@protonmail.com.


