{
  "name": "Send existing VAS reports to Slack with HTTP Request",
  "active": false,
  "nodes": [
    {
      "id": "run-manually",
      "name": "Run manually",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "parameters": {}
    },
    {
      "id": "configuration",
      "name": "Configuration",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        0
      ],
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "config-enabled",
              "name": "enabled",
              "value": false,
              "type": "boolean"
            },
            {
              "id": "config-scanId",
              "name": "scanId",
              "value": "",
              "type": "string"
            },
            {
              "id": "config-sendToSlack",
              "name": "sendToSlack",
              "value": false,
              "type": "boolean"
            },
            {
              "id": "config-slackChannel",
              "name": "slackChannel",
              "value": "",
              "type": "string"
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "validate-configuration",
      "name": "Validate configuration",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return [{json:(function validateConfiguration(config) {\n  if (!config || config.enabled !== true) throw new Error('Report reading is disabled. Review the tutorial and configure an existing scan before enabling.');\n  if (typeof config.scanId !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(config.scanId) || config.scanId.length !== 36) {\n    throw new Error('Enter one existing scan UUID, not a URL, path or expression. No scan was started.');\n  }\n  if (typeof config.sendToSlack !== 'boolean' || typeof config.slackChannel !== 'string' ||\n      (config.slackChannel !== '' && /^[CG][A-Z0-9]{8,30}$/.exec(config.slackChannel)?.[0] !== config.slackChannel) ||\n      (config.sendToSlack && config.slackChannel === '')) {\n    throw new Error('Keep Slack delivery false for a preview, or configure a fixed Slack channel ID and explicitly enable delivery.');\n  }\n  return { scanId: config.scanId.toLowerCase(), sendToSlack: config.sendToSlack, slackChannel: config.slackChannel };\n})($input.first().json)}];"
      }
    },
    {
      "id": "read-existing-scan",
      "name": "Read existing scan",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        0
      ],
      "parameters": {
        "method": "GET",
        "url": "=https://api.vibeappscanner.com/scans/{{ $json.scanId }}/status",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "options": {
          "timeout": 15000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "responseFormat": "json",
              "neverError": true
            }
          }
        }
      },
      "credentials": {
        "httpBearerAuth": {
          "id": "REPLACE_VAS_READ_CREDENTIAL",
          "name": "VAS API: scan:read only"
        }
      },
      "retryOnFail": false,
      "onError": "continueRegularOutput"
    },
    {
      "id": "preview-report-summary",
      "name": "Preview report summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return [{json:(function formatExistingReport(input, config, variant) {\n  const obj = value => value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};\n  const count = value => Number.isSafeInteger(value) && value >= 0 ? value : null;\n  const word = value => value === null ? 'unknown' : String(value);\n  const terminalStates = ['completed', 'failed', 'cancelled', 'timeout', 'timed_out'];\n  const states = [...terminalStates, 'queued', 'running', 'processing'];\n  const labels = {\n    unclassified: 'Application scope not classified', 'login-page': 'Login page only',\n    'blocked-page': 'Application behind a blocked page was not assessed',\n    'placeholder-only': 'Placeholder content only', 'parked-domain': 'Parked domain only',\n    unreachable: 'Application unreachable', 'reachability-unconfirmed': 'Application reachability not confirmed',\n    unknown: 'Application coverage unknown',\n  };\n  const raw = obj(input);\n  const body = variant === 'http' ? obj(raw.body) : raw;\n  const isTerminal = terminalStates.includes(body.status);\n  const valid = ['http', 'native'].includes(variant) && typeof body.scanId === 'string' && body.scanId.toLowerCase() === config.scanId && states.includes(body.status) &&\n    (variant === 'http' ? raw.statusCode === 200 : body.terminal === isTerminal) && !Object.hasOwn(raw, 'error');\n  const counts = Object.fromEntries(['total', 'critical', 'high', 'medium', 'low', 'info'].map(severity => [severity,\n    valid && isTerminal ? count(variant === 'native' ? obj(body.counts)[severity]\n      : body[severity === 'total' ? 'findingsCount' : `${severity}FindingsCount`]) : null]));\n  const hasChecks = valid && Array.isArray(body.scannerStatuses) && body.scannerStatuses.length <= 1000;\n  const checks = hasChecks ? body.scannerStatuses.map(obj) : [];\n  const sourceCoverage = obj(body.coverage);\n  const page = variant === 'native' ? sourceCoverage.pageCoverage : obj(body.qualityCoverage).pageCoverage;\n  const coverage = {\n    pageCoverage: valid && typeof page === 'string' && Object.hasOwn(labels, page) ? page : 'unknown',\n    reportedChecks: variant === 'native' ? (valid ? count(sourceCoverage.reportedChecks) : null) : (hasChecks ? checks.length : null),\n    incompleteChecks: variant === 'native' ? (valid ? count(sourceCoverage.incompleteChecks) : null) : (hasChecks ? checks.filter(s =>\n      ['failed', 'timed_out', 'timeout'].includes(s.status) ||\n      (s.coverageIncomplete === true && s.coverageReason !== 'scope-limit') ||\n      !['completed', 'failed', 'timed_out', 'timeout', 'skipped', 'not_applicable'].includes(s.status)).length : null),\n    skippedChecks: variant === 'native' ? (valid ? count(sourceCoverage.skippedChecks) : null) : (hasChecks ? checks.filter(s => ['skipped', 'not_applicable'].includes(s.status)).length : null),\n    boundedChecks: variant === 'native' ? (valid ? count(sourceCoverage.boundedChecks) : null) : (hasChecks ? checks.filter(s => s.status === 'completed' && s.coverageReason === 'scope-limit').length : null),\n  };\n  const reportAccess = !valid ? 'unknown' : variant === 'native'\n    ? (['available', 'locked'].includes(body.reportAccess) ? body.reportAccess : 'unknown')\n    : (body.locked === true || body.isUnlocked === false ? 'locked' : body.isUnlocked === true ? 'available' : 'unknown');\n  const status = valid ? body.status : 'unavailable';\n  const reportUrl = `https://vibeappscanner.com/dashboard/scans/${config.scanId}`;\n  const lines = [\n    `VAS existing scan status: ${status}.`,\n    `Findings reported: ${word(counts.total)}; critical: ${word(counts.critical)}; high: ${word(counts.high)}; medium: ${word(counts.medium)}; low: ${word(counts.low)}; info: ${word(counts.info)}.`,\n    `Coverage: ${labels[coverage.pageCoverage]}.`,\n    `Checks incomplete or unsuccessful: ${word(coverage.incompleteChecks)} of ${word(coverage.reportedChecks)} reported.`,\n    `Checks skipped or not applicable: ${word(coverage.skippedChecks)}; checks limited to a bounded sample: ${word(coverage.boundedChecks)}.`,\n  ];\n  if (!valid) lines.push('The read was not confirmed. Check account access and the existing scan. No replacement scan was started.');\n  else if (!isTerminal) lines.push('This scan is still pending. Counts are not final. This workflow does not poll or start a replacement scan.');\n  else if (status !== 'completed') lines.push('This is not a completed scan. Reported findings remain visible; no replacement scan was started.');\n  if (reportAccess !== 'available') lines.push('Detailed report access is not confirmed. Check VAS account permissions.');\n  lines.push('Completion and zero findings do not establish that the app is secure. Review report coverage and unresolved checks.', `Report: ${reportUrl}`);\n  const summary = lines.join('\\n');\n  return { scanId: config.scanId, status, terminal: valid ? isTerminal : null, counts, coverage, reportAccess, reportUrl,\n    summary, sendToSlack: config.sendToSlack,\n    slackPayload: { channel: config.slackChannel, text: summary, mrkdwn: false, parse: 'none', link_names: false, unfurl_links: false, unfurl_media: false } };\n})($input.first().json,$('Validate configuration').first().json,\"http\")}];"
      }
    },
    {
      "id": "slack-delivery-enabled",
      "name": "Slack delivery enabled",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1100,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "slack-enabled",
              "leftValue": "={{ $json.sendToSlack }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "send-slack-summary",
      "name": "Send Slack summary",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1320,
        -100
      ],
      "parameters": {
        "method": "POST",
        "url": "https://slack.com/api/chat.postMessage",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.slackPayload) }}",
        "options": {
          "timeout": 15000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "responseFormat": "json",
              "neverError": true
            }
          }
        }
      },
      "credentials": {
        "httpBearerAuth": {
          "id": "REPLACE_SLACK_CREDENTIAL",
          "name": "Slack bot: chat:write"
        }
      },
      "retryOnFail": false
    },
    {
      "id": "confirm-slack-delivery",
      "name": "Confirm Slack delivery",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1540,
        -100
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return [{json:(function confirmSlackDelivery(input) {\n  if (input?.statusCode !== 200 || input?.body?.ok !== true) {\n    throw new Error('Slack delivery was not confirmed. Check the bot and channel. Retry only the notification step; duplicate messages are possible.');\n  }\n  return { notification: 'sent' };\n})($input.first().json)}];"
      }
    },
    {
      "id": "preview-only",
      "name": "Preview only",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        100
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const {sendToSlack,slackPayload,...preview}=$input.first().json; return [{json:preview}];"
      }
    },
    {
      "id": "read-this-first",
      "name": "Read this first",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -850
      ],
      "parameters": {
        "content": "## Who this is for\nTeams using Vibe App Scanner who want to share an existing website security report in Slack without starting another scan.\n\n## What it does\nRun the workflow manually, validate one existing scan UUID, read its status once, and preview finding counts, coverage notes and the authenticated report link. Slack delivery is optional and off by default. Genuine terminal findings remain visible even when a scan failed or coverage is partial; missing data stays unknown, not zero. Completion is not a security certification.\n\n## Setup and requirements\nUse an eligible VAS REST API account and a scan:read-only key stored in n8n credentials. In Configuration, enter your existing scan UUID and enable reading. Preview first. To send, bind a separate Slack bot credential with chat:write, set a fixed channel ID and explicitly enable Slack delivery. No keys or private report IDs are included.\n\n## Availability and customization\nThe HTTP Request version uses built-in nodes on n8n Cloud or self-hosted n8n. The native version requires self-hosted n8n and the VAS package; n8n Cloud approval pending. Keep both requests fixed and customize only the configured report and Slack destination. No scan creation, polling, replacement scans or AI calls are included.\n\n[Read the setup tutorial](https://vibeappscanner.com/integrations/n8n/tutorial)",
        "height": 780,
        "width": 740,
        "color": 1
      }
    },
    {
      "id": "configure-and-preview",
      "name": "Configure and preview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        180,
        250
      ],
      "parameters": {
        "content": "## 1. Read an existing report\nBind the VAS scan:read credential on Read existing scan. In Configuration, enter an existing scan UUID and set enabled to true. Keep sendToSlack false for the first preview. Read errors stay uncertain and never create a replacement scan.",
        "height": 220,
        "width": 600,
        "color": 7
      }
    },
    {
      "id": "optional-slack-delivery",
      "name": "Optional Slack delivery",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        860,
        250
      ],
      "parameters": {
        "content": "## 2. Send only when ready\nBind a separate Slack bot credential, set the fixed channel ID in Configuration, then enable sendToSlack. Review the preview first. If Slack delivery is ambiguous, check the channel before retrying only the notification. Duplicate messages are possible.",
        "height": 220,
        "width": 600,
        "color": 7
      }
    }
  ],
  "connections": {
    "Run manually": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configuration": {
      "main": [
        [
          {
            "node": "Validate configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate configuration": {
      "main": [
        [
          {
            "node": "Read existing scan",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read existing scan": {
      "main": [
        [
          {
            "node": "Preview report summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Preview report summary": {
      "main": [
        [
          {
            "node": "Slack delivery enabled",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack delivery enabled": {
      "main": [
        [
          {
            "node": "Send Slack summary",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Preview only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Slack summary": {
      "main": [
        [
          {
            "node": "Confirm Slack delivery",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 120,
    "saveDataSuccessExecution": "none",
    "saveDataErrorExecution": "none",
    "saveManualExecutions": false,
    "saveExecutionProgress": false
  },
  "pinData": {},
  "tags": []
}
