etminan
Change-source correlation

Was this change actually planned?

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.

01 — What this is

An external process, never an in-process API client.

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.

2

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.

02 — Shipped source

Request Tracker, chosen as a concrete first example.

deploy/change-sources/request-tracker.sh

What it does

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

Security

HTTPS-only, enforced

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.

03 — Configuring Request Tracker

Four steps, same shape as any notify channel.

Install the plugin script, root-owned

$ sudo install -D -m 0755 -o root -g root \
deploy/change-sources/request-tracker.sh \
/etc/etminan-verifier/change-sources/request-tracker.sh

Allowlist it with its exact content hash

$ sha256sum /etc/etminan-verifier/change-sources/request-tracker.sh

Add it as one line in /etc/etminan-verifier/change-source-plugins.conf (copy from deploy/change-source-plugins.conf.example):

request-tracker /etc/etminan-verifier/change-sources/request-tracker.sh <sha256sum output>

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.

Configure RT access

In /etc/etminan-verifier/verifier.env — these are read by the plugin, not the verifier binary:

ETMINAN_CHANGE_SOURCES=request-tracker
ETMINAN_RT_BASE_URL=https://rt.example.com
ETMINAN_RT_API_TOKEN=1-24-abcdef0123456789...
# optional — omit to fall back to Subject-only search
ETMINAN_RT_HOST_FIELD=Host
# default shown; widen for maintenance-weekend changes
ETMINAN_RT_WINDOW_HOURS=24

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.

Verify the query, then verify the plugin

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:

$ ETMINAN_RT_DEBUG_QUERY=1 etminan-verifier check --host web-01 --addr 10.0.0.5:7620
request-tracker plugin: TicketSQL query for host 'web-01': (CF.{Host} = 'web-01'
OR Subject LIKE 'web-01') AND (LastUpdated > '2026-07-20 14:00:00' AND
LastUpdated < '2026-07-22 14:00:00')

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:

$ etminan-verifier plugins verify
04 — What a plugin receives and returns

Structured argv in, a JSON array out.

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.

$ ./request-tracker.sh --host web-01 --anchor 2026-07-21T00:00:00Z
stdout on success (empty array = "checked, nothing found")
[
  {
    "id": "RT#4821",
    "summary": "Patch Tuesday rollout — web fleet",
    "status": "resolved",
    "url": "https://rt.example.com/Ticket/Display.html?id=4821"
  }
]
FieldWhat it is
idWhatever 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.
summaryShort human-readable description of the ticket/change.
statusThe ticket's own status string (resolved, approved, ...) — passed through as-is, not interpreted.
urlOptional 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.

05 — What it looks like in baseline review

One extra line, right under the ownership verdict.

[42] host=web-01 path=/usr/local/bin/fds-proxy
Likely a legitimate install — matches package fds-proxy v2.3.1. Confirm
this deploy was expected before approving.
Matches change RT#4821 (resolved) via request-tracker: 'Patch Tuesday
rollout — web fleet' — confirm this change was expected.
detail: sha256=... (was ...) owner=uid:0/gid:0 mode=0755 size=... mtime=...
first_seen=...

If nothing matched, or correlation hasn't run yet for this host, one of these appears instead of the match line:

No approved change ticket found near this host for this time window
across any configured system — verify this wasn't unplanned.
Change-source correlation not yet checked for this host — re-run
`check`/`run` for this host.

If more than one configured source (or, in principle, more than one RT ticket) matches, every one is listed:

Matches 2 possible confirmations across configured systems: RT#4821
(resolved) via request-tracker; CHG0093 (approved) via servicenow

None of this changes what baseline approve/reject do — it's context for the operator making that call, never an input to it.

06 — Adding a second source

A shell script and a hash — same recipe as any plugin category.

ServiceNow, Jira, an internal change-management tool — any system not shipped is the same contract as Request Tracker, and needs no verifier rebuild.

Write the script

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.

Install it, root-owned, allowlist it, name it

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).

Run plugins verify

Confirms 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.