Skip to content

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 Host CF on your change-request queue). Without one, correlation falls back to a Subject text 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:

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

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:

ETMINAN_RT_DEBUG_QUERY=1 etminan-verifier check --host web-01 --addr 10.0.0.5:7620

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:

etminan-verifier plugins verify

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:

./request-tracker.sh --host web-01 --anchor 2026-07-21T00:00:00Z

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.
     Change-source correlation not yet checked for this host — re-run `check`/`run` for this host.

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:

  1. Invocation<plugin> --host <host_id> --anchor <RFC3339 timestamp>.
  2. Config — read whatever it needs from its own environment (pick a distinct ETMINAN_<X>_* prefix so it doesn't collide with another source).
  3. Output — a single JSON array on stdout on success ([] for "nothing found"); exit non-zero on failure, never partial JSON. Leave source out.

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.