# Alerts

The events the monitor records, what their timestamps mean, and how to poll for them.

An alert is an event Feint's monitor recorded about one of your canaries. The one that matters most is the first use of a decoy; the other three say the decoy itself has changed underneath you.

## Kinds

| `kind` | Meaning |
|---|---|
| `canary.used` | The token was used at least once since Feint last looked. This is the event the product exists to produce. |
| `canary.revoked_externally` | The credential was revoked or disabled at the provider by something other than Feint: an operator cleaning up, or someone burning the credential. |
| `canary.missing` | The provider has no record of the credential at all. |
| `canary.expired` | The canary passed its `expires_at`. The credential no longer works and the decoy no longer detects anything. Raised once, within minutes of the expiry. |

Your team is emailed for all four, and each is POSTed to any [webhook endpoints](https://feintsecurity.com/docs/webhooks) you register.

## Fields

| Field | Meaning |
|---|---|
| `id` | The event's id. De-duplicate on it. |
| `canary_token_id` | The canary it concerns. |
| `kind` | One of the kinds above. |
| `occurred_at` | For `canary.used`, the provider's own timestamp: GitLab's throttled last-used time, or the first CloudTrail record of the window for an AWS decoy. For `canary.expired`, the canary's `expires_at`. For the other two kinds, when Feint's monitor noticed the change. |
| `detected_at` | When Feint's monitor saw it. |
| `source_ips` | Addresses the provider reported for the use. May be empty for GitLab at first; complete for an AWS decoy. |
| `detail` | Context for the notification: for `canary.used`, the placement, the provider and the credential's id at the provider (`external_id`: the access key ID for an AWS decoy). For an AWS decoy also CloudTrail's summary of the window: `count` (exact), `calls` (one entry per call, in order, up to the first 50), `first_seen_at`, `last_seen_at`, `records_truncated` and, when AWS found the key in public and quarantined it, `aws_quarantined_at`. |
| `created_at` | When the event was written. |
| `records_url` | Where to fetch the provider's records behind the event: for an AWS decoy, every CloudTrail record of the window, via `GET /v1/alerts/{id}/records` with `alerts:read`. Empty for GitLab, which keeps none. |

## What a `canary.used` event means

**A GitLab token.** GitLab records a token's last use at most once every ten minutes or so, and Feint polls every few minutes. So:

- One event means the canary was used **at least once** in a window of roughly ten minutes ending at `occurred_at`. Several uses inside one window look like one. Do not present the number of events as the number of uses.
- Expect `detected_at` to trail the use by up to a quarter of an hour.
- `source_ips` is what GitLab attributes to the token, on its own schedule. A use may show an empty list at first; the address then appears on the canary's `last_used_ips`.

**An AWS access key.** Every call made with the key is a CloudTrail record, and Feint reads the trail as it is delivered. So:

- `detail.count` is exact, and `detail.calls` lists what was tried, one entry per call in order: `occurred_at`, `action` as `service:Operation` (`sts:GetCallerIdentity` succeeds, everything else is denied), `error` with AWS's error code or null, `source_ip` and `user_agent`, together, so an address reads against the call it made. The first 50 calls are listed; `count` says how many there were, and every one is at `records_url`.
- `detail.aws_quarantined_at` is set when AWS's own scanning found the key in public and attached its quarantine policy to the user. That attach is AWS's call, not the actor's: it is not among `calls` or in `count`, though its record is at `records_url`.
- Calls within about fifteen minutes of the first are gathered into one event; a later call opens a new one.
- `source_ips` and the addresses and user agents in `detail.calls` come from the records themselves, so they are complete from the first delivery.
- CloudTrail delivers in a few minutes, so `detected_at` trails the use by five to fifteen minutes.
- The records themselves are at `records_url`, in CloudTrail's own field names and reduced to what describes the request and the answer: time, service and action, Region, source address, user agent, error code and message, request parameters, response elements, resources, TLS details, and of the identity only the access key ID. Feint's account, AWS request ids and CloudTrail's bookkeeping are left out.

## Listing

`GET /v1/alerts` returns events for every canary in the organization, newest `detected_at` first. `limit` defaults to 100 and is capped at 500; values outside 1–500 are clamped rather than refused.

```bash title="curl"
curl -s "https://api.feintsecurity.com/v1/alerts?limit=200" \
  -H "Authorization: Bearer $FEINT_API_KEY"
```

```python title="Python"
r = requests.get(f"{API}/alerts", headers=HEADERS, params={"limit": 200})
r.raise_for_status()
events = r.json()
```

## Polling

To be told rather than to ask, register a [webhook endpoint](https://feintsecurity.com/docs/webhooks). To poll instead: there is no cursor yet. The list is newest-first, so a poller walks it and skips what it has already handled:

```python
import time

seen: set[str] = set()

while True:
    r = requests.get(f"{API}/alerts", headers=HEADERS, params={"limit": 200})
    r.raise_for_status()
    for event in reversed(r.json()):  # oldest first, so handling is in order
        if event["id"] in seen:
            continue
        seen.add(event["id"])
        handle(event)  # yours: page someone, open a ticket, forward to the SIEM
    time.sleep(300)
```

Poll no more often than every few minutes. The source data cannot change faster than the provider reports it, and a tighter loop learns nothing sooner.

## Acknowledging

An acknowledgement is recorded on the canary, not the event: `needs_attention` on the token means used, and nobody has acknowledged since. It is done in the console today; the API reflects it in `acknowledged_at` and `acknowledged_by`.
