{
  "id": "uA8EiLrfPrRgVqBu",
  "name": "Scan Incoming Email Attachments for Threats with VirusTotal and AI",
  "tags": [],
  "nodes": [
    {
      "id": "edd08ff9-d57a-4b27-9dba-b08b51ce1802",
      "name": "README",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -160,
        -320
      ],
      "parameters": {
        "width": 552,
        "height": 776,
        "content": "# Scan Incoming Email Attachments for Threats with VirusTotal and AI\n\nEvery minute, the workflow polls Gmail for unread emails with attachments, hashes each file for VirusTotal lookup, and asks AI whether the email text looks like phishing. A rule-based scorer combines both signals and routes the attachment to one of three outcomes: quarantine + Slack alert, human review queue, or safe Drive folder.\n\n## How it works\n1. Detect a new email with an attachment\n2. Hash each attachment with SHA256\n3. Look up the hash in VirusTotal\n4. Run an AI phishing check on the email text\n5. Combine both signals into a risk level (danger / suspicious / safe)\n6. Route to the matching action branch\n\n## Setup steps\n1. Get a free VirusTotal API key and add it as a Header Auth credential (Name: x-apikey)\n2. Create a Gmail label called QUARANTINE and a Google Drive folder for safe files\n3. Create a Google Sheet with columns: timestamp, email_from, email_subject, attachment, malicious_count, ai_verdict\n4. Open Set Configuration and fill in the Sheet ID, Drive folder ID, Slack channel, and label name\n5. Connect Gmail, Google Sheets, Google Drive, Slack, OpenAI, and VirusTotal credentials\n6. Activate the workflow"
      },
      "typeVersion": 1
    },
    {
      "id": "73d67af0-5a99-4f48-befd-dc2dcd3a3c38",
      "name": "Section 1 Trigger & Configure",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        416,
        -320
      ],
      "parameters": {
        "color": 7,
        "width": 420,
        "height": 340,
        "content": "## 1. Trigger & Configure\nPoll Gmail every minute for unread messages with attachments, then load all editable values (Sheet ID, Drive folder ID, Slack channel, quarantine label) from a single Set Configuration node."
      },
      "typeVersion": 1
    },
    {
      "id": "f80d9ede-cbf4-45b6-ae5f-a8307a0d4d0a",
      "name": "Section 2 Scan",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        864,
        -320
      ],
      "parameters": {
        "color": 7,
        "width": 700,
        "height": 340,
        "content": "## 2. Scan\nSplit the email into one item per attachment, calculate a SHA256 hash, look it up in VirusTotal, and run an AI phishing classifier on the subject and body."
      },
      "typeVersion": 1
    },
    {
      "id": "d26a61a1-8546-4c4d-8c8a-cf082bf0f3b8",
      "name": "Section 3 Score & Route",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1584,
        -320
      ],
      "parameters": {
        "color": 7,
        "width": 580,
        "height": 340,
        "content": "## 3. Score & Route\nCombine the VirusTotal malicious count and the AI verdict into a single risk level. AI never decides quarantine alone \u2014 the final rule is deterministic."
      },
      "typeVersion": 1
    },
    {
      "id": "4df7944f-ec3c-45d0-9d52-354244d9385d",
      "name": "Section 4A Danger",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2176,
        -320
      ],
      "parameters": {
        "color": 7,
        "width": 484,
        "height": 312,
        "content": "## 4A. Danger \u2192 Quarantine\nHigh-risk attachments: apply the QUARANTINE Gmail label and push a Slack alert to the security channel."
      },
      "typeVersion": 1
    },
    {
      "id": "0a1d3462-f5dd-447d-bbb3-01c7775f3596",
      "name": "Section 4B Suspicious",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2176,
        16
      ],
      "parameters": {
        "color": 7,
        "width": 340,
        "height": 312,
        "content": "## 4B. Suspicious \u2192 Human Review\nLog suspicious attachments to a Google Sheet review queue so a human can make the final call."
      },
      "typeVersion": 1
    },
    {
      "id": "d3996539-6309-4674-97d0-6031e619418a",
      "name": "Section 4C Safe",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2176,
        352
      ],
      "parameters": {
        "color": 7,
        "width": 340,
        "height": 296,
        "content": "## 4C. Safe \u2192 Drive\nSave clean attachments to the designated safe Google Drive folder for normal use."
      },
      "typeVersion": 1
    },
    {
      "id": "372d6818-7f5d-42b2-bca6-8157403dc8ca",
      "name": "Poll Gmail for New Attachments",
      "type": "n8n-nodes-base.gmailTrigger",
      "position": [
        464,
        -160
      ],
      "parameters": {
        "filters": {
          "q": "has:attachment is:unread"
        },
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "60ba2bf3-3913-4cff-aa20-109bbffda38f",
      "name": "Set Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        688,
        -160
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfg-1",
              "name": "review_sheet_id",
              "type": "string",
              "value": "REPLACE_WITH_YOUR_SHEET_ID"
            },
            {
              "id": "cfg-2",
              "name": "review_sheet_name",
              "type": "string",
              "value": "suspicious_queue"
            },
            {
              "id": "cfg-3",
              "name": "security_slack_channel",
              "type": "string",
              "value": "REPLACE_WITH_SLACK_CHANNEL"
            },
            {
              "id": "cfg-4",
              "name": "quarantine_label",
              "type": "string",
              "value": "REPLACE_WITH_QUARANTINE_LABEL"
            },
            {
              "id": "cfg-5",
              "name": "safe_drive_folder_id",
              "type": "string",
              "value": "REPLACE_WITH_DRIVE_FOLDER_ID"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "86e1a004-7b68-4e40-89dc-f14f284256d8",
      "name": "Extract Attachments from Email",
      "type": "n8n-nodes-base.code",
      "position": [
        896,
        -160
      ],
      "parameters": {
        "jsCode": "// Split the email into one item per attachment\nconst items = $input.all();\nconst output = [];\n\nfor (const item of items) {\n  const data = item.json;\n  const binary = item.binary || {};\n\n  for (const [key, attachment] of Object.entries(binary)) {\n    output.push({\n      json: {\n        email_id: data.id,\n        email_subject: data.subject || '',\n        email_from: data.from?.value?.[0]?.address || data.from || '',\n        email_body: (data.textPlain || data.snippet || '').substring(0, 2000),\n        attachment_filename: attachment.fileName || 'unknown',\n        attachment_mime: attachment.mimeType || '',\n        attachment_size: attachment.fileSize || 0\n      },\n      binary: {\n        attachment: attachment\n      }\n    });\n  }\n}\n\nreturn output;"
      },
      "typeVersion": 2
    },
    {
      "id": "bf9f6a52-40aa-4ebe-8b3e-ce921cb5679c",
      "name": "Compute SHA256 Hash of Attachment",
      "type": "n8n-nodes-base.code",
      "position": [
        1072,
        -160
      ],
      "parameters": {
        "jsCode": "// Calculate SHA256 hash for VirusTotal lookup\nconst crypto = require('crypto');\nconst output = [];\n\nfor (const item of $input.all()) {\n  const binaryData = item.binary.attachment;\n  const buffer = Buffer.from(binaryData.data, 'base64');\n  const hash = crypto.createHash('sha256').update(buffer).digest('hex');\n\n  output.push({\n    json: {\n      ...item.json,\n      file_hash: hash\n    },\n    binary: item.binary\n  });\n}\n\nreturn output;"
      },
      "typeVersion": 2
    },
    {
      "id": "f4d22ceb-7341-4d27-8304-0cb6f6c6b950",
      "name": "Look Up Hash in VirusTotal",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "maxTries": 3,
      "position": [
        1424,
        -160
      ],
      "parameters": {
        "url": "=https://www.virustotal.com/api/v3/files/{{ $json.file_hash }}",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "retryOnFail": true,
      "typeVersion": 4.2,
      "continueOnFail": true,
      "waitBetweenTries": 2000
    },
    {
      "id": "2ba6af90-373e-4480-91f9-97ef14ce98f5",
      "name": "Classify Phishing Risk with OpenAI",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "onError": "continueRegularOutput",
      "maxTries": 2,
      "position": [
        1600,
        -160
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "gpt-4o-mini"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "content": "You are an email phishing detection assistant. Analyze the email below and return a JSON object with two fields: \"verdict\" (one of: \"phishing\", \"suspicious\", \"safe\") and \"reason\" (short explanation, max 200 chars).\n\nEmail From: {{ $json.email_from }}\nEmail Subject: {{ $json.email_subject }}\nEmail Body:\n{{ $json.email_body }}\n\nRespond ONLY with valid JSON. No markdown, no prose."
            }
          ]
        },
        "jsonOutput": true
      },
      "retryOnFail": true,
      "typeVersion": 1.8,
      "waitBetweenTries": 2000
    },
    {
      "id": "ab3db115-f4d0-42fb-83c2-6c3050e526f1",
      "name": "Calculate Combined Risk Level",
      "type": "n8n-nodes-base.code",
      "position": [
        1872,
        -160
      ],
      "parameters": {
        "jsCode": "// Combine VirusTotal + AI verdict into a single risk level\nconst item = $input.first();\nconst data = item.json;\n\nconst hashData = $('Compute SHA256 Hash of Attachment').item.json;\n\nlet maliciousCount = 0;\nlet suspiciousCount = 0;\nlet vtAvailable = false;\n\ntry {\n  const vtData = $('Look Up Hash in VirusTotal').item.json;\n  if (vtData && vtData.data && vtData.data.attributes) {\n    const stats = vtData.data.attributes.last_analysis_stats || {};\n    maliciousCount = stats.malicious || 0;\n    suspiciousCount = stats.suspicious || 0;\n    vtAvailable = true;\n  }\n} catch (e) {\n  // File unknown to VirusTotal - treat as not-yet-seen\n}\n\nconst aiVerdict = (data.message?.content || '').toLowerCase().trim();\n\nlet risk_level;\nif (maliciousCount >= 3 || aiVerdict.includes('phishing')) {\n  risk_level = 'danger';\n} else if (maliciousCount > 0 || suspiciousCount >= 2 || aiVerdict.includes('suspicious')) {\n  risk_level = 'suspicious';\n} else {\n  risk_level = 'safe';\n}\n\nreturn {\n  json: {\n    ...hashData,\n    malicious_count: maliciousCount,\n    suspicious_count: suspiciousCount,\n    vt_available: vtAvailable,\n    ai_verdict: aiVerdict,\n    risk_level\n  },\n  binary: item.binary\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "4a97061c-49a3-418a-9c98-2801ca972c8a",
      "name": "Route by Threat Level",
      "type": "n8n-nodes-base.switch",
      "position": [
        2032,
        -176
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "danger",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "sw-1",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.risk_level }}",
                    "rightValue": "danger"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "suspicious",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "sw-2",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.risk_level }}",
                    "rightValue": "suspicious"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "safe",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "sw-3",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.risk_level }}",
                    "rightValue": "safe"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.2
    },
    {
      "id": "77a67f98-f9c3-4f4a-93e6-4be8f7a90649",
      "name": "Apply Quarantine Label on Gmail",
      "type": "n8n-nodes-base.gmail",
      "onError": "continueRegularOutput",
      "maxTries": 3,
      "position": [
        2272,
        -192
      ],
      "parameters": {
        "labelIds": "={{ [$('Set Configuration').item.json.quarantine_label] }}",
        "messageId": "={{ $json.email_id }}",
        "operation": "addLabels"
      },
      "retryOnFail": true,
      "typeVersion": 2.1,
      "waitBetweenTries": 2000
    },
    {
      "id": "c7627934-d71b-4c92-b1a4-01ca0eb1db12",
      "name": "Send Security Alert to Slack",
      "type": "n8n-nodes-base.slack",
      "onError": "continueRegularOutput",
      "maxTries": 3,
      "position": [
        2480,
        -192
      ],
      "parameters": {
        "text": "=DANGEROUS email attachment detected\n\n- From: {{ $json.email_from }}\n- Subject: {{ $json.email_subject }}\n- File: {{ $json.attachment_filename }}\n- VirusTotal malicious count: {{ $json.malicious_count }}\n- AI verdict: {{ $json.ai_verdict }}\n\nEmail was quarantined automatically.",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Set Configuration').item.json.security_slack_channel }}"
        },
        "otherOptions": {}
      },
      "retryOnFail": true,
      "typeVersion": 2.2,
      "waitBetweenTries": 2000
    },
    {
      "id": "697a9e3b-ffa8-4de7-a960-80166efbe457",
      "name": "Log to Review Queue on Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "onError": "continueRegularOutput",
      "maxTries": 3,
      "position": [
        2272,
        160
      ],
      "parameters": {
        "columns": {
          "value": {
            "timestamp": "={{ $now.toISO() }}",
            "ai_verdict": "={{ $json.ai_verdict }}",
            "attachment": "={{ $json.attachment_filename }}",
            "email_from": "={{ $json.email_from }}",
            "email_subject": "={{ $json.email_subject }}",
            "malicious_count": "={{ $json.malicious_count }}"
          },
          "mappingMode": "defineBelow",
          "matchingColumns": []
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Set Configuration').item.json.review_sheet_name }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Set Configuration').item.json.review_sheet_id }}"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.5,
      "waitBetweenTries": 2000
    },
    {
      "id": "888f4fc8-4962-4282-8d9a-c8f654cfaf6e",
      "name": "Save Attachment to Safe Drive Folder",
      "type": "n8n-nodes-base.googleDrive",
      "onError": "continueRegularOutput",
      "maxTries": 3,
      "position": [
        2272,
        480
      ],
      "parameters": {
        "name": "={{ $json.attachment_filename }}",
        "driveId": {
          "__rl": true,
          "mode": "list",
          "value": "My Drive"
        },
        "options": {},
        "folderId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Set Configuration').item.json.safe_drive_folder_id }}"
        }
      },
      "retryOnFail": true,
      "typeVersion": 3,
      "waitBetweenTries": 2000
    },
    {
      "id": "wait-9e8177ac",
      "name": "Wait 15s for VirusTotal Rate Limit",
      "type": "n8n-nodes-base.wait",
      "position": [
        1248,
        -160
      ],
      "parameters": {
        "amount": 15
      },
      "typeVersion": 1.1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "f35f9153-01ce-4cda-afb6-5c53f4e3f945",
  "connections": {
    "Set Configuration": {
      "main": [
        [
          {
            "node": "Extract Attachments from Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Threat Level": {
      "main": [
        [
          {
            "node": "Apply Quarantine Label on Gmail",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log to Review Queue on Google Sheets",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Save Attachment to Safe Drive Folder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Look Up Hash in VirusTotal": {
      "main": [
        [
          {
            "node": "Classify Phishing Risk with OpenAI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Combined Risk Level": {
      "main": [
        [
          {
            "node": "Route by Threat Level",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Attachments from Email": {
      "main": [
        [
          {
            "node": "Compute SHA256 Hash of Attachment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Poll Gmail for New Attachments": {
      "main": [
        [
          {
            "node": "Set Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Apply Quarantine Label on Gmail": {
      "main": [
        [
          {
            "node": "Send Security Alert to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compute SHA256 Hash of Attachment": {
      "main": [
        [
          {
            "node": "Wait 15s for VirusTotal Rate Limit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Phishing Risk with OpenAI": {
      "main": [
        [
          {
            "node": "Calculate Combined Risk Level",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 15s for VirusTotal Rate Limit": {
      "main": [
        [
          {
            "node": "Look Up Hash in VirusTotal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}