{
  "meta": {
    "instanceId": "",
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "",
      "name": "Form Input",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -1632,
        64
      ],
      "webhookId": "",
      "parameters": {
        "path": "",
        "options": {},
        "formTitle": "SerpAPI Google Recon Scanner",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Target Domain",
              "placeholder": "https://example.com",
              "requiredField": true
            },
            {
              "fieldType": "dropdown",
              "fieldLabel": "Scan Depth",
              "fieldOptions": {
                "values": [
                  {
                    "option": "Quick (Basic dorks only)"
                  },
                  {
                    "option": "Standard (All categories)"
                  },
                  {
                    "option": "Deep (Includes validation)"
                  }
                ]
              },
              "requiredField": true
            },
            {
              "fieldType": "checkbox",
              "fieldLabel": "Authorization Confirmed",
              "fieldOptions": {
                "values": [
                  {}
                ]
              },
              "requiredField": true
            }
          ]
        },
        "formDescription": "Authorized domain scan with categorized Google dorks via SerpAPI"
      },
      "typeVersion": 2
    },
    {
      "id": "",
      "name": "Generate Categorized Dorks",
      "type": "n8n-nodes-base.code",
      "position": [
        -1408,
        64
      ],
      "parameters": {
        "jsCode": "const inputUrl = $json[\"Target Domain\"];\nconst scanDepth = $json[\"Scan Depth\"];\nconst authorized = $json[\"Authorization Confirmed\"];\n\nif (!authorized) {\n  throw new Error('You must confirm authorization to scan this domain');\n}\n\nconst match = inputUrl.match(/^(?:https?:\\/\\/)?(?:www\\.)?([^\\/]+)/i);\nconst domain = match ? match[1] : 'example.com';\n\nconst dorkCategories = {\n  basic: [\n    { category: 'Sensitive Files', severity: 'HIGH', dork: 'site:.example.com ext:pdf intext:invoice | intext:confidential' },\n    { category: 'Backup Files', severity: 'CRITICAL', dork: 'site:.example.com ext:sql | ext:bak | ext:old' },\n    { category: 'Config Files', severity: 'CRITICAL', dork: 'site:.example.com (ext:json | ext:log | ext:txt | ext:conf | ext:env)' },\n    { category: 'Login Pages', severity: 'MEDIUM', dork: 'site:.example.com intitle:login | intitle:\"sign in\" | inurl:login' },\n    { category: 'Directory Listings', severity: 'MEDIUM', dork: 'site:.example.com intitle:\"Index of /\"' },\n    { category: 'Git Exposure', severity: 'CRITICAL', dork: 'site:.example.com inurl:/.git/config' },\n    { category: 'PHP Info', severity: 'HIGH', dork: 'site:.example.com intitle:\"phpinfo\" intext:\"HTTP_HOST\"' },\n    { category: 'Upload Forms', severity: 'MEDIUM', dork: 'site:.example.com intext:\"Choose file\"' }\n  ],\n  advanced: [\n    { category: 'API Keys', severity: 'CRITICAL', dork: 'site:.example.com intext:\"api_key\" | intext:\"apikey\" | intext:\"api-key\"' },\n    { category: 'Secrets', severity: 'CRITICAL', dork: 'site:.example.com intext:\"secret_key\" | intext:\"client_secret\" | intext:\"private_key\"' },\n    { category: 'Database Dumps', severity: 'CRITICAL', dork: 'site:.example.com ext:sql intext:\"INSERT INTO\" | intext:\"CREATE TABLE\"' },\n    { category: 'Admin Panels', severity: 'HIGH', dork: 'site:.example.com inurl:admin | inurl:administrator | inurl:wp-admin' },\n    { category: 'Server Info', severity: 'MEDIUM', dork: 'site:.example.com intitle:\"server status\" | intitle:\"apache status\"' },\n    { category: 'Subdomains', severity: 'INFO', dork: 'site:*.example.com -www' },\n    { category: 'Email Addresses', severity: 'INFO', dork: 'site:.example.com intext:\"@example.com\"' },\n    { category: 'Error Messages', severity: 'MEDIUM', dork: 'site:.example.com intext:\"error\" | intext:\"warning\" | intext:\"fatal\"' },\n    { category: 'Cloud Storage', severity: 'HIGH', dork: 'site:s3.amazonaws.com \"example.com\"' },\n    { category: 'Internal IPs', severity: 'MEDIUM', dork: 'site:.example.com intext:\"192.168.\" | intext:\"10.0.\"' }\n  ]\n};\n\nlet selectedDorks = [...dorkCategories.basic];\n\nif (scanDepth.includes('Standard') || scanDepth.includes('Deep')) {\n  selectedDorks = [...dorkCategories.basic, ...dorkCategories.advanced];\n}\n\nconst customizedDorks = selectedDorks.map(dorkObj => {\n  const customized = dorkObj.dork.replace(/\\.example\\.com/g, `.${domain}`).replace(/\"example\\.com\"/g, `\"${domain}\"`);\n  return {\n    json: {\n      dork: customized,\n      category: dorkObj.category,\n      severity: dorkObj.severity,\n      domain: domain,\n      targetDomain: domain\n    }\n  };\n});\n\nreturn customizedDorks;"
      },
      "typeVersion": 2
    },
    {
      "id": "",
      "name": "Split Into Batches",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -1184,
        64
      ],
      "parameters": {
        "options": {},
        "batchSize": 5
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Rate Limit Delay",
      "type": "n8n-nodes-base.wait",
      "position": [
        -960,
        64
      ],
      "webhookId": "rate-limit-delay",
      "parameters": {
        "unit": "seconds",
        "amount": 2
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Flatten Serper Results",
      "type": "n8n-nodes-base.code",
      "position": [
        -512,
        64
      ],
      "parameters": {
        "jsCode": "const serperData = $json;\nconst batchItem = $input.first(); // More reliable than $('Split Into Batches')\nconst dorkMeta = batchItem?.json || {};\n\nif (!dorkMeta.dork) {\n  return [{\n    json: {\n      error: \"Missing dork metadata – check batching\",\n      rawInput: batchItem\n    }\n  }];\n}\n\n// Helper: Clean snippet (remove \"...\" and Google junk)\nconst cleanSnippet = (text = '') => \n  text\n    .replace(/…/g, '...')\n    .replace(/ - \\d+ - /g, ' – ')\n    .trim()\n    .substring(0, 500);\n\n// Helper: Risk score\nconst getRiskScore = (severity) => {\n  switch (severity) {\n    case 'CRITICAL': return 10;\n    case 'HIGH':     return 8;\n    case 'MEDIUM':   return 5;\n    case 'INFO':     return 2;\n    default:         return 3;\n  }\n};\n\nconst results = serperData.organic ?? [];\n\n// Deduplicate by URL (Serper sometimes repeats)\nconst seenUrls = new Set();\nconst enriched = [];\n\nfor (const result of results) {\n  const url = result.link?.trim();\n  if (!url || !url.startsWith('http') || seenUrls.has(url)) {\n    continue; // Skip invalid or duplicate\n  }\n  seenUrls.add(url);\n\n  enriched.push({\n    json: {\n      url,\n      title: (result.title || 'No title').replace(/ - [^-]+$/g, ''), // Remove trailing domain\n      snippet: cleanSnippet(result.snippet),\n      category: dorkMeta.category,\n      severity: dorkMeta.severity,\n      riskScore: getRiskScore(dorkMeta.severity),\n      domain: dorkMeta.domain,\n      targetDomain: dorkMeta.targetDomain,\n      dorkUsed: dorkMeta.dork,\n      position: result.position ?? enriched.length + 1,\n      cached: !!result.cache, // True if from Google cache\n      timestamp: new Date().toISOString(),\n      accessible: null,     // Will be filled in Deep mode\n      statusCode: null\n    }\n  });\n}\n\n// If no results → still emit a clean \"zero findings\" item for reporting\nif (enriched.length === 0) {\n  const reason = !results.length \n    ? 'No results from Serper' \n    : 'All results filtered (invalid/duplicate URLs)';\n\n  enriched.push({\n    json: {\n      url: null,\n      title: 'No findings',\n      snippet: `Serper returned ${results.length} results → ${reason}`,\n      category: dorkMeta.category,\n      severity: dorkMeta.severity,\n      riskScore: 0,\n      domain: dorkMeta.domain,\n      targetDomain: dorkMeta.targetDomain,\n      dorkUsed: dorkMeta.dork,\n      findingsCount: 0,\n      timestamp: new Date().toISOString()\n    }\n  });\n}\n\nreturn enriched;"
      },
      "typeVersion": 2
    },
    {
      "id": "",
      "name": "Clean Output",
      "type": "n8n-nodes-base.code",
      "position": [
        -288,
        64
      ],
      "parameters": {
        "jsCode": "return $input.all()\n  .map(item => {\n    const data = item.json;\n\n    // Skip zero-findings placeholder items (they have url: null)\n    if (!data.url) {\n      return { json: data }; // Pass through \"No findings\" items untouched\n    }\n\n    const url = data.url.trim();\n\n    // Final sanity checks – eliminate Google noise & invalid URLs\n    const isValid =\n      url &&\n      (url.startsWith('http://') || url.startsWith('https://')) &&\n      !url.includes('google.com') &&\n      !url.includes('googleusercontent.com') &&\n      !url.includes('/search?q=') &&\n      !url.startsWith('/url?') &&\n      !url.startsWith('#') &&\n      !url.includes('webcache.googleusercontent.com');\n\n    if (!isValid) {\n      return null; // Drop junk\n    }\n\n    // Final enriched item – ready for reporting\n    return {\n      json: {\n        ...data,\n        cleanedLink: url,\n        // Add a short domain for quick visual scanning in reports\n        displayDomain: new URL(url).hostname.replace(/^www\\./, ''),\n        // Final risk badge for Slack/Markdown\n        riskBadge: data.severity === 'CRITICAL' ? 'CRITICAL'\n                 : data.severity === 'HIGH'     ? 'HIGH'\n                 : data.severity === 'MEDIUM'   ? 'MEDIUM'\n                 : 'INFO'\n      }\n    };\n  })\n  .filter(Boolean); // Remove nulls"
      },
      "typeVersion": 2
    },
    {
      "id": "",
      "name": "Generate Report",
      "type": "n8n-nodes-base.code",
      "position": [
        -64,
        64
      ],
      "parameters": {
        "jsCode": "const allFindings = $input.all();\n\nconst bySeverity = {\n  CRITICAL: [],\n  HIGH: [],\n  MEDIUM: [],\n  INFO: []\n};\n\nallFindings.forEach(item => {\n  const severity = item.json.severity || 'INFO';\n  if (item.json.url) {\n    bySeverity[severity].push(item.json);\n  }\n});\n\nlet markdown = `# GrokDork Security Recon Report\\n\\n`;\nmarkdown += `**Target:** ${allFindings[0]?.json.targetDomain || 'N/A'} | **Date:** ${new Date().toISOString().split('T')[0]}\\n`;\nmarkdown += `**Total Findings:** ${allFindings.length} | **Dorks Run:** ${Math.ceil(allFindings.length / 10)}\\n\\n`;\n\nmarkdown += `## Summary\\n`;\nmarkdown += `- 🔴 Critical: ${bySeverity.CRITICAL.length}\\n`;\nmarkdown += `- 🟠 High: ${bySeverity.HIGH.length}\\n`;\nmarkdown += `- 🟡 Medium: ${bySeverity.MEDIUM.length}\\n`;\nmarkdown += `- 🔵 Info: ${bySeverity.INFO.length}\\n\\n`;\n\n['CRITICAL', 'HIGH', 'MEDIUM', 'INFO'].forEach(severity => {\n  if (bySeverity[severity].length === 0) return;\n\n  const icon = severity === 'CRITICAL' ? '🔴' : severity === 'HIGH' ? '🟠' : severity === 'MEDIUM' ? '🟡' : '🔵';\n  markdown += `## ${icon} ${severity} Findings\\n\\n`;\n\n  const byCategory = {};\n  bySeverity[severity].forEach(finding => {\n    const cat = finding.category || 'Uncategorized';\n    if (!byCategory[cat]) byCategory[cat] = [];\n    byCategory[cat].push(finding);\n  });\n\n  Object.keys(byCategory).forEach(category => {\n    markdown += `### ${category} (${byCategory[category].length})\\n\\n`;\n    byCategory[category].forEach((finding, idx) => {\n      markdown += `${idx + 1}. [${finding.title}](${finding.cleanedLink || finding.url})\\n`;\n      markdown += `   Snippet: ${finding.snippet.substring(0, 200)}...\\n`;\n      if (finding.statusCode) markdown += `   Status: ${finding.statusCode}\\n`;\n      if (!finding.accessible) markdown += `   ⚠️ Not Accessible\\n`;\n      markdown += `\\n`;\n    });\n  });\n});\n\nmarkdown += `\\n---\\n*Authorized testing only. Powered by Serper.dev & GrokDork.*\\n`;\n\nconst jsonReport = {\n  target: allFindings[0]?.json.targetDomain,\n  scanDate: new Date().toISOString(),\n  summary: {\n    total: allFindings.length,\n    critical: bySeverity.CRITICAL.length,\n    high: bySeverity.HIGH.length,\n    medium: bySeverity.MEDIUM.length,\n    info: bySeverity.INFO.length\n  },\n  findings: allFindings.map(f => ({...f.json, riskScore: f.json.severity === 'CRITICAL' ? 10 : f.json.severity === 'HIGH' ? 7 : 3 }))\n};\n\nreturn [{\n  json: {\n    markdown: markdown,\n    reportData: jsonReport,\n    criticalCount: bySeverity.CRITICAL.length,\n    highCount: bySeverity.HIGH.length\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "",
      "name": "Check If Alerting Needed",
      "type": "n8n-nodes-base.if",
      "position": [
        160,
        64
      ],
      "parameters": {
        "conditions": {
          "number": [
            {
              "value1": "={{ $json.criticalCount + $json.highCount }}",
              "operation": "larger"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Google search",
      "type": "n8n-nodes-serpapi.serpApi",
      "position": [
        -736,
        64
      ],
      "parameters": {
        "q": "=={{ $json.dork }}",
        "requestOptions": {},
        "additionalFields": {}
      },
      "credentials": {
        "serpApi": {
          "id": "",
          "name": "SerpApi account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Convert Markdown To PDF",
      "type": "n8n-nodes-pdf4me.PDF4me",
      "position": [
        384,
        160
      ],
      "parameters": {
        "resource": "convert",
        "operation": "Convert Markdown To PDF",
        "markdownCode": "={{ $json.markdown }}{{ $json.reportData }}",
        "inputDataType": "markdownCode",
        "outputFileName": "={{ $json.reportData.findings[0].rawInput.json.inline_images[0].source_name }}.pdf",
        "advancedOptions": {}
      },
      "credentials": {
        "pdf4meApi": {
          "id": "",
          "name": "PDF4ME account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Convert Markdown To PDF1",
      "type": "n8n-nodes-pdf4me.PDF4me",
      "position": [
        384,
        -32
      ],
      "parameters": {
        "resource": "convert",
        "operation": "Convert Markdown To PDF",
        "markdownCode": "={{ $json.markdown }}{{ $json.reportData }}",
        "inputDataType": "markdownCode",
        "outputFileName": "={{ $json.reportData.findings[0].rawInput.json.inline_images[0].source_name }}.pdf",
        "advancedOptions": {}
      },
      "credentials": {
        "pdf4meApi": {
          "id": "",
          "name": "PDF4ME account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -784,
        -32
      ],
      "parameters": {
        "width": 192,
        "height": 256,
        "content": "## Step 1\n**Double click** to edit and configure SerpAPI"
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        320,
        -224
      ],
      "parameters": {
        "width": 224,
        "height": 544,
        "content": "## Step 2.\n**Double click** to edit and configure your PDF4Me API Key Credentials\n\nDouble Click either node to download corresponding PDF if available"
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Send a message",
      "type": "n8n-nodes-base.gmail",
      "position": [
        608,
        -32
      ],
      "webhookId": "",
      "parameters": {
        "options": {}
      },
      "credentials": {
        "gmailOAuth2": {
          "id": "",
          "name": "Gmail account"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        544,
        -144
      ],
      "parameters": {
        "width": 224,
        "height": 272,
        "content": "## Step 3. (optional) \n**Double click** to edit and configure email settings and credentials"
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2080,
        -464
      ],
      "parameters": {
        "width": 400,
        "height": 576,
        "content": "## Google Dorks with SerpAPI\n\nHow it Works:\n- Accepts a domain from a web form\n- Generates a list of Google dorks targeting that domain\n- Scrapes Google search results for each dork using SerpAPI\n- Filters out junk links (Google internal, non-http)\n- Formats valid results as a markdown report\n- Converts the report to PDF using PDF4me\n- Emails the report to your inbox with Gmail\n\n----\n\n# How To Use:\nStep 1. Double Click the SerpAPI node and configure the API key\nStep 2. Double Click each PDF4me node and configure the API key\nStep 3. (Optional) Souble Click and configure your OAuth credentials and gmail settings on the Gmail node.\nStep 4. Click 'Execute Workflow' and wait for your report to be generated."
      },
      "typeVersion": 1
    }
  ],
  "pinData": {},
  "connections": {
    "Form Input": {
      "main": [
        [
          {
            "node": "Generate Categorized Dorks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean Output": {
      "main": [
        [
          {
            "node": "Generate Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google search": {
      "main": [
        [
          {
            "node": "Flatten Serper Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Report": {
      "main": [
        [
          {
            "node": "Check If Alerting Needed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rate Limit Delay": {
      "main": [
        [
          {
            "node": "Google search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Into Batches": {
      "main": [
        [
          {
            "node": "Rate Limit Delay",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flatten Serper Results": {
      "main": [
        [
          {
            "node": "Clean Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert Markdown To PDF": {
      "main": [
        []
      ]
    },
    "Check If Alerting Needed": {
      "main": [
        [
          {
            "node": "Convert Markdown To PDF1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Convert Markdown To PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert Markdown To PDF1": {
      "main": [
        [
          {
            "node": "Send a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Categorized Dorks": {
      "main": [
        [
          {
            "node": "Split Into Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}