
MCP server for CVE/config remediation orchestration: SSH inventory, OSV/NVD lookups, CIS/DISA STIG-mapped Ansible playbook suggestions.
See OVERVIEW.md for a high-level description of the
repository's purpose, components, and scope before making behavioral
changes.
A remediation orchestrator, exposed as an MCP server. Connect an MCP client (Claude Desktop, Claude Code, or similar) and submit a natural-language request, for example:
Log into host a.b.c, check installed packages, find CVEs, and suggest a fix - Ansible if possible, and tell me what compliance controls it maps to.
prescryb supplies the primitives (SSH inventory, CVE matching, live advisory
lookups, compliance-topic mapping, Ansible playbook rendering). The
connected model does the reasoning: which findings matter, which CVEs to
dig into, which playbook to generate. prescryb never applies anything to the
target host - every tool is read-only against it, or pure text/data
generation.
| Tool | What it does |
|---|---|
inventory_host(host, user="", port=22, hostname="", identity_file="", trust_unknown_host=False) | SSH in, detect the distro, list installed packages with versions. |
check_cves(system, packages) | Batch-match package versions against OSV.dev using its ecosystem-aware version comparison (not name-only matching). Each match is enriched with an EPSS exploitation-probability score. |
fetch_advisory(cve_id) | Fetch the current NVD record for one CVE - description, CVSS, CWE, references - live, not from training data. |
fetch_epss(cve_ids) | Batch-fetch EPSS exploitation-probability scores for CVE IDs not already covered by check_cves (e.g. from fetch_advisory or a web search). |
map_compliance(area) | Map a free-text topic ("ssh", "sudo", "kernel modules", ...) to CIS/DISA STIG topic areas and, if present in the konstruktoid.hardening GitHub repo, the matching role - plus the MITRE ATT&CK techniques and mitigations that area addresses. |
lookup_cce(target, keyword, cce_id) | Look up NIST CCE (Common Configuration Enumeration) entries for a platform (e.g. "rhel8"), sourced from the community JSON conversion at konstruktoid/cce-web. |
list_cce_targets() | List every platform lookup_cce can query. |
list_local_docs() | List documents (Markdown/text/PDF) found under the local, gitignored LOCAL_DOCS_DIR. |
search_local_docs(query, top_k=5) | RAG-style semantic search over those local documents, embedded entirely on-machine. |
generate_playbook(system, cve_matches, compliance_areas, hosts_alias) | Render a suggest-only Ansible playbook: CVE fixes become package-upgrade tasks, compliance areas become roles: references. |
Typical flow: inventory_host, then check_cves on the returned packages,
then optionally fetch_advisory on interesting CVEs, then map_compliance
(and lookup_cce) for any insecure-config areas noticed, then optionally
search_local_docs for relevant internal runbook/policy context, then
generate_playbook to produce something to review.
flowchart TD
U["Operator: natural-language request"] --> M["Connected model\n(Claude Desktop / Claude Code)"]
M --> A["inventory_host\nSSH in, list packages"]
A --> B["check_cves\nOSV.dev match + EPSS score"]
B --> C{"Interesting\nCVE?"}
C -->|yes| D["fetch_advisory\nNVD detail for one CVE"]
C -->|no| E
D --> E["map_compliance / lookup_cce\nCIS/DISA STIG + ATT&CK mapping"]
E --> F["search_local_docs\ninternal runbooks (optional)"]
F --> G["generate_playbook\nsuggest-only Ansible playbook"]
G --> H["Model reasons over the results\nand presents findings + playbook"]
H --> R["Operator reviews\n(ansible-playbook --check --diff)\nand applies manually"]
style R fill:#f9f,stroke:#333,stroke-width:1px
Only the tool boxes are prescryb: inventory_host, check_cves,
fetch_advisory, map_compliance/lookup_cce, search_local_docs, and
generate_playbook - each a read-only lookup or pure text/data generation
call. The operator and connected model are not part of prescryb, and
nothing in this chain touches the target host beyond inventory_host's
read-only SSH session - applying the generated playbook is a deliberate,
separate step the operator takes outside prescryb.
uv sync
Claude Code:
claude mcp add prescryb -- uv --directory /path/to/prescryb run prescryb
Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"prescryb": {
"command": "uv",
"args": ["--directory", "/path/to/prescryb", "run", "prescryb"]
}
}
}
inventory_host never accepts a password argument. MCP tool-call arguments
can be logged by clients and are visible to the connected model, so
credentials must never flow through them. Auth works exactly like running
ssh host yourself:
~/.ssh/config.inventory_host's hostname/identity_file arguments override the
resolved address/key path directly, for hosts you do not want to add to
~/.ssh/config (only a path is passed, never key contents).trust_unknown_host=True
ssh host manually once to pin the key instead.qemu/vm.sh boots a cloud image (almalinux10, trixie, resolute) under
QEMU with a cloud-init seed and an SSH port forward on 127.0.0.1
(2201, 2202, 2203). It needs qemu-system-x86_64, qemu-img, cloud-localds
(cloud-image-utils), curl and ssh; KVM is used when /dev/kvm is writable.
Images are checksum-verified and cached in qemu/.state/.
qemu/vm.sh up trixie
qemu/vm.sh ssh trixie true # retry until the guest answers
claude 'inventory 127.0.0.1 port 2202 user prescryb with identity file
qemu/.state/id_ed25519 (trust_unknown_host is fine, it is a throwaway VM),
check any vulnerabilities and suggest a fix, include compliance mapping if
possible, write the playbook suggestion to /tmp/ and print the file location'
qemu/vm.sh destroy trixie
For a host already reachable via ssh (resolved through ~/.ssh/config,
an agent key, or the default identity file), specify the hostname
directly; no port or identity-file configuration is required:
Inventory prod-web-01, check installed packages, find CVEs, and suggest a fix - Ansible if possible, and tell me what compliance controls it maps to.
If the host is not in ~/.ssh/config yet, either add a Host block or pass
user/port/hostname/identity_file straight to inventory_host for a
one-off connection - same as the molecule example below.
molecule converge -s default
Find the ssh_port/ssh_user from that scenario's molecule.yml platform
entry (e.g. ssh_port: 22201, ssh_user: almalinux for the almalinux10
platform) and the private key molecule login uses to connect - either add
a Host block to ~/.ssh/config, or skip the file entirely and pass them
straight to inventory_host for a one-off, ephemeral connection:
Inventory 127.0.0.1, port 22201, user almalinux, identity_file /path/to/molecule's/generated/key, check installed packages, find CVEs, and suggest a fix - Ansible if possible, and tell me what compliance controls it maps to.
Run molecule destroy -s default when finished; prescryb will not do it
for you, and will not touch the instance beyond reading it.