Skip to main content

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 grype or trivy).
  • 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: true in your config (see Plugins).
  • The cve-scanner plugin installed under ~/.config/honey/plugins/cve-scanner/.
  • On each target host:
    • A scanner for the scan action: grype or trivy.
    • A package manager for the patch action: apt, dnf/yum, apk, or zypper (passwordless sudo or root).

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
}
}
FieldDefaultDescription
scannerautoauto prefers grype, falls back to trivy
targetdir:/dir:<path> (filesystem) or image:<ref> (container image)
min_severitynoneDrop findings below this level
only_fixedfalseKeep 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
}
}
FieldDefaultDescription
managerautoForce a package manager, or auto-detect
security_onlytrueLimit to security updates where the manager supports it
packagesallUpgrade 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: 5 caps 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: true surfaces it in the report without aborting the whole campaign.
  • retry with exponential backoff tolerates transient mirror/lock errors.
  • schedules turns 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/patch run on the target over SSH (remote_exec), so scanner output is not subject to the small host_exec output cap.
  • Review plugin.yaml before installing — cve-scanner declares only allow_remote_exec. Restrict loadable plugins with plugins.allowlist.
  • patch requires passwordless sudo or root on targets; without it the step fails cleanly and is reported.

See also