Vulnerability & patch management
Honey can run patch and vulnerability-management campaigns across your fleet
using the cve-scanner WASM plugin together with CUE recipes.
The workflow mirrors a classic patch pipeline: detect → prioritize → patch →
report.
- Detect — scan every target for known CVEs (via
grypeortrivy). - Prioritize — filter by severity / fix availability; gate on a compliance budget.
- Patch — apply security-only package upgrades with a staged (canary) rollout.
- Report — summarize findings and outcomes (summarize step, notifications).
Requirements
plugins.enabled: truein your config (see Plugins).- The
cve-scannerplugin installed under~/.config/honey/plugins/cve-scanner/. - On each target host:
The plugin shells out on the target via the remote_exec host function, so it
declares only allow_remote_exec — no host-side daemon required.
honey plugins install https://github.com/shareed2k/honey/releases/download/<tag>/cve-scanner.tar.gz
scan action
Runs the scanner on the host and emits a normalized JSON report on stdout,
ready for downstream env_from / loop_from.
plugin: {
id: "cve-scanner"
action: "scan"
config: {
scanner: "auto" // auto | grype | trivy (auto prefers grype)
target: "dir:/" // dir:<path> | image:<ref>
min_severity: "high" // negligible | low | medium | high | critical
only_fixed: true // drop CVEs with no fix available
}
}
| Field | Default | Description |
|---|---|---|
scanner | auto | auto prefers grype, falls back to trivy |
target | dir:/ | dir:<path> (filesystem) or image:<ref> (container image) |
min_severity | none | Drop findings below this level |
only_fixed | false | Keep only CVEs that have a fixed version |
Output:
{
"scanner": "grype",
"target": "dir:/",
"total": 3,
"by_severity": {"critical": 1, "high": 2},
"cves": [
{"id": "CVE-2024-1234", "severity": "critical",
"package": "openssl", "installed": "3.0.2", "fixed": "3.0.13"}
]
}
Consume the report in a later step:
// Pull a single value into an env var:
env_from: [{step: "scan", extract: {CRITICAL: ".by_severity.critical // 0"}}]
// Or fan out one step per affected package:
loop_from: {step: "scan", extract: ".cves[].package"}
scan succeeds regardless of how many CVEs it finds — gating is a recipe
concern (when, failed_when, an exit-code check, or an opa step).
patch action
Detects the package manager and applies upgrades. Without --execute it lists
pending upgrades (dry-run) instead of applying them.
plugin: {
id: "cve-scanner"
action: "patch"
config: {
manager: "auto" // auto | apt | dnf | yum | apk | zypper
security_only: true // security updates only where supported
// packages: ["openssl", "libc6"] // pin to specific packages
}
}
| Field | Default | Description |
|---|---|---|
manager | auto | Force a package manager, or auto-detect |
security_only | true | Limit to security updates where the manager supports it |
packages | all | Upgrade only these packages instead of a full upgrade |
patch returns changed, exit_code, and stdout/stderr per host. It uses
sudo -n when not running as root.
Campaign walkthrough
examples/recipe/patch_campaign.cue
runs a full campaign as a graph recipe:
recipe: {
name: "patch-campaign"
type: "graph"
defaults: {max_parallel: 5} // canary -> staged rollout
// schedules: [{cron: "0 3 * * 6", timezone: "UTC"}] // weekly campaign
steps: [
{id: "scan", host: "*", plugin: {id: "cve-scanner", action: "scan",
config: {scanner: "auto", target: "dir:/", min_severity: "high", only_fixed: true}}},
{id: "gate", host: "*", depends: ["scan"],
env_from: [{step: "scan", extract: {CRITICAL: ".by_severity.critical // 0"}}],
command: "test \"${CRITICAL:-0}\" -eq 0 || { echo \"critical budget exceeded: ${CRITICAL}\" >&2; exit 1; }",
ignore_errors: true},
{id: "patch", host: "*", depends: ["scan"], plugin: {id: "cve-scanner", action: "patch",
config: {manager: "auto", security_only: true}}, retry: {attempts: 2, delay_ms: 5000, backoff: "exponential"}},
{id: "report", host: "_", depends: ["patch"], summarize: {prompt: "Summarize CVEs per host and any failed patches."}},
]
}
max_parallel: 5caps host concurrency — a canary/staged rollout.- The gate step pulls the critical count from the scan report and fails the
host (nonzero exit) when the budget is exceeded;
ignore_errors: truesurfaces it in the report without aborting the whole campaign. retrywith exponential backoff tolerates transient mirror/lock errors.schedulesturns the recipe into a recurring campaign (cron).
Run it:
honey cue-validate examples/recipe/patch_campaign.cue
honey cue-exec examples/recipe/patch_campaign.cue "<filter>" # dry-run plan
honey cue-exec --execute examples/recipe/patch_campaign.cue "<filter>" # apply
Read-only audit
When you only want to detect and prioritize (no changes), use
examples/recipe/cve_scan_audit.cue:
scan every host then a summarize audit (top CVEs by severity, systems impacted per
CVE, fix availability). Safe to run anywhere.
Security notes
scan/patchrun on the target over SSH (remote_exec), so scanner output is not subject to the smallhost_execoutput cap.- Review
plugin.yamlbefore installing —cve-scannerdeclares onlyallow_remote_exec. Restrict loadable plugins withplugins.allowlist. patchrequires passwordlesssudoor root on targets; without it the step fails cleanly and is reported.
See also
- Plugins — install, enable, allowlist.
- Plugin development — the step schema and writing your own.
- CUE recipes — graph mode,
env_from/loop_from,when, scheduling.