Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion docs/email-security/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ faster, which is worth doing for its own sake.

| Route | Does |
|---|---|
| `POST /messages/{msg_uuid}/actions` | Perform a typed action on one message. Body: `action` (`quarantine_message`, `trash_message`, `move_to_spam`, `restore_message`, `banner_message`, `unbanner_message`), optional `force` (boolean; see [alert-only overrides](#explicit-override-in-alert-only-mode)), optional `reason`, optional `attempt` (idempotency token — omit to collapse onto the existing attempt). `banner_message` uses the organization's own banner, rendered from its `mailsec_policy` record of type `banners`; its optional `text` (plain text, at most 512 characters; refused on any other action) replaces the wording for that one banner. No caller supplies HTML. Requires `mailsec.act` |
| `POST /messages/{msg_uuid}/actions` | Perform a typed action on one message. Body: `action` (`quarantine_message`, `trash_message`, `move_to_spam`, `restore_message`, `release_message`, `banner_message`, `unbanner_message`), optional `force` (boolean; see [alert-only overrides](#explicit-override-in-alert-only-mode)), optional `reason`, optional `attempt` (idempotency token — omit to collapse onto the existing attempt). `banner_message` uses the organization's own banner, rendered from its `mailsec_policy` record of type `banners`; its optional `text` (plain text, at most 512 characters; refused on any other action) replaces the wording for that one banner. No caller supplies HTML. Requires `mailsec.act` |
| `POST /messages/{msg_uuid}/actions` with `submit_sample` or `withdraw_sample` | Copy one message to LimaCharlie, or withdraw that copy. `submit_sample` requires `category` (`missed_threat`, `false_positive`, `other`) and a `reason` of 1-1024 characters; `withdraw_sample` takes an optional `reason`. Only a person can run either: D&R rules, automations and the AI agent are refused. Submission requires organization opt-in; withdrawal remains available after opt-out or provider disconnect. See [Sample Submission](sample-submission.md). Requires `mailsec.act` |
| `DELETE /submissions/{submission_id}` | Withdraw a submission: hard-deletes the stored copy and its metadata and returns `{withdrawn: true, submission_id, action_id}`. An unknown or already-deleted id returns `{withdrawn: false, submission_id}` with no `action_id`; an expired id is still cleaned up if metadata remains. Requires `mailsec.act` |
| `POST /campaigns/{campaign_id}/actions` | Sweep a campaign. Same body plus `confirm`. **Without `confirm` this previews** and changes nothing, returning the member ids, the distinct mailboxes, the counts and a `confirm` token derived from that exact member set. With `confirm` it executes exactly that set; a campaign that grew since the preview is refused. Capped at 500 members. `reason` is recorded on **every member's** audit row and on the sweep's own row (`action_id` in the response); `attempt` (bounded at 128 characters, refused not truncated) mints a new row per member, so a deliberate retry is recorded beside what it retried instead of over it. Neither is part of the `confirm` token. Requires `mailsec.act` |
Expand Down Expand Up @@ -484,6 +484,8 @@ telemetry and every customer rule is keyed on them.
| `EMAIL_VERDICT` | Once per verdict **decision**. `revision/seq: 0` with `revision/mode: auto` is the rule pack's own verdict, emitted at ingest immediately after that message's `EMAIL_MESSAGE`; `seq: 1…` is one per override (`analyst`, `ai`, `detonation`) |
| `EMAIL_ACTION` | Once per remediation outcome, including failures and skips |
| `EMAIL_USER_REPORT` | Once per message that reaches the abuse mailbox |
| `EMAIL_DISPOSITION` | Independent analyst/SOAR disposition changed or cleared; carries actor, source, note, server timestamp, prior value, and sequence |
| `EMAIL_REPORT_RESOLVED` | Report resolved; carries report/message identities, recorded disposition and resolver, and mailbox when available |
| `EMAIL_INGEST_ERROR` | Once per message that could not be fetched or processed |

Two consequences worth stating plainly:
Expand Down Expand Up @@ -525,3 +527,19 @@ release status values are `requested`, `released` and `denied`. Responses includ
independent per-feed `coverage`; an empty list with `not_granted`, pending, stale
or error coverage is not proof of zero blocked messages. See
[Provider Quarantine](provider-quarantine.md#cli-and-api) for the row contract.

### Disposition and release

| Route | Body and behavior |
|---|---|
| `POST /messages/{msg_uuid}/disposition` | `disposition` from the five-value vocabulary, optional `note`; or `clear: true`. Requires `mailsec.set`. |
| `POST /messages/dispositions` | Same decision plus 1–500 unique `msg_uuids`. Returns per-message results, including partial failures. Requires `mailsec.set`. |
| `GET /messages?disposition=<value>` | Filter by one disposition, or `none` for no current label. |
| `POST /messages/{msg_uuid}/actions` with `action: release_message` | Restore, benign verdict revision, benign disposition, history repair. Optional `mode` (`analyst` or `ai`), `reason`, `force`, `attempt`. Requires `mailsec.act`. |
| `POST /reports/{report_id}/resolve` | `disposition`; optional `remediation` with `scope` (`message`, `group` or `campaign`), `action` and optional `confirm`, `reason`, `force`, `attempt` (a required UUID for group scope, reused for preview, confirmation and polling; see [Message Groups](groups.md)). Without confirm, remediation is previewed and the report stays open. Pure resolution requires `mailsec.set`; remediation also requires `mailsec.act`. |

The five dispositions are `malicious`, `spam`, `graymail`, `benign`, and
`simulation`. Disposition is separate from verdict. Message detail includes
`disposition_info: {disposition, note, actor, source, ts}` and `disposition_seq`.
Source is `analyst`, `api`, `extension`, `report`, or `ai`; attribution and time
are assigned by the service. Bodies cannot forge them.
22 changes: 21 additions & 1 deletion docs/email-security/automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ is your abuse mailbox. The person who sent the report is `event/reporter`.
| `EMAIL_USER_REPORT` | When a message reaches the abuse mailbox and becomes a report |
| `EMAIL_PROVIDER_QUARANTINE` | Microsoft reports a quarantined, spam-filtered or failed delivery. `provider_status` distinguishes them; this is provider delivery metadata, not an engine verdict |
| `EMAIL_RELEASE_REQUEST` | An end user requests release from Microsoft hosted quarantine. Releases/denials are retained as history and do not emit this event |
| `EMAIL_DISPOSITION` | Independent analyst/SOAR disposition changed or cleared; carries actor, source, note, server timestamp, prior value, and sequence |
| `EMAIL_REPORT_RESOLVED` | Report resolved; carries report/message identities, recorded disposition and resolver, and mailbox when available |
| `EMAIL_INGEST_ERROR` | When a message could not be fetched or processed. Coverage honesty: failures are visible, never silent |

`EMAIL_MESSAGE` is emitted once and is immutable. When a verdict changes, the
Expand Down Expand Up @@ -302,7 +304,7 @@ rules:

The typed actions available to `extension request` are the same six the console
and the CLI use: `quarantine_message`, `trash_message`, `move_to_spam`,
`restore_message`, `banner_message`, `unbanner_message`. They route to the same
`restore_message`, `release_message`, `banner_message`, `unbanner_message`. They route to the same
executor, so the organization's `alert_only` / `enforce` mode, the audit row and
idempotency all apply unchanged — there is exactly one remediation path in this
product.
Expand Down Expand Up @@ -667,3 +669,21 @@ before notification from delay inside processing. The same pattern can alarm
on another present timing field. `analysis_ms` includes the delayed analysis
window; `end_to_end_ms` ends at the initial verdict. Check `clock_skew` before
interpreting clamped measurements.

### Feedback events and typed actions

`EMAIL_DISPOSITION` and `EMAIL_REPORT_RESOLVED` carry `group_id` when the indexed original has a message group, alongside top-level `disposition`,
`actor`, `source`, and `ts`. Disposition events also carry `seq` and `prior`.
Resolution events carry `report_id`. Both carry `msg_uuid`, provider, and mailbox
when an indexed message supplies it. An unlinked resolution retains its explicit
report identity; it never invents a mailbox. Durable retries retain `job_id`.

The Email Security extension exposes `set_disposition`, `revise_verdict`,
`release_message`, and `resolve_report` as typed actions. They use the caller's
permissions and authenticated identity. A D&R rule may record disposition or
request a release; releases obey alert-only/force and retain the action audit.
Automated revisions and releases require explicit `mode: ai` and preserve the
rule attribution. Report resolution requires an interactive analyst decision.
The revise action takes `mode: analyst|ai`, a verdict, and a nonempty list of
rationale strings. Resolve accepts the same five dispositions and optional
message/campaign remediation with preview/confirm.
37 changes: 36 additions & 1 deletion docs/email-security/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ error: `submission get` returns `submission: null` and `submission withdraw` ret

### Revising a verdict is `mailsec.act`, not `mailsec.set`

`message revise` records a human disposition over the scorer's, appending to the
`message revise` records a human verdict revision over the scorer's, appending to the
message's history rather than overwriting it. `--rationale` is required and
audited — at least one, at most ten, each 280 characters or fewer.

Expand Down Expand Up @@ -436,3 +436,38 @@ Microsoft delivery and hosted-quarantine activity. They support connection,
status, time-window and cursor filters and return independent coverage. They
require a CLI build containing these commands and `mailsec.get`. See
[Provider Quarantine](provider-quarantine.md#cli-and-api) for examples and limits.

## Independent disposition and release

```bash
limacharlie mailsec message disposition <msg_uuid> --disposition benign --note "Reviewed"
limacharlie mailsec message disposition <msg_uuid> --clear
limacharlie mailsec message list --disposition none
limacharlie mailsec message bulk-disposition --msg-uuids <id1> --msg-uuids <id2> --disposition spam
limacharlie mailsec message release <msg_uuid> --reason "Reviewed as safe" --mode analyst
```

Disposition accepts `malicious`, `spam`, `graymail`, `benign`, or `simulation` and
never changes the engine verdict. A bulk selection is limited to 500 unique IDs;
individual failures are reported and cause a nonzero CLI exit. Release restores
placement and records a benign verdict and disposition. It needs `mailsec.act`;
`--force` supplies explicit consent in alert-only mode. See [Messages](messages.md).

For report remediation, first preview:

```bash
limacharlie mailsec report resolve <report_id> --disposition malicious --scope message --action quarantine_message
# all recipient copies of the reported message's group (durable job)
limacharlie mailsec report resolve <report_id> --disposition malicious --scope group --action quarantine_message --attempt $(uuidgen)
```

Read `remediation_preview`, then repeat with `--confirm <token>` and, when needed,
`--force`. Pure resolution uses `mailsec.set`; remediation also needs `mailsec.act`.
The report remains open during preview or when provider remediation fails.

Group report remediation uses `--scope group` with an explicit UUID `--attempt`,
reused through preview, confirmation and polling. Add `--wait` to wait up to
300 seconds for a complete preview or resolution; timeouts exit with code 2 and
the durable job continues. Resume with the same attempt and confirmation.
See [group report remediation](user-reports.md#remediate-the-same-message-across-recipients)
for the complete workflow.
50 changes: 50 additions & 0 deletions docs/email-security/messages.md
Original file line number Diff line number Diff line change
Expand Up @@ -486,3 +486,53 @@ A key with no profile means **no history at all**, and the response says so
explicitly rather than returning a zeroed profile that would read as a
known-but-quiet sender. Keys are lowercased, and a bare address or domain is
resolved for you.

## Analyst disposition

Disposition records the security team's decision independently of the engine
verdict: `malicious`, `spam`, `graymail`, `benign`, or `simulation`. Setting it
preserves the verdict and runs no automations. The decision includes a note of up
to 1024 characters, authenticated actor, source, and server timestamp. Message
lists expose `disposition`, which is `null` when no decision is set or it was
cleared; message detail includes `disposition_info` and `disposition_seq`.

```bash
limacharlie mailsec message disposition <msg_uuid> --disposition spam --note "Reviewed" --oid $OID
limacharlie mailsec message disposition <msg_uuid> --clear --oid $OID
limacharlie mailsec message list --disposition none --oid $OID
limacharlie mailsec message bulk-disposition --msg-uuids <id1> --msg-uuids <id2> --disposition malicious --oid $OID
```

These writes require `mailsec.set`. Bulk input is limited to 500 unique IDs and
returns one ordered result per message, including individual failures. A missing
message fails alone without affecting the others. An error starting with `not
confirmed, retry the same decision` means the outcome is unknown; repeating the same
request is safe because an identical decision is a no-op. The CLI exits with a
failure status when any member fails. The `none` list filter selects
messages with no current disposition, including cleared decisions.

A benign decision removes this message's credited sender-history flag; malicious
credits it once. The message-count history remains intact. `EMAIL_DISPOSITION`
reports each real decision change, including a clear (empty disposition), with
its sequence and previous value. Event delivery may retry with the same `job_id`;
consumers should deduplicate by that ID or message/sequence.

## Release a message

```bash
limacharlie mailsec message release <msg_uuid> --reason "Reviewed as safe" --oid $OID
```

`release_message` restores provider placement and records both a benign verdict
revision and a benign disposition. It repairs sender history and records one
idempotent action. The revision mode is `analyst` by default; an AI caller can
choose `--mode ai`. It requires `mailsec.act` and follows restore's enforcement
rule: in alert-only mode it is recorded and withheld; `--force` is explicit consent
to perform it. The withheld action changes neither verdict nor disposition.

Use ordinary `restore_message` when you intend only to move the message back
without deciding it is benign. Revising a verdict to benign alone does not restore
mail automatically.

Disposition writes require an indexed message within the message retention window.
Retained evidence outside that window remains available for backtesting.
10 changes: 7 additions & 3 deletions docs/email-security/policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -736,15 +736,19 @@ The templated acknowledgement sent to someone who reported a message. See
```yaml
policy_type: reporter_reply
enabled: true
acknowledgement: "Your report was received and is being reviewed."
on_resolve: true
templates:
malicious: "Thanks — you were right. We removed that message from every mailbox it reached."
benign: "Thanks for checking. That message is legitimate; no action was needed."
malicious: "Your report has been reviewed and classified as malicious."
benign: "Your report has been reviewed and classified as benign."
```

| Field | Default | |
|---|---|---|
| `enabled` | `false` | It sends mail on your behalf to your own staff; opt-in |
| `templates` | — | Keyed by verdict. A verdict with no template falls back to a generic acknowledgement, so enabling replies can never leave a reporter with silence |
| `acknowledgement` | Neutral receipt wording | Plain text for the receipt reply; at most 4096 UTF-8 bytes, no markup |
| `on_resolve` | `false` | Send a separate reply after report resolution |
| `templates` | — | Plain-text resolution templates keyed by malicious, spam, graymail, benign, simulation. Missing entries state the recorded disposition; each is at most 4096 UTF-8 bytes |

Template keys must be verdicts. Values are plain text (no `<` or `>`), capped at
4096 characters.
Expand Down
1 change: 1 addition & 0 deletions docs/email-security/rule-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ have different wrappers and validation rules.
| `dr-mail`, `post_verdict` only | `verdict/verdict` |
| `dr-general` on `EMAIL_MESSAGE` | `event/sender/email/domain/root` |
| `dr-general` on `EMAIL_VERDICT` or `EMAIL_ANALYSIS_COMPLETE` | `event/revision/verdict` |
| `dr-general` on `EMAIL_DISPOSITION` or `EMAIL_REPORT_RESOLVED` | `event/disposition` |
| Inside `scope` with `path: links` | `href_url/domain/root` |

The MDM is the root of a mail rule. Do not add `mdm/` or `event/`. The Hive
Expand Down
Loading
Loading