# Change-source correlation — worked example (Request Tracker)

This walks through configuring the verifier's change-source correlation
feature end to end, using Request Tracker (RT) as the concrete example.
For the architecture and design rationale, see `docs/design.md`'s "More
operator context" section and its Deferred notes. For the short version
aimed at operators, see `docs/user-guide.md`'s "Extending: correlating
against another external system".

## What this is

When a pending baseline change shows up in `baseline review`, an operator
has two questions: *does this look like a package install* (the existing
package-ownership check) and *was this actually planned* — was there a
change ticket, maintenance window, or approved deploy that explains it?
Change-source correlation answers the second question automatically, by
querying whichever external systems you've configured and showing every
match found — purely informational, never a gate. A pending change is
still only ever accepted via an explicit, signed `baseline approve`.

Request Tracker is the first supported source, chosen as a concrete,
common example. Every source — RT included — is an **external plugin
process** (any language), never in-process Rust: `verifier/src/
change_source.rs` opens, verifies, and executes a separately-vetted
executable, it doesn't implement any ticket-system's API itself. This is
the `change-source` category of the shared
**[Etminan Plugin API](plugins.md)** — see that page for the full,
category-agnostic spec (also used by notification channels, see
[`docs/notifications.md`](notifications.md)). See "Security model" below
for a pointer to exactly what's verified before a plugin ever runs, and
"Adding a second source" for how little is needed to add one.

## Prerequisites

- An RT 5.x instance reachable from wherever the plugin runs (the same
  host as `etminan-verifier`) over HTTPS.
- An RT API token: **Admin → Tools → API Tokens** in RT's web UI, or ask
  your RT administrator. The token needs read access to search tickets;
  it does not need to create or modify anything.
- Optional: a custom field your change process already populates with
  the affected hostname (e.g. a `Host` CF on your change-request queue).
  If you don't have one, correlation falls back to a plain `Subject`
  text search for the host_id — workable, but coarser.
- The shipped reference plugin (`deploy/change-sources/request-tracker.sh`)
  needs `curl`, `jq`, and a `date(1)` supporting GNU's `-d` on the host
  running `etminan-verifier`.

## Configuration

**1. Install the plugin script** somewhere root-owned and not
group/world-writable — e.g.:

```bash
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:

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

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

**Recompute and update that hash every time you touch the plugin file** —
a stale/wrong hash means the verifier refuses to run it at all (a clear
error, not a silent skip), never a security bypass.

**3. Configure RT access**, in `/etc/etminan-verifier/verifier.env` (see
`deploy/verifier.env.example` for the shipped, commented-out template).
These are read by the *plugin*, not the verifier binary — see the module
doc comment in `change_source.rs` for why sources are external processes:

```bash
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 at all. Leaving it unset is always
safe — every other setting below only matters once `request-tracker` is
both listed here *and* allowlisted in step 2.

`ETMINAN_RT_BASE_URL` must be `https://` — 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.

Restart is not required for a config-only change since
`etminan-verifier` is invoked fresh per `check`/`run` (it's not a
long-lived daemon reading env vars once at startup) — the next scheduled
`run` (or a manual `check`) picks up the new config.

## Security model

Change-source correlation is the `change-source` category of the shared
**[Etminan Plugin API](plugins.md)** — the verification steps (root-owned,
not group/world-writable, SHA256-pinned, re-checked fresh before every
single execution, bounded timeout, never a directory scan) are documented
once there and shared by every plugin category, not re-explained per
category here. Run `etminan-verifier plugins verify` to check your
configured sources on demand — see `docs/plugins.md`'s "Verifying your
configuration" section.

## Verifying the query before trusting it

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 the automated correlation:

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

This prints the constructed TicketSQL to stderr, e.g.:

```
etminan-verifier: RT 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 web search UI (**Search → Tickets → Advanced**,
or the query box directly) and confirm it returns what you expect. If RT
interprets the timestamps in a different timezone than you expected, or
your custom field name doesn't match, this is where you'll see it —
before it silently returns zero matches during a real review.

## 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, you'll
see one of:

```
     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, not an input to it.

## Adding a second source

No Rust code, no verifier rebuild — write a new script satisfying the
`change-source` category's contract (see
[`docs/plugins.md`](plugins.md#writing-a-new-plugin) for the full
walkthrough, shared with every other plugin category):

- **Invocation**: `<plugin> --host <host_id> --anchor <RFC3339 timestamp>`.
- **Config**: read whatever it needs from its own environment (inherited
  from `verifier.env`) — pick a distinct `ETMINAN_<X>_*` prefix so it
  doesn't collide with another configured source.
- **Output on success**: a single JSON array on stdout, `[]` for "checked,
  nothing found" — `[{"id": "...", "summary": "...", "status": "...",
  "url": "..." or null}, ...]`. Leave out `source` entirely; the verifier
  sets it from the plugin's own allowlist name, never from plugin output.
- **On failure**: exit non-zero. Don't print partial/best-guess JSON.

Then allowlist it (name, path, pinned sha256) in
`change-source-plugins.conf`, add its name to `ETMINAN_CHANGE_SOURCES`,
and run `etminan-verifier plugins verify` to confirm it checks out.
`ChangeRecord`, the storage, the `perform_check` wiring, and the
`baseline review` rendering are all already source-agnostic — nothing
else changes.
