| 1 | # Approval flow |
| 2 | |
| 3 | The gate is the point of the project, so it gets its own document. |
| 4 | |
| 5 | ## 1. Bundle generated |
| 6 | |
| 7 | A pipeline run produces a candidate bundle and stages it under |
| 8 | `output/candidates/<bundle-id>/`: |
| 9 | |
| 10 | ``` |
| 11 | lists/cti-malicious-ip |
| 12 | lists/cti-malicious-domain |
| 13 | lists/cti-malicious-url |
| 14 | lists/cti-malware-hash |
| 15 | lists/cti-leaked-email |
| 16 | local_cti_rules.xml |
| 17 | ttp_coverage.md |
| 18 | manifest.json |
| 19 | keys.json |
| 20 | ``` |
| 21 | |
| 22 | `manifest.json` carries the counts, the technique list, and the diff against the |
| 23 | last approved bundle (added / removed / unchanged). |
| 24 | |
| 25 | ## 2. Analyst emailed |
| 26 | |
| 27 | The email summarizes the bundle - new vs. aged-out indicators, counts by type, and |
| 28 | the extracted ATT&CK techniques - and links to the review page. The link embeds an |
| 29 | `itsdangerous` signed token: |
| 30 | |
| 31 | ``` |
| 32 | URLSafeTimedSerializer(secret, salt="cti-rule-approval").dumps({"bundle_id": ...}) |
| 33 | ``` |
| 34 | |
| 35 | The token is signed with `CTI_APPROVAL_SECRET` and carries a TTL (`token_ttl`, |
| 36 | default 24h). It can't be forged without the secret and stops working once it |
| 37 | expires, so a stale email can't approve an old bundle. |
| 38 | |
| 39 | The backend is pluggable: `file` (writes the rendered email to disk, used by the |
| 40 | demo), `console`, or `smtp` for a real inbox. |
| 41 | |
| 42 | ## 3. Review |
| 43 | |
| 44 | The review page renders the manifest - the diff cards, indicators by type, the |
| 45 | generated CDB lists and their sizes, and the full ATT&CK coverage table. The analyst |
| 46 | has the whole picture without leaving the page. |
| 47 | |
| 48 | ## 4. Decision |
| 49 | |
| 50 | - **Approve** → `promote` copies the candidate into `output/active/`, updates the |
| 51 | baseline used for the next diff, and (when `wazuh_etc_dir` is set) writes the lists |
| 52 | and rules into the Wazuh manager. A reload picks them up. |
| 53 | - **Reject** → a `REJECTED` marker with the optional reason is written to the |
| 54 | candidate directory and nothing is deployed. |
| 55 | |
| 56 | Either way the decision is recorded and the bundle's status shows on the dashboard. |
| 57 | |
| 58 | ## Token, not session |
| 59 | |
| 60 | There's deliberately no login. The signed link is the authorization - it's what was |
| 61 | emailed to the analyst, it's scoped to one bundle, and it expires. That keeps the |
| 62 | service stateless and means it can sit behind a reverse proxy on the SOC network |
| 63 | without its own user store. For a multi-analyst setup you'd put SSO in front of it; |
| 64 | the token model doesn't change. |