# Phishing Auto-Triage Workflow

An n8n workflow that automatically triages user-reported phishing emails. Polls Gmail for emails tagged with a `reported-phishing` label, extracts URLs from the message body, submits them to VirusTotal for analysis, scores the result, and fires Slack alerts + Jira incidents for confirmed malicious emails.

![Workflow Preview](preview.png "Workflow Preview")

## Flow

```
Every 10 Minutes (cron)
  → Get many messages (Gmail, label: reported-phishing)
    → Loop Over Items (outer loop — one email at a time)
      → Get a message (full message with attachments)
        → Parse to Text from message/rfc822
          → Reconstruct Email Parts (Code — extract headers, body, URLs)
            → Extract URLs array (Code — split urls[] into individual items)
              → Iterate each URL (inner loop — one URL at a time)
                → Prepare URL for VT (Code — base64-encode for VT API)
                  → GET from VT (check if URL already analysed)
                    → If URL not found?
                      ├─ YES (found): → Triage Score
                      └─ NO (not found): → Submit for Analysis → Wait 30s → GET result from VT → Triage Score
                        → Triage Score (Code — compute verdict)
                          → Is Malicious? (IF)
                            ├─ TRUE:  → Slack Phish Alert + Create Jira Incident + Add Phishing-Confirmed label
                            └─ FALSE: → Add Phishing-False-Positive label
                              → Loop Continue → Add Phishing-Analysis-Completed label → Remove reported-phishing label → back to Iterate each URL
      ← (inner loop done) → back to Loop Over Items (next email)
```

## Node Descriptions

### Trigger & Email Retrieval

| # | Node | Purpose |
|---|---|---|
| 1 | **Every 10 Minutes** | Cron trigger (`*/10 * * * *`) — polls on schedule |
| 2 | **Get many messages** | Fetches all emails with the `reported-phishing` Gmail label |
| 3 | **Loop Over Items** | Outer SplitInBatches — processes one email at a time |
| 4 | **Get a message** | Re-fetches the full message by ID with attachments downloaded (needed for RFC822 parsing) |

### Email Parsing

| # | Node | Purpose |
|---|---|---|
| 5 | **Parse to Text from message/rfc822** | Extracts raw text from the `attachment_0` binary (the original forwarded email in RFC822 format) |
| 6 | **Reconstruct Email Parts** | Code node that parses the raw RFC822 content: extracts To/From/Subject headers, splits MIME parts by boundary, decodes base64 and quoted-printable bodies, and extracts all URLs from the combined text+HTML body. Filters out noise domains (w3.org, wikimedia.org, google.com, schema.org) |
| 7 | **Extract URLs array** | Code node that splits the `urls[]` array from the previous node into individual items — one `{ url }` per item — so the inner SplitInBatches can iterate over them |

### URL Scanning (Inner Loop)

| # | Node | Purpose |
|---|---|---|
| 8 | **Iterate each URL** | Inner SplitInBatches — processes one URL at a time through VirusTotal |
| 9 | **Prepare URL for VT** | Base64url-encodes the URL into a VT-compatible ID (`base64(url)` with `+→-`, `/→_`, strip `=`) and returns `{ url, vtId }` |
| 10 | **GET from VT** | `GET /api/v3/urls/{vtId}` — checks if VirusTotal already has analysis results for this URL |
| 11 | **If URL not found** | Routes based on whether VT returned results or a "not found" error |
| 12 | **Submit for Analysis** | `POST /api/v3/urls` with form-encoded body — submits the URL for fresh VT analysis (only if not already known) |
| 13 | **Wait for completion** | Pauses 30 seconds for VirusTotal to complete the scan |
| 14 | **GET result from VT** | Re-fetches VT results after the analysis completes |

### Scoring & Verdict

| # | Node | Purpose |
|---|---|---|
| 15 | **Triage Score** | Computes a verdict from VT analysis stats. Collects flagged engine names and threat names. Verdict logic: `detections >= 5` → MALICIOUS, `>= 1` → SUSPICIOUS, `url_count == 0` → NO_URLS, else → CLEAN |
| 17 | **Is Malicious?** | IF node — checks if verdict is MALICIOUS or SUSPICIOUS |

### Response Actions

| # | Node | Purpose |
|---|---|---|
| 18 | **Slack Phish Alert** | Posts to `#soc-phishing` with subject, sender, VT detections, and the suspicious URL |
| 19 | **Create Jira Incident** | Creates a Jira issue via REST API for SOC follow-up |
| 20 | **Add Phishing-Confirmed label** | Applies `phishing-confirmed` Gmail label (malicious path) |
| 21 | **Add Phishing-False-Positive label** | Applies `phishing-false-positive` Gmail label (clean path — the user report was a false positive) |
| 22 | **Loop Continue** | Emits `{ loop_continue: true }` to keep the inner URL loop flowing |
| 23 | **Add Phishing-Analysis-Completed label** | Applies `phishing-analysis-completed` Gmail label to mark the email as processed |
| 24 | **Remove reported-phishing label** | Strips the original `reported-phishing` label so the email is not re-processed on the next poll. Then loops back to **Iterate each URL** for the next URL |

## Verdicts

| Verdict | Condition | Actions |
|---|---|---|
| **MALICIOUS** | VT detections >= 5 | Slack alert, Jira incident, `phishing-confirmed` label |
| **SUSPICIOUS** | VT detections >= 1 | Slack alert, Jira incident, `phishing-confirmed` label |
| **CLEAN** | VT detections = 0 | `phishing-false-positive` label |
| **NO_URLS** | No URLs found in email | `phishing-false-positive` label |

## Gmail Labels

| Label | Purpose |
|---|---|
| `reported-phishing` | Applied by users to report suspicious emails — triggers the workflow. **Removed** by the workflow after processing to prevent re-polling |
| `phishing-confirmed` | Applied by workflow when verdict is MALICIOUS or SUSPICIOUS |
| `phishing-false-positive` | Applied by workflow when verdict is CLEAN or NO_URLS (user report was a false positive) |
| `phishing-analysis-completed` | Applied after all URLs in the email have been scanned |

## Credentials Required

| Credential | Type | Used By |
|---|---|---|
| Gmail account | `gmailOAuth2` | Get many messages, Get a message, label operations |
| VirusTotal account | `virusTotalApi` | GET from VT, Submit for Analysis, GET result from VT |
| VT Custom Auth | `httpCustomAuth` | Submit for Analysis (form-encoded POST) |
| Slack Credentials | Slack OAuth | Slack Phish Alert |
| Jira API | HTTP Basic Auth | Create Jira Incident |


## Design Decisions

- **Two-pass VT lookup:** The workflow first checks if VT already has results for the URL (GET). If not found, it submits for fresh analysis, waits 30 seconds, then fetches results. This avoids unnecessary re-scans and respects VT rate limits.
- **Nested loops:** The outer loop iterates emails, the inner loop iterates URLs within each email. When the inner loop completes, control returns to the outer loop for the next email.
- **Label-based state machine:** Gmail labels track processing state — `reported-phishing` (input, removed after processing), `phishing-confirmed`/`phishing-false-positive` (verdict), `phishing-analysis-completed` (processed). The `reported-phishing` label is explicitly removed after analysis so the email is not re-polled on the next cron run.
- **Domain exclusion list:** Common benign domains (w3.org, wikimedia.org, google.com, schema.org) are filtered out during URL extraction to reduce noise and VT API calls.
