Workflow: 0808_Code_Form_Send_Webhook
Integrations: aggregate, code, formTrigger, gmail, httpRequest, merge, stickyNote

{"meta": {"instanceId": "c2589fa234defe76e8a1321c3a7d0a73579d0120d64d927e88f5e3be584ae8d4"}, "nodes": [{"id": "634f2fc5-0ba7-42ad-bdf5-ade3415dd288", "name": "Landing Page Url", "type": "n8n-nodes-base.formTrigger", "position": [-200, 580], "webhookId": "afe067a5-4878-4c9d-b746-691f77190f54", "parameters": {"options": {}, "formTitle": "Website Security Scanner", "formFields": {"values": [{"fieldLabel": "Landing Page Url", "placeholder": "https://example.com", "requiredField": true}]}, "formDescription": "Check your website for security vulnerabilities and get a detailed report"}, "typeVersion": 2.2}, {"id": "6cee63ca-d0f6-444a-b882-22da1a9fd70c", "name": "Scrape Website", "type": "n8n-nodes-base.httpRequest", "position": [0, 580], "parameters": {"url": "={{ $json['Landing Page Url'] }}", "options": {"redirect": {"redirect": {"maxRedirects": 5}}, "response": {"response": {"fullResponse": true, "responseFormat": "text"}}}}, "typeVersion": 4.2}, {"id": "0d5d1e76-e627-4565-a1ee-6a610f4b2028", "name": "OpenAI Headers Analysis", "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", "position": [340, 600], "parameters": {"model": {"__rl": true, "mode": "list", "value": "gpt-4o-mini", "cachedResultName": "gpt-4o-mini"}, "options": {}}, "credentials": {"openAiApi": {"id": "yZ0AIg9abV8HJadB", "name": "OpenAi account"}}, "typeVersion": 1.2}, {"id": "04427ef7-515d-4a1a-88d2-ade10aeefc87", "name": "OpenAI Content Analysis", "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", "position": [340, 980], "parameters": {"model": {"__rl": true, "mode": "list", "value": "gpt-4o-mini", "cachedResultName": "gpt-4o-mini"}, "options": {}}, "credentials": {"openAiApi": {"id": "yZ0AIg9abV8HJadB", "name": "OpenAi account"}}, "typeVersion": 1.2}, {"id": "d4ee4db8-aa04-4068-9b97-d16acf98c027", "name": "Security Vulnerabilities Audit", "type": "@n8n/n8n-nodes-langchain.agent", "position": [360, 780], "parameters": {"text": "=You are an elite cybersecurity expert specializing in web application security.\n\nIn this task, you will analyze the HTML and visible content of the webpage to identify potential security vulnerabilities.\n\nAudit Structure\nYou will review all client-side security aspects of the page and present your findings in three sections:\n- Critical Vulnerabilities \u2013 Issues that could lead to immediate compromise\n- Information Leakage \u2013 Sensitive data exposed in page source\n- Client-Side Weaknesses \u2013 JavaScript vulnerabilities, XSS opportunities, etc.\n\nFor each issue found, provide:\n1. A clear description of the vulnerability\n2. The potential impact\n3. A specific recommendation to fix it\n\nIf you find no issues in a particular section, explicitly state that no issues were found in that category.\n\nEnsure the output is properly formatted, clean, and highly readable. Focus only on issues that can be detected from the client-side code.\n\nHere is the content of the webpage: {{ $json.data }}", "options": {}, "promptType": "define"}, "typeVersion": 1.7}, {"id": "c9702f2b-845b-464d-9c32-3d5be308ef77", "name": "Security Configuration Audit", "type": "@n8n/n8n-nodes-langchain.agent", "position": [360, 380], "parameters": {"text": "=You are an elite web security expert specializing in secure configurations.\n\nIn this task, you will analyze the HTTP headers, cookies, and overall configuration of a webpage to identify security misconfigurations.\n\nAudit Structure\nYou will begin by listing ALL security headers that ARE present and properly configured.\n\nBe very clear and explicit about which headers are present and which are missing. For each header, clearly state whether it is present or missing, and if present, what its value is.\n\nThen, present your findings in three sections:\n- Header Security \u2013 Missing or misconfigured security headers\n- Cookie Security \u2013 Insecure cookie configurations\n- Content Security \u2013 CSP issues, mixed content, etc.\n\nFor each finding, provide:\n1. A clear description of the misconfiguration\n2. The security implications\n3. The recommended secure configuration with example code\n\nIf you find no issues in a particular section, explicitly state that no issues were found.\n\nUse proper formatting with code blocks for configuration examples. Only include issues that can be detected from client-side inspection.\nHere are the response headers: {{ $json.formattedHeaders }}\n\nPlease Respond like this\n\n### [any section heading that includes \"Headers]\n\n1. **[Header Title]**\n   - **Present?** Yes/No\n   - **Value:** `actual-header-value`\n", "options": {}, "promptType": "define"}, "typeVersion": 1.7}, {"id": "3b43be75-c35c-44e4-8ecc-a29c48e3625c", "name": "Merge Security Results", "type": "n8n-nodes-base.merge", "position": [860, 580], "parameters": {}, "typeVersion": 3, "alwaysOutputData": true}, {"id": "da134256-d7fa-4a3f-ba24-acc320a944a2", "name": "Aggregate Audit Results", "type": "n8n-nodes-base.aggregate", "position": [1060, 580], "parameters": {"options": {}, "fieldsToAggregate": {"fieldToAggregate": [{"fieldToAggregate": "output"}]}}, "typeVersion": 1}, {"id": "aef1da93-0b01-4a7f-9439-1f74c2af12d6", "name": "Process Audit Results", "type": "n8n-nodes-base.code", "position": [1240, 580], "parameters": {"jsCode": "// \u2705 Updated extractSecurityHeaders and related logic remains unchanged\n\nfunction extractSecurityHeaders(rawHeaders = {}, configOutput = '') {\n  const securityHeaders = [\n    'Content-Security-Policy',\n    'Strict-Transport-Security',\n    'X-Content-Type-Options',\n    'X-Frame-Options',\n    'Referrer-Policy',\n    'Permissions-Policy',\n    'X-XSS-Protection',\n    'Cross-Origin-Embedder-Policy',\n    'Cross-Origin-Opener-Policy',\n    'X-Permitted-Cross-Domain-Policies'\n  ];\n\n  const headerStatus = {};\n  for (const header of securityHeaders) {\n    headerStatus[header] = { present: false, value: '' };\n  }\n\n  for (const header in rawHeaders) {\n    const norm = header.trim().toLowerCase();\n    for (const standard of securityHeaders) {\n      if (norm === standard.toLowerCase()) {\n        headerStatus[standard].present = true;\n        headerStatus[standard].value = rawHeaders[header];\n      }\n    }\n  }\n\n  const presentSection = configOutput.match(/(?:###|##|\\*\\*)[^\\n]*?\\bheaders?\\b[\\s\\S]*?(?=###|##|\\*\\*|$)/i);\n  if (presentSection) {\n    const section = presentSection[0];\n    for (const header of securityHeaders) {\n      const title = header.replace(/-/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase());\n      const regex = new RegExp(`\\\\*\\\\*${title}\\\\*\\\\*[^\\\\n]*?\\\\*\\\\*Present\\\\?\\\\*\\\\*\\\\s*Yes[^\\\\n]*?\\\\*\\\\*Value:\\\\*\\\\*\\\\s*\\`([^\\\\\\`]+)\\``, 'is');\n      const match = section.match(regex);\n      if (match && match[1]) {\n        headerStatus[header].present = true;\n        headerStatus[header].value = match[1].trim();\n      }\n    }\n  }\n\n  return headerStatus;\n}\n\nfunction hasUnsafeInline(value) {\n  return value && value.includes('unsafe-inline');\n}\n\nfunction determineGrade(headerStatus) {\n  const critical = [\n    'Content-Security-Policy',\n    'Strict-Transport-Security',\n    'X-Content-Type-Options',\n    'X-Frame-Options'\n  ];\n  const important = ['Referrer-Policy', 'Permissions-Policy'];\n  const additional = [\n    'X-XSS-Protection',\n    'Cross-Origin-Embedder-Policy',\n    'Cross-Origin-Opener-Policy',\n    'X-Permitted-Cross-Domain-Policies'\n  ];\n\n  let criticalCount = 0;\n  let importantCount = 0;\n  let hasCSPIssue = false;\n\n  for (const h of critical) {\n    if (headerStatus[h]?.present) {\n      criticalCount++;\n      if (h === 'Content-Security-Policy' && hasUnsafeInline(headerStatus[h].value)) {\n        hasCSPIssue = true;\n      }\n    }\n  }\n\n  for (const h of important) {\n    if (headerStatus[h]?.present) importantCount++;\n  }\n\n  if (criticalCount === critical.length) {\n    if (importantCount === important.length) return hasCSPIssue ? 'A-' : 'A+';\n    if (importantCount >= 1) return hasCSPIssue ? 'B+' : 'A-';\n    return hasCSPIssue ? 'B' : 'B+';\n  } else if (criticalCount >= critical.length - 1) {\n    return importantCount >= 1 ? 'B' : 'C+';\n  } else if (criticalCount >= 2) {\n    return 'C';\n  } else if (criticalCount >= 1) {\n    return 'D';\n  } else {\n    return 'F';\n  }\n}\n\nfunction formatHeadersForDisplay(headerStatus) {\n  const present = Object.keys(headerStatus).filter(h => headerStatus[h].present);\n  return present.length > 0 ? present.join(', ') : 'No security headers detected';\n}\n\nfunction processSecurityHeaders(items) {\n  try {\n    const json = items[0].json || items[0];\n\n    // \u26cf\ufe0f Try to grab from originalHeaders if available\n    const rawHeaders =\n      json?.originalHeaders ||\n      $('Extract Headers for Debug')?.first()?.json?.originalHeaders ||\n      json?.headers ||\n      {};\n\n    const configOutput = json.configOutput || json.output?.[0] || '';\n    const vulnOutput = json.vulnOutput || json.output?.[1] || '';\n\n    const headerStatus = extractSecurityHeaders(rawHeaders, configOutput);\n    const presentHeaders = formatHeadersForDisplay(headerStatus);\n    const grade = determineGrade(headerStatus);\n\n    const timestamp = new Date().toLocaleString('en-US', {\n      year: 'numeric',\n      month: 'long',\n      day: 'numeric',\n      hour: '2-digit',\n      minute: '2-digit'\n    });\n\n    const url =\n      json?.formValues?.url ||\n      json?.['Landing Page Url'] ||\n      $('Landing Page Url')?.first()?.json?.['Landing Page Url'] ||\n      json?.Landing_Page_Url ||\n      json?.landingPageUrl ||\n      json?.url ||\n      'https://example.com';\n\n    return [\n      {\n        json: {\n          ...json,\n          auditData: {\n            url,\n            timestamp,\n            grade,\n            criticalCount:\n              headerStatus['Content-Security-Policy'].present &&\n              hasUnsafeInline(headerStatus['Content-Security-Policy'].value)\n                ? 1\n                : 0,\n            warningCount: Object.keys(headerStatus).filter(\n              h =>\n                !headerStatus[h].present &&\n                !['Strict-Transport-Security', 'Content-Security-Policy'].includes(h)\n            ).length,\n            presentHeaders,\n            configOutput,\n            vulnOutput,\n            headerStatus,\n            originalHeaders: rawHeaders\n          }\n        }\n      }\n    ];\n  } catch (err) {\n    return [{ json: { ...items[0].json, error: err.message } }];\n  }\n}\n\nreturn processSecurityHeaders(items);\n"}, "typeVersion": 2}, {"id": "ced29b26-474c-4d62-808a-3284103c9d60", "name": "Send Security Report", "type": "n8n-nodes-base.gmail", "position": [1580, 580], "webhookId": "2979e4dc-1689-447e-8cd4-eb907b4eedf4", "parameters": {"sendTo": "=example@here.com", "message": "={{ $json.emailHtml }}", "options": {}, "subject": "=Website Security Audit - {{ $json.auditData.url }}"}, "credentials": {"gmailOAuth2": {"id": "9CEpbF4jIWb2OETv", "name": "Gmail account"}}, "typeVersion": 2.1}, {"id": "918c0fc4-2f02-4594-bfc9-e36035f2d802", "name": "Sticky Note - Setup Instructions", "type": "n8n-nodes-base.stickyNote", "position": [-820, 400], "parameters": {"width": 500, "height": 440, "content": "## Quick Setup Guide\n\n1. **Add OpenAI API Credentials**\n   - Go to Settings \u2192 Credentials \u2192 New \u2192 OpenAI API\n   - Enter your API key from platform.openai.com\n\n2. **Add Gmail Credentials**\n   - Go to Settings \u2192 Credentials \u2192 New \u2192 Gmail OAuth2 API\n   - Complete the OAuth setup process\n\n3. **Update Email Configuration**\n   - Open the 'Send Security Report' node\n   - Change the recipient email address from the default\n\n4. **Activate and Deploy Workflow**\n   - Click 'Active' toggle in the top right\n   - Copy the form URL to share with others or use yourself"}, "typeVersion": 1}, {"id": "6e31b9b8-ae02-4da4-a75e-5d784b210c64", "name": "Sticky Note - OpenAI Analysis", "type": "n8n-nodes-base.stickyNote", "position": [300, 120], "parameters": {"color": 3, "width": 420, "height": 240, "content": "## OpenAI Security Analysis\n\n- Add your OpenAI credentials (required)\n- Using GPT-4o models provides more detailed security analysis\n- Analyzes for XSS, information disclosure, CSRF, and more\n- Each agent scans different aspects of website security\n- Consider upgrading to GPT-4o (not mini) for production use"}, "typeVersion": 1}, {"id": "590b1f1c-024d-4002-a8eb-d9dc81528f89", "name": "Sticky Note - Email Configuration", "type": "n8n-nodes-base.stickyNote", "position": [1480, 220], "parameters": {"color": 3, "width": 360, "height": 200, "content": "## Send Security Report\n\n- Connects securely to Gmail for sending detailed reports\n- Report is sent as HTML formatted email\n- Subject line includes the scanned URL\n- Requires Gmail OAuth credentials to be set up"}, "typeVersion": 1}, {"id": "dc6223f8-a98c-497a-97c9-af39e80e6d66", "name": "Sticky Note - Audit Process", "type": "n8n-nodes-base.stickyNote", "position": [-200, 780], "parameters": {"color": 2, "width": 420, "height": 300, "content": "## Security Audit Process\n\n- This workflow performs two parallel security analyses\n- Top path: Checks headers, cookies, and security configurations\n- Bottom path: Analyzes HTML/JavaScript for client-side vulnerabilities\n- Results are merged and formatted into a comprehensive report\n- Analysis is non-invasive and only examines client-side content"}, "typeVersion": 1}, {"id": "cbda16d4-f1f4-491c-b38c-43d7544e129b", "name": "Sticky Note - How To Use", "type": "n8n-nodes-base.stickyNote", "position": [-240, 240], "parameters": {"color": 4, "width": 400, "height": 280, "content": "## How To Use This Workflow\n\n1. **Deploy the workflow** and activate it\n2. **Access the form** via the provided URL\n3. **Enter any website URL** to scan (must include http:// or https://)\n4. **Submit the form** to trigger the analysis\n5. **Check your email** for the detailed security report\n6. **Share the results** with your development team to implement fixes"}, "typeVersion": 1}, {"id": "4859416f-4de3-43ea-9461-3ead8a38db6e", "name": "Sticky Note - Report Formatting", "type": "n8n-nodes-base.stickyNote", "position": [1160, 220], "parameters": {"color": 5, "width": 300, "height": 280, "content": "## Report Formatting\n\n- Creates beautiful, professional HTML email report\n- Visual grade indicator (A-F) based on findings\n- Includes count of critical issues and warnings\n- Color-coded sections for easy readability\n- Mobile-friendly responsive design"}, "typeVersion": 1}, {"id": "a02db4c7-2cad-41ff-b5ad-e1b19604a699", "name": "Sticky Note - Results Processing", "type": "n8n-nodes-base.stickyNote", "position": [840, 240], "parameters": {"width": 300, "height": 240, "content": "## Results Processing\n\n- Analyzes AI output to determine security grade\n- Counts critical issues and warnings\n- Extracts present security headers\n- Prepares data for the email report template\n- Generates timestamp for the report"}, "typeVersion": 1}, {"id": "41b834c8-62f7-47e7-9d9d-e0e1244faecb", "name": "Extract Headers for Debug", "type": "n8n-nodes-base.code", "position": [200, 460], "parameters": {"jsCode": "// Format headers into a readable string\nlet formattedHeaders = '';\nif (items[0].json.headers) {\n  for (const key in items[0].json.headers) {\n    formattedHeaders += `${key}: ${items[0].json.headers[key]}\\n`;\n  }\n}\n\n// Return both the original data and the formatted headers\nreturn [{\n  json: {\n    ...items[0].json,\n    formattedHeaders: formattedHeaders,\n    originalHeaders: items[0].json.headers // Keep the original headers too\n  }\n}];"}, "typeVersion": 2}, {"id": "0b76b396-fc96-41fc-a095-30971dd88271", "name": "convert to HTML", "type": "n8n-nodes-base.code", "position": [1400, 580], "parameters": {"jsCode": "// Create a direct HTML template with improved styling\nconst auditData = items[0].json.auditData;\n\nfunction formatConfigurationIssues() {\n  if (!auditData.configOutput || auditData.configOutput.trim() === '') {\n    return '<p>No specific configuration issues detected.</p>';\n  }\n\n  try {\n    const config = auditData.configOutput.trim();\n    let html = '';\n    const renderedKeys = new Set();\n\n    const renderBlock = (title, description, impact, recommendation) => `\n      <div style=\"border-left: 4px solid #3498DB; padding: 10px; margin-bottom: 15px;\">\n        <div style=\"font-weight: bold; color: #3498DB;\">${title}</div>\n        ${description ? `<div style=\"margin-top: 5px;\">${description}</div>` : ''}\n        ${impact ? `<div style=\"margin-top: 5px; font-style: italic; color: #7F8C8D;\">Impact: ${impact}</div>` : ''}\n        ${recommendation ? `<div style=\"margin-top: 5px;\"><strong>Recommendation:</strong></div>\n          <pre style=\"background-color: #f8f9fa; padding: 10px; border-radius: 5px; overflow-x: auto; font-family: monospace;\">${recommendation}</pre>` : ''}\n      </div>`;\n\n    const sections = config.split(/(?=^###\\s+)/gm).filter(Boolean);\n\n    for (const section of sections) {\n      const sectionTitleMatch = section.match(/^###\\s+(.*)/);\n      const sectionTitle = sectionTitleMatch?.[1]?.trim() || 'Unnamed Section';\n      const sectionKey = sectionTitle.toLowerCase();\n\n      // Skip \"no issues found\" sections\n      if (/no issues? (found|were found)/i.test(section)) continue;\n\n      const lines = section.split(/\\n+/).filter(line => line.trim() !== '');\n\n      let currentTitle = '';\n      let description = '';\n      let impact = '';\n      let recommendation = '';\n\n      for (let i = 0; i < lines.length; i++) {\n        const line = lines[i].trim();\n\n        // Start of a new numbered or bolded issue\n        const numberedTitle = line.match(/^\\d+\\.\\s+\\*\\*(.*?)\\*\\*/);\n        const bulletTitle = line.match(/^\\*\\*(.*?)\\*\\*/);\n\n        if (numberedTitle || (!currentTitle && bulletTitle)) {\n          // Flush last block\n          if (currentTitle && !renderedKeys.has(`${sectionKey}::${currentTitle.toLowerCase()}`)) {\n            html += renderBlock(currentTitle, description, impact, recommendation);\n            renderedKeys.add(`${sectionKey}::${currentTitle.toLowerCase()}`);\n          }\n\n          currentTitle = (numberedTitle || bulletTitle)[1].trim();\n          description = '';\n          impact = '';\n          recommendation = '';\n          continue;\n        }\n\n        const valueMatch = line.match(/- \\*\\*Value:\\*\\*\\s*`?(.*?)`?$/i);\n        const presentMatch = line.match(/- \\*\\*Present\\?\\*\\*.*?(Yes|No)/i);\n        const descMatch = line.match(/- \\*\\*Description:\\*\\*\\s*(.*)/i);\n        const impactMatch = line.match(/- \\*\\*(?:Impact|Security Implication|Potential Impact):\\*\\*\\s*(.*)/i);\n        const recMatch = line.match(/```(?:\\w*)?\\n([\\s\\S]*?)```/i);\n\n        if (descMatch) {\n          description = descMatch[1].trim();\n        } else if (valueMatch || presentMatch) {\n          const present = presentMatch?.[1]?.trim() || 'Unknown';\n          const value = valueMatch?.[1]?.trim() || '[Not provided]';\n          description = `This header is ${present.toLowerCase()}. Value: ${value}.`;\n        }\n\n        if (impactMatch) {\n          impact = impactMatch[1].trim();\n        }\n\n        if (recMatch) {\n          recommendation = recMatch[1].trim();\n        }\n      }\n\n      // Final block in section\n      if (currentTitle && !renderedKeys.has(`${sectionKey}::${currentTitle.toLowerCase()}`)) {\n        html += renderBlock(currentTitle, description, impact, recommendation);\n        renderedKeys.add(`${sectionKey}::${currentTitle.toLowerCase()}`);\n      }\n    }\n\n    return html || '<p>No configuration issues detected.</p>';\n  } catch (e) {\n    console.error('Error in formatConfigurationIssues:', e);\n    return `<p>Error processing configuration issues: ${e.message}</p>`;\n  }\n}\n\n\n\n// Create header badge HTML\nfunction createHeaderBadge(headerName, isWarning = false) {\n  const isPresent = auditData.headerStatus && \n                   auditData.headerStatus[headerName] && \n                   auditData.headerStatus[headerName].present;\n  \n  const color = isWarning && isPresent ? \"#F39C12\" : (isPresent ? \"#27AE60\" : \"#E74C3C\");\n  const icon = isPresent ? \"\u2713\" : \"\u2717\";\n  \n  return `<span style=\"display: inline-block; margin: 2px; padding: 4px 8px; background-color: ${color}; color: white; border-radius: 4px; font-size: 12px;\">${icon} ${headerName}</span>`;\n}\n\n// Format warnings section\nfunction formatWarningsSection() {\n  if (!auditData.warningCount || auditData.warningCount === 0 || !auditData.headerStatus) {\n    return '<p>No warnings detected.</p>';\n  }\n\n  const csp = Object.entries(auditData.headerStatus).find(([k]) => k.toLowerCase() === 'content-security-policy');\n  const hsts = Object.entries(auditData.headerStatus).find(([k]) => k.toLowerCase() === 'strict-transport-security');\n  const xss = Object.entries(auditData.headerStatus).find(([k]) => k.toLowerCase() === 'x-xss-protection');\n\n  let warnings = '';\n\n  if (csp && csp[1].value && csp[1].value.includes('unsafe-inline')) {\n    warnings += `\n      <div style=\"margin-top: 15px;\">\n        <div style=\"border-left: 4px solid #F39C12; padding: 10px;\">\n          <strong style=\"color: #F39C12;\">Content-Security-Policy: unsafe-inline</strong>\n          <p>The use of 'unsafe-inline' allows potentially malicious scripts to execute.</p>\n        </div>\n      </div>`;\n  }\n\n  if (hsts && hsts[1].value) {\n    const match = hsts[1].value.match(/max-age=(\\d+)/);\n    const age = match ? parseInt(match[1]) : 0;\n    if (age < 2592000) {\n      warnings += `\n        <div style=\"margin-top: 15px;\">\n          <div style=\"border-left: 4px solid #F39C12; padding: 10px;\">\n            <strong style=\"color: #F39C12;\">Strict-Transport-Security</strong>\n            <p>max-age is too low (${age}). Should be at least 2592000 (30 days).</p>\n          </div>\n        </div>`;\n    }\n  }\n\n  if (xss && !xss[1].present) {\n    warnings += `\n      <div style=\"margin-top: 15px;\">\n        <div style=\"border-left: 4px solid #F39C12; padding: 10px;\">\n          <strong style=\"color: #F39C12;\">Missing X-XSS-Protection</strong>\n          <p>This header enables the browser's XSS filter. Lack of it increases XSS risks.</p>\n        </div>\n      </div>`;\n  }\n\n  if (!warnings) {\n    warnings = `\n      <div style=\"margin-top: 15px;\">\n        <div style=\"border-left: 4px solid #F39C12; padding: 10px;\">\n          <strong style=\"color: #F39C12;\">${auditData.warningCount} warnings detected</strong>\n          <p>See the Configuration Issues section below for more info.</p>\n        </div>\n      </div>`;\n  }\n\n  return warnings;\n}\n\nfunction formatLongValue(value) {\n  if (!value || typeof value !== 'string') return '[empty]';\n\n  // Convert URLs into clickable links\n  value = value.replace(/(https?:\\/\\/[^\\s]+)/g, '<a href=\"$1\" style=\"color: #3498DB; text-decoration: none;\" target=\"_blank\">$1</a>');\n\n  // Add line breaks after commas or semicolons for readability\n  if (value.length > 100) {\n    value = value.replace(/([,;])\\s*/g, '$1<br>');\n  }\n\n  return value;\n}\n\nfunction formatDetailedRawHeaders() {\n  const allHeaders = [];\n  const seen = new Set();\n\n  const addHeader = (name, value) => {\n    const key = name.toLowerCase();\n    if (seen.has(key)) return;\n    seen.add(key);\n\n    const status = Object.entries(auditData.headerStatus || {}).find(\n      ([k]) => k.toLowerCase() === name.toLowerCase()\n    );\n    const present = status ? status[1].present : !!value;\n\n    allHeaders.push({\n      name: name.trim(),\n      present,\n      value: value || '[empty]'\n    });\n  };\n\n  Object.entries(auditData.originalHeaders || {}).forEach(([key, value]) => {\n    if (key) addHeader(key, value);\n  });\n\n  const securityHeaders = [\n    'content-security-policy',\n    'strict-transport-security',\n    'x-content-type-options',\n    'x-frame-options',\n    'referrer-policy',\n    'permissions-policy',\n    'x-xss-protection'\n  ];\n\n  const isWarningHeader = (name, value) => {\n    const lower = name.toLowerCase();\n    if (lower === 'strict-transport-security') {\n      const match = value.match(/max-age=(\\d+)/);\n      return match && parseInt(match[1]) < 2592000;\n    }\n    if (lower === 'content-security-policy') return value.includes(\"'unsafe-inline'\");\n    return false;\n  };\n\n  const tableRows = allHeaders.map(header => {\n    const isSecurity = securityHeaders.includes(header.name.toLowerCase());\n    const warning = isSecurity && isWarningHeader(header.name, header.value);\n    const missing = isSecurity && !header.present;\n\n    let bgColor = '#F8F9FA';\n    let textColor = '#333';\n\n    if (isSecurity) {\n      if (missing) {\n        bgColor = '#FFEBEE';\n        textColor = '#C62828';\n      } else if (warning) {\n        bgColor = '#FFF9C4';\n        textColor = '#F57F17';\n      } else {\n        bgColor = '#E8F5E9';\n        textColor = '#2E7D32';\n      }\n    }\n\n    return `\n      <tr style=\"background-color: ${bgColor}; color: ${textColor};\">\n        <td title=\"${isSecurity ? (missing ? 'Missing' : (warning ? 'Needs review' : 'Secure')) : 'Informational'}\" style=\"padding: 8px; font-weight: bold;\">${header.name}</td>\n        <td style=\"padding: 8px; text-align: center;\">${header.present ? 'present' : 'absent'}</td>\n        <td style=\"padding: 8px; word-break: break-word; font-family: monospace;\">${formatLongValue(header.value)}</td>\n      </tr>`;\n  }).join('');\n\n  return `\n    <table style=\"width: 100%; border-collapse: collapse; margin-top: 10px;\">\n      <thead>\n        <tr style=\"background-color: #E0E0E0;\">\n          <th style=\"padding: 10px;\">Header</th>\n          <th style=\"padding: 10px;\">Status</th>\n          <th style=\"padding: 10px;\">Value</th>\n        </tr>\n      </thead>\n      <tbody>\n        ${tableRows}\n      </tbody>\n    </table>`;\n}\n\n// Format additional information section\nfunction formatAdditionalInfo() {\n  const headers = [\n    {\n      name: 'access-control-allow-origin',\n      description: 'This is a very lax CORS policy. Such a policy should only be used on a public CDN.'\n    },\n    {\n      name: 'strict-transport-security',\n      description: 'HTTP Strict Transport Security is an excellent feature to support on your site and strengthens your implementation of TLS by getting the User Agent to enforce the use of HTTPS.'\n    },\n    {\n      name: 'content-security-policy',\n      description: 'Content Security Policy is an effective measure to protect your site from XSS attacks. By whitelisting sources of approved content, you can prevent the browser from loading malicious assets. Analyse this policy in more detail. You can sign up for a free account on Report URI to collect reports about problems on your site.'\n    },\n    {\n      name: 'permissions-policy',\n      description: 'Permissions Policy is a new header that allows a site to control which features and APIs can be used in the browser.'\n    },\n    {\n      name: 'referrer-policy',\n      description: 'Referrer Policy is a new header that allows a site to control how much information the browser includes with navigations away from a document and should be set by all sites.'\n    },\n    {\n      name: 'x-content-type-options',\n      description: 'X-Content-Type-Options stops a browser from trying to MIME-sniff the content type and forces it to stick with the declared content-type. The only valid value for this header is \"X-Content-Type-Options: nosniff\".'\n    },\n    {\n      name: 'x-frame-options',\n      description: 'X-Frame-Options tells the browser whether you want to allow your site to be framed or not. By preventing a browser from framing your site you can defend against attacks like clickjacking.'\n    },\n    {\n      name: 'report-to',\n      description: 'Report-To enables the Reporting API. This allows a website to collect reports from the browser about various errors that may occur. You can sign up for a free account on Report URI to collect these reports.'\n    },\n    {\n      name: 'nel',\n      description: 'Network Error Logging is a new header that instructs the browser to send reports during various network or application errors. You can sign up for a free account on Report URI to collect these reports.'\n    },\n    {\n      name: 'server',\n      description: 'Server value has been changed. Typically you will see values like \"Microsoft-IIS/8.0\" or \"nginx 1.7.2\".'\n    }\n  ];\n  \n  let rows = '';\n  \n  for (const header of headers) {\n    const isSecurityHeader = ['content-security-policy', 'strict-transport-security', 'x-content-type-options', 'x-frame-options', 'referrer-policy', 'permissions-policy'].includes(header.name);\n    const headerColor = isSecurityHeader ? '#27AE60' : '#3498DB';\n    \n    rows += `\n      <tr>\n        <td style=\"padding: 8px; border-bottom: 1px solid #eee; color: ${headerColor}; font-weight: bold;\">${header.name}</td>\n        <td style=\"padding: 8px; border-bottom: 1px solid #eee;\">${header.description}</td>\n      </tr>\n    `;\n  }\n  \n  return `\n    <table style=\"width: 100%; border-collapse: collapse; margin-top: 10px;\">\n      <tbody>\n        ${rows}\n      </tbody>\n    </table>\n  `;\n}\n\nfunction formatSecurityGrade() {\n  const gradeColors = {\n    'A+': '#27AE60',\n    'A': '#27AE60',\n    'A-': '#27AE60',\n    'B+': '#3498DB',\n    'B': '#3498DB',\n    'B-': '#3498DB',\n    'C+': '#F39C12',\n    'C': '#F39C12',\n    'C-': '#F39C12',\n    'D+': '#E74C3C',\n    'D': '#E74C3C',\n    'D-': '#E74C3C',\n    'F': '#E74C3C'\n  };\n  \n  return `<div class=\"grade\" style=\"font-size: 64px; font-weight: bold; width: 100px; height: 100px; line-height: 100px; text-align: center; background-color: ${gradeColors[auditData.grade] || '#E74C3C'}; color: white; border-radius: 5px; margin: 0 auto;\">${auditData.grade}</div>`;\n}\n\nfunction formatCriticalVulnerabilities() {\n  if (!auditData.vulnOutput || auditData.vulnOutput.trim() === '') {\n    return '<p>No vulnerabilities detected.</p>';\n  }\n\n  try {\n    const vuln = auditData.vulnOutput.trim();\n    let html = '';\n    const renderedTitles = new Set();\n\n    // Match sections like ## Category (e.g., ## Critical Vulnerabilities)\n    const categories = vuln.split(/(?=^##\\s+)/gm).filter(Boolean);\n\n    for (const categoryBlock of categories) {\n      const categoryMatch = categoryBlock.match(/^##\\s+(.*)/);\n      const categoryTitle = categoryMatch?.[1]?.trim() || 'Uncategorized';\n\n      // Find numbered items: 1. **Title**\n      const vulns = categoryBlock.split(/(?=^\\d+\\.\\s+\\*\\*)/gm).filter(Boolean);\n\n      for (const vulnBlock of vulns) {\n        const titleMatch = vulnBlock.match(/^\\d+\\.\\s+\\*\\*(.*?)\\*\\*/);\n        const title = titleMatch?.[1]?.trim() || 'Unnamed Vulnerability';\n        const key = `${categoryTitle}::${title}`.toLowerCase();\n        if (renderedTitles.has(key)) continue;\n\n        const descriptionMatch = vulnBlock.match(/\\*\\*Description\\*\\*:?\\s*([\\s\\S]*?)(?=\\n\\*\\*|\\n$)/i);\n        const impactMatch = vulnBlock.match(/\\*\\*(?:Impact|Potential Impact)\\*\\*:?\\s*([\\s\\S]*?)(?=\\n\\*\\*|\\n$)/i);\n        const recommendationMatch = vulnBlock.match(/\\*\\*(?:Recommendation|Mitigation|Fix)\\*\\*:?\\s*([\\s\\S]*?)(?=\\n\\*\\*|\\n$)/i);\n\n        const description = descriptionMatch?.[1]?.trim() || '';\n        const impact = impactMatch?.[1]?.trim() || '';\n        const recommendation = recommendationMatch?.[1]?.trim() || '';\n\n        if (description || impact || recommendation) {\n          html += `\n            <div style=\"border-left: 4px solid #E74C3C; padding: 10px; margin-bottom: 15px;\">\n              <div style=\"font-weight: bold; color: #E74C3C;\">${title}</div>\n              ${description ? `<div style=\"margin-top: 5px;\">${description}</div>` : ''}\n              ${impact ? `<div style=\"margin-top: 5px; font-style: italic; color: #7F8C8D;\">Impact: ${impact}</div>` : ''}\n              ${recommendation ? `<div style=\"margin-top: 5px;\"><strong>Recommendation:</strong> ${recommendation}</div>` : ''}\n            </div>`;\n          renderedTitles.add(key);\n        }\n      }\n    }\n\n    return html || '<p>No vulnerabilities parsed from output.</p>';\n  } catch (e) {\n    console.error('Error in formatCriticalVulnerabilities:', e);\n    return `<p>Error processing vulnerabilities: ${e.message}</p>`;\n  }\n}\n\n\n// Generate all security header badges\nfunction generateAllHeaderBadges() {\n  // Only include the necessary security headers\n  const securityHeaders = [\n    'Content-Security-Policy',\n    'Strict-Transport-Security',\n    'X-Content-Type-Options',\n    'X-Frame-Options',\n    'Referrer-Policy',\n    'Permissions-Policy'\n  ];\n  \n  let badges = '';\n  securityHeaders.forEach(header => {\n                      \n    const isWarning = header === 'Strict-Transport-Security' &&\n                  auditData.headerStatus?.[header]?.value &&\n                  parseInt(auditData.headerStatus[header].value.match(/max-age=(\\d+)/)?.[1] || 0) < 2592000;\n    \n    badges += createHeaderBadge(header, isWarning);\n  });\n  \n  return badges;\n}\n\n<!-- Modify the HTML to directly access auditData.originalHeaders or allHeaders -->\nconst html = `<!DOCTYPE html>\n<html>\n<head>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <title>Website Security Audit Report</title>\n    <style>\n        body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f9f9f9; }\n        .container { max-width: 950px; margin: 0 auto; }\n        .header { background-color: #2c3e50; color: white; padding: 25px 20px; text-align: center; }\n        .header h1 { color: white; font-size: 28px; margin: 0; text-shadow: 1px 1px 2px rgba(0,0,0,0.5); }\n        .content { padding: 20px; }\n        .summary-box { background-color: #EBF5FB; padding: 15px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }\n        .warning-box { background-color: #FEF5E7; padding: 15px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }\n        .headers-box { background-color: #F5F7FA; padding: 15px; margin-bottom: 20px; border-radius: 5px; }\n        .findings-box { background-color: white; padding: 15px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }\n        .raw-headers-box { background-color: #F5F7FA; padding: 15px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }\n        .additional-info-box { background-color: #F5F7FA; padding: 15px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }\n        .details-table { width: 100%; border-collapse: collapse; }\n        .details-table th { text-align: left; padding: 8px; background-color: #f2f2f2; }\n        .details-table td { padding: 8px; border-bottom: 1px solid #eee; }\n        .header-badges { margin-top: 10px; }\n        h1, h2, h3 { color: #2c3e50; }\n        .critical-item { border-left: 4px solid #E74C3C; padding: 10px; margin-bottom: 15px; }\n        .critical-title { font-weight: bold; color: #E74C3C; }\n        .config-item { border-left: 4px solid #3498DB; padding: 10px; margin-bottom: 15px; }\n        .config-title { font-weight: bold; color: #3498DB; }\n        pre { background-color: #f8f9fa; padding: 10px; border-radius: 5px; overflow-x: auto; font-family: monospace; margin-top: 5px; }\n    </style>\n</head>\n<body>\n    <div class=\"container\">\n        <!-- Report Header -->\n        <div class=\"header\">\n            <h1 style=\"color: white; text-shadow: 1px 1px 2px rgba(0,0,0,0.5);\">Website Security Audit Report</h1>\n        </div>\n        \n        <div class=\"content\">\n            <!-- Security Report Summary -->\n            <div class=\"summary-box\">\n                <h2>Security Report Summary</h2>\n                <table style=\"width: 100%;\">\n                    <tr>\n                        <td style=\"width: 120px;\" valign=\"top\">\n                            ${formatSecurityGrade()}\n                        </td>\n                        <td valign=\"top\">\n                            <table style=\"width: 100%;\">\n                                <tr>\n                                    <td><strong>Site:</strong></td>\n                                    <td><a href=\"${auditData.url}\" style=\"color: #3498db;\">${auditData.url}</a></td>\n                                </tr>\n                                <tr>\n                                    <td><strong>Report Time:</strong></td>\n                                    <td>${auditData.timestamp}</td>\n                                </tr>\n                                <tr>\n                                    <td valign=\"top\"><strong>Headers:</strong></td>\n                                    <td>\n                                        <div class=\"header-badges\">\n                                            ${generateAllHeaderBadges()}\n                                        </div>\n                                    </td>\n                                </tr>\n                                <tr>\n                                    <td><strong>Critical Issues:</strong></td>\n                                    <td>${auditData.criticalCount || 0}</td>\n                                </tr>\n                                <tr>\n                                    <td><strong>Warnings:</strong></td>\n                                    <td>${auditData.warningCount || 0}</td>\n                                </tr>\n                            </table>\n                        </td>\n                    </tr>\n                </table>\n            </div>\n\n            <!-- Warnings Section -->\n            <div class=\"warning-box\">\n                <h2>Warnings</h2>\n                ${formatWarningsSection()}\n            </div>\n\n            <!-- Raw Headers Section -->\n            <div class=\"raw-headers-box\">\n                <h2>Raw Headers</h2>\n                ${formatDetailedRawHeaders()}\n            </div>\n\n            <!-- Security Findings -->\n            <div class=\"findings-box\">\n                <h2>Security Findings</h2>\n                \n                <!-- Vulnerabilities -->\n                <h3>Vulnerabilities</h3>\n                ${formatCriticalVulnerabilities()}\n                \n                <!-- Configuration Issues -->\n                <h3>Configuration Issues</h3>\n                ${formatConfigurationIssues()}\n            </div>\n            \n            <div class=\"additional-info-box\">\n              <h2>Additional Information</h2>\n              ${formatAdditionalInfo()}\n            </div>\n            \n            <!-- Implementation Guide -->\n            <div class=\"findings-box\">\n                <h2>Implementation Guide</h2>\n                <p>This report highlights security issues detected through client-side analysis. For a comprehensive security assessment, consider engaging a professional penetration tester.</p>\n                \n                <div style=\"background-color: #eafaf1; padding: 15px; margin-top: 15px; border-left: 4px solid #2ecc71; border-radius: 3px;\">\n                    <p><strong>To implement the fixes above:</strong></p>\n                    <ol style=\"padding-left: 20px; margin-top: 10px;\">\n                        <li>Work with your development team to address each issue in order of criticality</li>\n                        <li>Retest after implementing each fix</li>\n                        <li>Consider implementing a web application firewall for additional protection</li>\n                    </ol>\n                </div>\n            </div>\n            \n            <!-- Footer -->\n            <div style=\"text-align: center; padding: 20px; font-size: 12px; color: #777;\">\n                <p>This report was automatically generated and represents an automated assessment of publicly accessible aspects of your website. For a more comprehensive security assessment, consider engaging with a professional security consultant.</p>\n                <p>&copy; 2025 Website Security Scanner | Generated on ${auditData.timestamp}</p>\n            </div>\n        </div>\n    </div>\n</body>\n</html>`;\n\nreturn [{\n  json: {\n    ...items[0].json,\n    emailHtml: html\n  }\n}];"}, "typeVersion": 2}], "pinData": {}, "connections": {"Scrape Website": {"main": [[{"node": "Security Vulnerabilities Audit", "type": "main", "index": 0}, {"node": "Extract Headers for Debug", "type": "main", "index": 0}]]}, "convert to HTML": {"main": [[{"node": "Send Security Report", "type": "main", "index": 0}]]}, "Landing Page Url": {"main": [[{"node": "Scrape Website", "type": "main", "index": 0}]]}, "Process Audit Results": {"main": [[{"node": "convert to HTML", "type": "main", "index": 0}]]}, "Merge Security Results": {"main": [[{"node": "Aggregate Audit Results", "type": "main", "index": 0}]]}, "Aggregate Audit Results": {"main": [[{"node": "Process Audit Results", "type": "main", "index": 0}]]}, "OpenAI Content Analysis": {"ai_languageModel": [[{"node": "Security Vulnerabilities Audit", "type": "ai_languageModel", "index": 0}]]}, "OpenAI Headers Analysis": {"ai_languageModel": [[{"node": "Security Configuration Audit", "type": "ai_languageModel", "index": 0}]]}, "Extract Headers for Debug": {"main": [[{"node": "Security Configuration Audit", "type": "main", "index": 0}]]}, "Security Configuration Audit": {"main": [[{"node": "Merge Security Results", "type": "main", "index": 0}]]}, "Security Vulnerabilities Audit": {"main": [[{"node": "Merge Security Results", "type": "main", "index": 1}]]}}}