Change-source correlation¶
Asking your change-management system, during baseline review, whether a pending change was actually planned — worked end to end with Request Tracker and FreeITSM.
When a pending baseline change appears in baseline review, an operator has two questions: does this look like a package install? (the built-in package-ownership check answers that) and was this actually planned? — was there a change ticket, maintenance window, or approved deploy behind it? Change-source correlation answers the second automatically, by querying whichever external systems you have configured and showing every match found.
Correlation is informational — never a gate
A match is context for the operator, never an input to the decision. A pending
change is still only ever accepted via an explicit, signed
baseline approve. Nothing here auto-approves,
auto-rejects, or weakens the baseline workflow's "no silent auto-accept" rule.
Like notification channels, every source is an external plugin process under the shared Etminan Plugin API: verifier/src/change_source.rs opens, verifies, and executes a separately-vetted script — it never implements any ticket system's API in Rust, and this project has no in-process HTTP client at all. Adding a source needs no verifier rebuild.
Shipped reference sources¶
| Source | Script | Needs | Matches on |
|---|---|---|---|
| Request Tracker (RT 5.x) | deploy/change-sources/request-tracker.sh |
curl, jq, a date(1) supporting GNU -d |
A custom field (CF.{...}) or a Subject text search |
| FreeITSM | deploy/change-sources/freeitsm.sh |
curl, jq |
A change's title only |
RT is the primary worked example below; FreeITSM follows the same shape with a different env prefix.
Prerequisites (Request Tracker)¶
- An RT 5.x instance reachable over HTTPS from the host running
etminan-verifier. - An RT API token (Admin → Tools → API Tokens, or ask your RT administrator). It needs read access to search tickets — nothing more.
- Optional: a custom field your change process already populates with the affected hostname (e.g. a
HostCF on your change-request queue). Without one, correlation falls back to aSubjecttext search for the host id — workable but coarser.
Configuring Request Tracker¶
Same four-step shape as any notify channel. No restart is ever needed — etminan-verifier runs fresh per check/run.
1. 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
2. Allowlist it, with its exact content hash¶
Copy deploy/change-source-plugins.conf.example to /etc/etminan-verifier/change-source-plugins.conf and add a name path sha256 line:
Recompute the hash every time you touch the plugin file
A stale or wrong hash means the verifier refuses to run the plugin — a clear
error, never a silent skip. The pin is re-checked fresh before every single
execution. Override the allowlist path with
ETMINAN_CHANGE_SOURCE_PLUGINS_CONF if needed.
3. Configure RT access¶
In /etc/etminan-verifier/verifier.env — 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...
ETMINAN_RT_HOST_FIELD=Host # optional — omit to fall back to Subject-only search
ETMINAN_RT_WINDOW_HOURS=24 # default shown; widen for maintenance-weekend changes
ETMINAN_CHANGE_SOURCES is the master switch: unset or empty, this feature never executes any plugin. Every other setting only matters once request-tracker is both listed here and allowlisted in step 2.
HTTPS is enforced — the token is never sent in cleartext
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
(a hard exit, not a warning) rather than leak that token.
4. 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:
This prints the constructed TicketSQL to stderr:
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:
How correlation runs during a check¶
The verifier queries every configured, allowlisted source and aggregates whatever each finds — several systems might each confirm a change, and none should be silently discarded in favour of a first match. Each plugin's own failure (missing, ownership violation, hash mismatch, timeout, non-zero exit, malformed output) is caught and logged so one broken source never blocks another, and correlation never fails the check cycle.
sequenceDiagram
participant Run as run/check cycle
participant CS as change_source::correlate
participant P1 as request-tracker.sh
participant P2 as freeitsm.sh
participant DB as baseline DB (ChangeRecord)
participant Rev as baseline review
Run->>CS: host_id + anchor (earliest uncorrelated first_seen)
CS->>CS: load allowlist, verify each pinned hash
CS->>P1: --host web-01 --anchor 2026-07-21T00:00:00Z
P1-->>CS: [ {id,summary,status,url} ] (exit 0)
CS->>P2: --host web-01 --anchor 2026-07-21T00:00:00Z
P2-->>CS: [] (checked, nothing found)
Note over CS: source set from allowlist name,<br/>never from plugin output
CS->>DB: store aggregated matches
Rev->>DB: read correlations
Rev-->>Run: one extra line under each pending entry
The plugin contract¶
Unlike notify plugins (JSON on stdin), a change-source plugin takes a couple of scalar values as flags — its whole job is a query using the host and time it's given, so argv is natural and lets you test it by hand:
The --anchor is an RFC 3339 timestamp: the earliest uncorrelated pending measurement's first-seen time, which the plugin widens by ±ETMINAN_RT_WINDOW_HOURS. On success the plugin prints a single JSON array on stdout ([] for "checked, nothing found" — a real, final answer, not "not checked"):
[
{
"id": "RT#4821",
"summary": "Patch Tuesday rollout — web fleet",
"status": "resolved",
"url": "https://rt.example.com/Ticket/Display.html?id=4821"
}
]
| Field | Meaning |
|---|---|
id |
Whatever identifier means something to the operator (RT ticket number prefixed RT#, a ServiceNow change number, …). Sanitized to 128 chars. |
summary |
Short human-readable description. Sanitized to 500 chars. |
status |
The ticket's own status string, passed through as-is, not interpreted. Sanitized to 64 chars. |
url |
Optional deep link. Stored end to end (ChangeRecord), validated, but not yet rendered in baseline review output — send it anyway. |
The plugin cannot spoof its own source name
Leave source out of the plugin's output entirely. The verifier sets it from
the plugin's allowlisted name, never from anything the plugin prints — so a
compromised or misbehaving plugin can't misattribute a match to a different
configured source. Exit non-zero on any real failure; never print
partial/best-guess JSON.
What it looks like in baseline review¶
Once configured, a second line appears under each pending entry's existing package-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:
No approved change ticket found near this host for this time window across any configured system — verify this wasn't unplanned.
If more than one source (or more than one 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.
A second source: FreeITSM¶
FreeITSM (source) is a real, free/open-source ITSM tool, shipped as the second reference plugin (deploy/change-sources/freeitsm.sh) — built directly against its actual REST API v1 (the /changes filters and response shape, its {"data":...} / {"error":...} envelope, and the UI's own change-detail deep-link pattern), not a guessed generic ITSM shape.
Allowlist it exactly like RT (steps 1–2, substituting freeitsm.sh / freeitsm), then in verifier.env:
ETMINAN_CHANGE_SOURCES=freeitsm # or "request-tracker,freeitsm" for both
ETMINAN_FREEITSM_BASE_URL=https://itsm.example.com
ETMINAN_FREEITSM_API_TOKEN=fitsm_abcdef0123456789...
ETMINAN_FREEITSM_COMPANY_ID=1 # optional, multi-tenant installs only
ETMINAN_FREEITSM_WINDOW_HOURS=24 # default shown
The API key (System → API) needs at least the changes.read permission and is sent as Authorization: Bearer fitsm_.... Correlation matches on a change's title only — FreeITSM's search doesn't expose a custom-field match the way RT's CF.{Name} does — so name changes to include the affected host id, e.g. web-01 kernel patch rollout.
Known API limitation, disclosed in the plugin header
GET /changes offers only a lower-bound modified_since filter, no matching
upper bound, so the window's far edge is applied client-side against the newest
100 matches. Not yet verified against a live FreeITSM instance — confirm
the exact permission set and modified_since semantics before trusting it in
production. ETMINAN_FREEITSM_DEBUG_QUERY=1 prints the constructed request to
stderr, same as ETMINAN_RT_DEBUG_QUERY.
Security model¶
Change-source correlation is the change-source category of the shared Etminan Plugin API. The verification steps — root-owned, not group/world-writable, SHA-256-pinned, re-checked fresh before every execution, bounded timeout, never a directory scan — are documented once there and shared by every plugin category. Run etminan-verifier plugins verify to check your configured sources on demand.
Change-source correlation is read-only inbound — it only queries your change-management systems for context and never writes back to them.
Adding a second source¶
No Rust code, no verifier rebuild — write a script satisfying the change-source contract:
- Invocation —
<plugin> --host <host_id> --anchor <RFC3339 timestamp>. - Config — read whatever it needs from its own environment (pick a distinct
ETMINAN_<X>_*prefix so it doesn't collide with another source). - Output — a single JSON array on stdout on success (
[]for "nothing found"); exit non-zero on failure, never partial JSON. Leavesourceout.
Then allowlist it (name, path, pinned sha256) in change-source-plugins.conf, add its name to ETMINAN_CHANGE_SOURCES (comma-separated — every configured source runs, every match is kept), and run plugins verify. ChangeRecord, the storage, the perform_check wiring, and the baseline review rendering are all already source-agnostic — nothing else changes.
Related¶
- Plugin API — the shared verify-then-exec security model.
- Notification channels — the other plugin category, same mechanism.
- Baseline review — where these correlations surface.