When a pending baseline change shows up in baseline review,
package ownership answers "does this look like an install." Change-source correlation
answers the other question: was there a change ticket, maintenance window, or approved
deploy that explains it? It queries whichever external ticket systems you've configured and
shows every match found — purely informational, never a gate. A pending change is still
only ever accepted via an explicit, signed baseline approve.
Request Tracker ships as the first reference source, under the same
Etminan Plugin API notification channels use.
verifier/src/change_source.rs opens, verifies, and executes a
separately-vetted script — it never implements a ticket system's API in Rust itself, and
it never executes anything not on an explicit, operator-maintained allowlist. Same
security model, same reason, as notification channels: this project has no in-process
HTTP client at all.
environment variables are the master switches: ETMINAN_CHANGE_SOURCES
(which sources run) and ETMINAN_CHANGE_SOURCE_PLUGINS_CONF
(where the allowlist lives). Both unset means the feature never executes anything —
off by default, purely additive when enabled.
Builds a TicketSQL search over a time window centered on the earliest
uncorrelated pending measurement's first-seen time, widened
±ETMINAN_RT_WINDOW_HOURS (default 24h) on both sides —
matching either a custom field or a plain Subject search
against the host id — and returns every matching ticket as JSON.
Needs: curl, jq, a date(1)
supporting GNU's -d
The plugin sends ETMINAN_RT_API_TOKEN in a plain
Authorization header on every request, and refuses to run
at all against an http:// base URL rather than send that
token in cleartext — a hard refusal (exit 2), not a warning.
Add it as one line in
/etc/etminan-verifier/change-source-plugins.conf (copy
from deploy/change-source-plugins.conf.example):
A stale or wrong hash means the verifier refuses to run the plugin at all — a clear error, never a silent bypass. Recompute it every time you touch the file.
In /etc/etminan-verifier/verifier.env — these are read
by the plugin, not the verifier binary:
ETMINAN_CHANGE_SOURCES is the master switch — unset or
empty, no plugin ever executes. Everything else only matters once a source's name is
both listed here and allowlisted in the previous step. Restart is never needed
for a config-only change: etminan-verifier is invoked
fresh per check/run, not a
long-lived daemon reading env vars once at startup.
RT's TicketSQL date-literal format and your instance's actual custom field name are the two things this integration cannot know in advance — confirm them once before relying on it:
Paste that into RT's own Search → Tickets → Advanced and confirm it returns what you expect — a timezone mismatch or a wrong custom-field name shows up here, not as a silent zero-match during a real review. Then:
Unlike notify plugins (JSON on stdin, since finding text is free-form), a change-source plugin's whole job is querying an external system using the host and time it's given — a couple of scalar values as flags is natural, and lets a plugin be tested by hand trivially.
[
{
"id": "RT#4821",
"summary": "Patch Tuesday rollout — web fleet",
"status": "resolved",
"url": "https://rt.example.com/Ticket/Display.html?id=4821"
}
]
| Field | What it is |
|---|---|
| id | Whatever identifier makes sense for the source (RT ticket number
prefixed RT#, a ServiceNow change number, etc.) — free-form,
just needs to mean something to the operator reading it. |
| summary | Short human-readable description of the ticket/change. |
| status | The ticket's own status string (resolved,
approved, ...) — passed through as-is, not interpreted. |
| url | Optional deep link to the ticket. Currently stored end to end but
not yet rendered anywhere in baseline review's
output — send it anyway; it's wired up on the storage side already, just not surfaced to
the operator yet. |
Leave source out of the plugin's own output entirely — the
verifier sets it from the plugin's allowlisted name, never from anything the plugin prints,
so a plugin can't spoof which source a match came from. Exit non-zero on any real failure;
don't print partial or best-guess JSON.
If nothing matched, or correlation hasn't run yet for this host, one of these appears instead of the match line:
If more than one configured source (or, in principle, more than one RT ticket) matches, every one is listed:
None of this changes what baseline approve/reject do — it's context for the operator making that call, never an
input to it.
ServiceNow, Jira, an internal change-management tool — any system not shipped is the same contract as Request Tracker, and needs no verifier rebuild.
Accept --host <id> --anchor <RFC3339 timestamp>,
read whatever the source needs from its own environment (pick a distinct
ETMINAN_<X>_* prefix so it doesn't collide with
another configured source), print the JSON array contract above on stdout, exit
non-zero on any real failure.
Same two steps as Request Tracker: install to
/etc/etminan-verifier/change-sources/, add its path and
sha256sum to
change-source-plugins.conf, add its name to
ETMINAN_CHANGE_SOURCES (comma-separated — every configured
source runs, and every match any of them finds is kept, not just the first).
plugins verifyConfirms the allowlist entry and pinned hash both check out. Nothing else changes —
ChangeRecord, the storage, the perform_check wiring, and baseline
review's rendering are all already source-agnostic.
Full security model (why root-owned, why content-hash pinned, what's re-verified on every single run) lives in the Etminan Plugin API — shared with notification channels, since both categories are the same underlying mechanism.