{
  "id": "ETHbjTJvq3p1U2k1b2z4W",
  "name": "Phishing URL Reputation Checker",
  "tags": [],
  "nodes": [
    {
      "id": "9f276b8f-a5f7-4ee9-9bdc-5de4df3071e1",
      "name": "Webhook - Submit URL for Analysis",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -816,
        496
      ],
      "parameters": {
        "path": "phishing-check",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "d0dda112-56e7-42e5-82f6-50d074ca4523",
      "name": "Normalize Input URL",
      "type": "n8n-nodes-base.code",
      "position": [
        -624,
        496
      ],
      "parameters": {
        "jsCode": "let rawUrl = $input.first().json.body.url;\n\n// Ensure string\nrawUrl = typeof rawUrl === 'string' ? rawUrl : '';\nrawUrl = rawUrl.trim();\n\nif (!rawUrl) {\n  return [{\n    original_url: rawUrl,\n    normalized_url: \"\",\n    is_valid: false\n  }];\n}\n\nlet normalizedUrl = rawUrl;\n\n// Add scheme if missing\nif (!/^[a-zA-Z]+:\\/\\//.test(normalizedUrl)) {\n  normalizedUrl = 'http://' + normalizedUrl;\n}\n\n// Lightweight validation without URL class or regex rules\nlet isValid = false;\n\ntry {\n  // This trick works in restricted sandbox\n  const parts = normalizedUrl.split('://');\n\n  if (parts.length === 2) {\n    const protocol = parts[0].toLowerCase();\n    const hostPart = parts[1].split('/')[0];\n\n    if (\n      (protocol === 'http' || protocol === 'https') &&\n      hostPart.length > 0 &&\n      hostPart.includes('.')\n    ) {\n      isValid = true;\n    }\n  }\n\n} catch (err) {\n  isValid = false;\n}\n\nreturn [{\n  original_url: rawUrl,\n  normalized_url: normalizedUrl,\n  is_valid: isValid\n}];\n\n\n\n\n"
      },
      "typeVersion": 2
    },
    {
      "id": "be6503df-d23c-4eda-98ad-ad6112faec0d",
      "name": "IF - URL is Valid?",
      "type": "n8n-nodes-base.if",
      "position": [
        -384,
        496
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "96418dd7-c3d2-4384-b3b2-fd4645e0b977",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.is_valid }}",
              "rightValue": false
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "35b3ab91-a3c1-460c-a5e2-ada7bbe610c3",
      "name": "Respond - Invalid URL error",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -368,
        720
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "{\n  \"error\": \"Invalid or malformed URL\",\n  \"message\": \"Please submit a valid URL\"\n}\n"
      },
      "typeVersion": 1.5
    },
    {
      "id": "1e01c712-1adb-4f25-9f19-f87dadae2594",
      "name": "VirusTotal - Submit URL for Scan",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueErrorOutput",
      "position": [
        -64,
        480
      ],
      "parameters": {
        "url": "https://www.virustotal.com/api/v3/urls",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "form-urlencoded",
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "url",
              "value": "={{ $json.normalized_url }}"
            }
          ]
        },
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/x-www-form-urlencoded"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "b62c79d8-e809-48ed-a83a-abc98038b1df",
      "name": "Wait - VirusTotal Scan Processing",
      "type": "n8n-nodes-base.wait",
      "position": [
        256,
        480
      ],
      "parameters": {
        "amount": 10
      },
      "typeVersion": 1.1
    },
    {
      "id": "c0f96217-dd5d-4cab-a4b2-353644158989",
      "name": "VirusTotal - Get Scan Analysis",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueErrorOutput",
      "position": [
        432,
        480
      ],
      "parameters": {
        "url": "=https://www.virustotal.com/api/v3/analyses/{{ $json.data.id }}",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "7dbe02af-08e2-4b45-83c2-f2b93d4e8d9d",
      "name": "IF - VirusTotal Analysis Completed?",
      "type": "n8n-nodes-base.if",
      "position": [
        688,
        512
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "8dcc6478-58cb-4fd9-93c9-a2bc02011889",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.data.attributes.status }}",
              "rightValue": "completed"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "63c7b3f7-4730-452b-92d2-42c94592478e",
      "name": "Wait - Retry VT Analysis Poll",
      "type": "n8n-nodes-base.wait",
      "position": [
        256,
        704
      ],
      "parameters": {
        "amount": 15
      },
      "typeVersion": 1.1
    },
    {
      "id": "5a7f0141-9a96-469f-83fa-ccb2c998dfb1",
      "name": "Extract VirusTotal Verdict Stats",
      "type": "n8n-nodes-base.code",
      "position": [
        960,
        496
      ],
      "parameters": {
        "jsCode": "const stats = $json.data.attributes.stats;\nconst url_info  = $json.meta.url_info;\n\nreturn [{\n  vt_malicious: stats.malicious || 0,\n  vt_suspicious: stats.suspicious || 0,\n  vt_harmless: stats.harmless || 0,\n  vt_undetected: stats.undetected || 0,\n  vt_status: $json.data.attributes.status,\n  url: url_info.url\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "2c15f50f-9139-4760-8a52-4f43c4b190c4",
      "name": "Build Phishing Verdict",
      "type": "n8n-nodes-base.code",
      "position": [
        1216,
        496
      ],
      "parameters": {
        "jsCode": "let risk = \"Low\";\nlet verdict = \"SAFE\";\n\nconst malicious = $json.vt_malicious || 0;\nconst suspicious = $json.vt_suspicious || 0;\nlet url = $json.url || \"\";\n\n// High confidence phishing\nif (malicious >= 3) {\n  risk = \"High\";\n  verdict = \"PHISHING\";\n}\n\n// Medium confidence suspicious (few malicious engines)\nelse if (malicious >= 1 && malicious <= 2) {\n  risk = \"Medium\";\n  verdict = \"SUSPICIOUS\";\n}\n\n// Medium confidence suspicious (multiple suspicious engines)\nelse if (suspicious >= 3) {\n  risk = \"Medium\";\n  verdict = \"SUSPICIOUS\";\n}\n\n// Defang the URL if verdict is NOT SAFE\nif (verdict !== \"SAFE\") {\n  // Simple defanging: replace . with [.] and hxxp:// instead of http\n  url = url.replace(/^http:\\/\\//i, \"hxxp://\")\n           .replace(/^https:\\/\\//i, \"hxxps://\")\n           .replace(/\\./g, \"[.]\");\n}\n\nreturn [{\n  url,\n  verdict,\n  risk_level: risk,  \n  engines: {\n    virustotal: {\n      malicious,\n      suspicious\n    }\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "7f4b09de-1bd0-4416-a086-59a108f3ffc3",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -880,
        304
      ],
      "parameters": {
        "color": 7,
        "width": 384,
        "height": 592,
        "content": "## URL Input & Normalization\nAccepts user URLs via webhook and ensures consistent formatting before security analysis.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "37658244-61f2-426f-8043-5517a224039c",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -480,
        304
      ],
      "parameters": {
        "color": 7,
        "width": 320,
        "height": 592,
        "content": "## Validation\nChecks for malformed or missing URLs and returns an error if validation fails.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "c9f716ab-354f-4d5d-93cf-eb72d636bba4",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -144,
        304
      ],
      "parameters": {
        "color": 7,
        "width": 304,
        "height": 592,
        "content": "## Threat Intelligence Submission\nSubmits validated URLs to VirusTotal for multi-engine reputation scanning.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "d8672204-2303-4825-8326-f22944603ff2",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        176,
        304
      ],
      "parameters": {
        "color": 7,
        "width": 688,
        "height": 1008,
        "content": "## Asynchronous Scan Handling\nPolls VirusTotal until the analysis is completed or retries reach the limit.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "fcebb1a2-d6e0-445d-9664-27de29b5dec8",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        880,
        336
      ],
      "parameters": {
        "color": 7,
        "height": 384,
        "content": "## Detection Signal Extraction\nExtracts VirusTotal detection statistics used for phishing classification.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "60432bcf-0edc-4014-9651-6f7cd3fdc199",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1136,
        336
      ],
      "parameters": {
        "color": 7,
        "width": 256,
        "height": 384,
        "content": "## Phishing Decision Engine\nApplies threshold logic to classify URLs as SAFE, SUSPICIOUS, or PHISHING.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "6dd7cbd5-43c1-4a28-b0b2-331fc49900ec",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1408,
        336
      ],
      "parameters": {
        "color": 7,
        "width": 256,
        "height": 384,
        "content": "## Logging & Output\nStores scan results in Google Sheets for monitoring and incident tracking.\n\n "
      },
      "typeVersion": 1
    },
    {
      "id": "c015ee89-b8fc-4853-8aaa-54cd4c779279",
      "name": "Respond - VT Service Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        800,
        64
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "{\n  \"error\": \"Threat intelligence service unavailable\",\n  \"message\": \"VirusTotal request failed. Please try again later.\"\n}"
      },
      "typeVersion": 1.5
    },
    {
      "id": "c6e341ff-f9bb-4b4c-a9e3-42537fb81edd",
      "name": "IF - VT Analysis Error?",
      "type": "n8n-nodes-base.if",
      "position": [
        576,
        144
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "a5b806df-b3ce-4743-b2cb-3f297924c14d",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{$json.error}}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "e9269973-2a40-42fc-bfac-77a571540389",
      "name": "IF - VT Submit Error?",
      "type": "n8n-nodes-base.if",
      "position": [
        112,
        128
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "59a51a9b-ffbf-46cf-b7ba-b97ebbb0791e",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{$json.error}}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "1ddd4032-8b2d-4033-9c7c-dcddf4079352",
      "name": "Log Scan Result",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1488,
        496
      ],
      "parameters": {
        "columns": {
          "value": {
            "verdict": "={{ $json.verdict }}",
            "malicious": "={{ $json.engines.virustotal.malicious }}",
            "timestamp": "={{ new Date().toISOString() }}",
            "risk_level": "={{ $json.risk_level }}",
            "suspicious": "={{ $json.engines.virustotal.suspicious }}",
            "original_url": "={{ $json.url }}"
          },
          "schema": [
            {
              "id": "timestamp",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "timestamp",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "original_url",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "original_url",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "verdict",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "verdict",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "risk_level",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "risk_level",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "malicious",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "malicious",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "suspicious",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "suspicious",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/14qydoIHflwd-3gYyj7g0QKH5Bfsny_OiLzdnvEKOwfo/edit#gid=0",
          "cachedResultName": "Phishing URL scan"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "14qydoIHflwd-3gYyj7g0QKH5Bfsny_OiLzdnvEKOwfo",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/14qydoIHflwd-3gYyj7g0QKH5Bfsny_OiLzdnvEKOwfo/edit?usp=drivesdk",
          "cachedResultName": "Phishing URL"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "b6e9da2e-27a0-400a-8bce-d296a9a61752",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        32,
        -16
      ],
      "parameters": {
        "color": 7,
        "width": 944,
        "height": 304,
        "content": "## Error Handling & Resilience\nHandles VirusTotal API failures, validation errors, and timeout conditions to ensure reliable execution.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "162e190f-6e38-464d-9350-7fad93cf93c5",
      "name": "Increment Retry Counter",
      "type": "n8n-nodes-base.code",
      "position": [
        672,
        832
      ],
      "parameters": {
        "jsCode": "const retry = $json.retry_count || 0;\nconst newRetry = retry + 1;\n\nreturn [{\n  json: {\n    ...$json,\n    retry_count: newRetry\n  }\n}];\n\n"
      },
      "typeVersion": 2
    },
    {
      "id": "6d76ed8e-d24d-4b30-9ce4-39c628ee0558",
      "name": "IF Max Retry Reached?",
      "type": "n8n-nodes-base.if",
      "position": [
        448,
        1088
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "a864ef76-3d9a-4909-b6d4-f3d7d5144b84",
              "operator": {
                "type": "number",
                "operation": "gte"
              },
              "leftValue": "={{ $json.retry_count }}",
              "rightValue": 5
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "344723b0-aa1b-4584-b1c7-d5d26cb4f719",
      "name": "Respond Timeout",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        688,
        1072
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "{\n  \"status\": \"timeout\",\n  \"reason\": \"VirusTotal analysis not ready after max retries\",\n  \"url\": \"={{$json.url}}\",\n  \"retry_count\": \"={{$json.retry_count}}\"\n}\n"
      },
      "typeVersion": 1.5
    },
    {
      "id": "e0d9f480-2c11-4567-980c-1602d8c61198",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1472,
        240
      ],
      "parameters": {
        "width": 560,
        "height": 704,
        "content": "## Phishing URL Reputation Checker\nThis workflow analyzes submitted URLs to determine whether they are phishing or malicious using VirusTotal\u2019s threat intelligence data. It validates user input, submits the URL for scanning, polls for results, extracts detection signals, and generates a clear phishing verdict with risk scoring. Results are optionally logged to Google Sheets for tracking and investigation.\n\n### How it works\n1. A webhook accepts a URL from an API, form, chatbot, or automation.\n2. The URL is normalized and validated to prevent malformed or unsafe input.\n3. Valid URLs are submitted to VirusTotal for multi-engine reputation analysis.\n4. The workflow polls VirusTotal asynchronously until the scan is complete or retries are exhausted.\n5. Detection statistics are extracted and evaluated using threshold-based phishing logic.\n6. Suspicious or malicious URLs are defanged to prevent accidental clicks.\n7. The final verdict and risk level are returned and optionally logged to Google Sheets.\n\n### Setup steps\n1. Add your VirusTotal API key in the HTTP Header Auth credentials.\n2. Connect Google Sheets to store scan results.\n3. Trigger the webhook with { \"url\": \"example.com\" }.\n\n### Customization\n1. Adjust phishing thresholds in the \u201cBuild Phishing Verdict\u201d node or add additional reputation sources for stronger detection.\n2. You can add a Slack, Discord, or email notification when the verdict is not SAFE to alert security teams about potential phishing URLs in real time."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "80bb187f-9bb0-43ac-beba-67c32c7bced0",
  "connections": {
    "Log Scan Result": {
      "main": [
        []
      ]
    },
    "IF - URL is Valid?": {
      "main": [
        [
          {
            "node": "VirusTotal - Submit URL for Scan",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond - Invalid URL error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Input URL": {
      "main": [
        [
          {
            "node": "IF - URL is Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF - VT Submit Error?": {
      "main": [
        [
          {
            "node": "Respond - VT Service Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait - VirusTotal Scan Processing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Max Retry Reached?": {
      "main": [
        [
          {
            "node": "Respond Timeout",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait - Retry VT Analysis Poll",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Phishing Verdict": {
      "main": [
        [
          {
            "node": "Log Scan Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF - VT Analysis Error?": {
      "main": [
        [
          {
            "node": "Respond - VT Service Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "IF - VirusTotal Analysis Completed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Increment Retry Counter": {
      "main": [
        [
          {
            "node": "IF Max Retry Reached?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait - Retry VT Analysis Poll": {
      "main": [
        [
          {
            "node": "VirusTotal - Get Scan Analysis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "VirusTotal - Get Scan Analysis": {
      "main": [
        [
          {
            "node": "IF - VT Analysis Error?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract VirusTotal Verdict Stats": {
      "main": [
        [
          {
            "node": "Build Phishing Verdict",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "VirusTotal - Submit URL for Scan": {
      "main": [
        [
          {
            "node": "IF - VT Submit Error?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait - VirusTotal Scan Processing": {
      "main": [
        [
          {
            "node": "VirusTotal - Get Scan Analysis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook - Submit URL for Analysis": {
      "main": [
        [
          {
            "node": "Normalize Input URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF - VirusTotal Analysis Completed?": {
      "main": [
        [
          {
            "node": "Extract VirusTotal Verdict Stats",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Increment Retry Counter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}