N
n8n Store
Workflow Market
Transform Cloud Documentation into Security Control Baselines with OpenAI and Google Drive

Transform Cloud Documentation into Security Control Baselines with OpenAI and Google Drive

by followdrabbit0 views

説明

Categories

🤖 AI & Machine Learning

Nodes Used

n8n-nodes-base.ifn8n-nodes-base.ifn8n-nodes-base.ifn8n-nodes-base.ifn8n-nodes-base.setn8n-nodes-base.coden8n-nodes-base.coden8n-nodes-base.coden8n-nodes-base.coden8n-nodes-base.code
Price無料
Views0
最終更新11/28/2025
workflow.json
{
  "meta": {
    "instanceId": "247ad01c2ef7bb169c88da0fdc2f39730d8a7e76f71913465e5d08e401bd237a",
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "c76e4bd4-dbdf-4e35-808e-b225e66427e6",
      "name": "check_mandatory_fields",
      "type": "n8n-nodes-base.if",
      "position": [
        -2624,
        128
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "77e24e89-4efa-4d68-b5d3-b8c8252d8834",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.body.cloudProvider }}",
              "rightValue": ""
            },
            {
              "id": "9e527511-acd4-4364-9b28-06a3a03bba50",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.body.technology }}",
              "rightValue": ""
            },
            {
              "id": "d96a8b69-7c38-4519-b706-88ea47442051",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.body.urls }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.2
    },
    {
      "id": "a8ff546c-af04-4fef-b7f5-7b55ec65af0f",
      "name": "No Operation, do nothing",
      "type": "n8n-nodes-base.noOp",
      "position": [
        -2400,
        128
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "1cbc1ca7-e5db-4e5a-a1f7-e774a70e1201",
      "name": "generate_uuid",
      "type": "n8n-nodes-base.code",
      "position": [
        -2400,
        -64
      ],
      "parameters": {
        "jsCode": "function generateShortUUID() {\n  return Math.random().toString(36).substring(2, 14); // 12 chars\n}\n\nreturn [\n  {\n    json: {\n      uuid: generateShortUUID()\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "1f8f124a-1417-41b7-abf9-177f9a908cac",
      "name": "create",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -2848,
        128
      ],
      "webhookId": "1d225cee-cc5a-43ea-8567-41aeb419acfb",
      "parameters": {
        "path": "create",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode",
        "authentication": "basicAuth"
      },
      "credentials": {
        "httpBasicAuth": {
          "id": "YhK2c1HjuoPNeyGw",
          "name": "Unnamed credential"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "6ede342c-2ad9-41cc-b11c-59186de696ea",
      "name": "settings",
      "type": "n8n-nodes-base.set",
      "position": [
        -1504,
        -64
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "ff72f1c6-0cea-4bc5-a94c-5f39fff86882",
              "name": "uuid",
              "type": "string",
              "value": "={{ $('generate_uuid').first().json.uuid }}"
            },
            {
              "id": "8844a9ff-7117-4bcb-a726-ad77135ea598",
              "name": "cloudprovider",
              "type": "string",
              "value": "={{ $('create').first().json.body.cloudProvider }}"
            },
            {
              "id": "bf424f91-1487-4c39-aeaf-e6c65471ed33",
              "name": "technology",
              "type": "string",
              "value": "={{ $('create').first().json.body.technology }}"
            },
            {
              "id": "5484b003-dd53-4500-baf6-c13a2d29832e",
              "name": "urls",
              "type": "array",
              "value": "={{ $('create').first().json.body.urls }}"
            },
            {
              "id": "83c4994d-cada-4a58-bf1e-285ab0efeb9e",
              "name": "gdrive_target",
              "type": "string",
              "value": "={{ $('get_gdrive_id').first().json.id }}"
            },
            {
              "id": "d0c2ba9f-274c-4a2f-a95b-ba5d366b0236",
              "name": "assistant_extractor_id",
              "type": "string",
              "value": "={{ $json.assistant_extractor_id }}"
            },
            {
              "id": "0b73530e-5bac-4583-9b3b-8edd642265e0",
              "name": "assistant_composer_id",
              "type": "string",
              "value": "={{ $json.assistant_composer_id }}"
            },
            {
              "id": "9b4cd1e7-8029-4fcd-8079-5d120d00a253",
              "name": "assistant_baseline_id",
              "type": "string",
              "value": "={{ $json.assistant_baseline_id }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "da40bbec-d7ea-4499-a6d7-09762f81c529",
      "name": "http_get_url",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -832,
        -208
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {
          "timeout": 10000,
          "allowUnauthorizedCerts": true
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
            }
          ]
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "831e22cb-9d60-41a1-b49e-6ce03f0bee9f",
      "name": "html_sanitizer",
      "type": "n8n-nodes-base.code",
      "position": [
        -608,
        -208
      ],
      "parameters": {
        "jsCode": "// 1. Pega o conteúdo HTML\nconst htmlRaw = $json.data || $json.body;\n\nif (!htmlRaw || typeof htmlRaw !== 'string') {\n  throw new Error('Campo `data` ou `body` ausente ou inválido.');\n}\n\n// 2. Sanitiza o HTML\nlet cleaned = htmlRaw\n  .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, '')\n  .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, '')\n  .replace(/<!--[\\s\\S]*?-->/g, '')\n  .replace(/<(head|header|footer|nav|button|form|aside|meta|link|iframe|noscript)[^>]*>[\\s\\S]*?<\\/\\1>/gi, '')\n  .replace(/<[^>]+>/g, '') // remove tags HTML restantes\n  .replace(/\\s{2,}/g, ' ')\n  .replace(/\\n{2,}/g, '\\n')\n  .trim();\n\n// 3. Pega os dados do nó anterior \"process_url\" (sem quebrar o fluxo)\nconst processData = $node[\"process_url\"].json;\n\n// 4. Retorna novo objeto com os metadados + texto sanitizado\nreturn [\n  {\n    json: {\n      uuid: processData.uuid,\n      cloudProvider: processData.cloudProvider || processData.cloudprovider,\n      technology: processData.technology,\n      url: processData.url,\n      sanitizedText: cleaned\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "a7f30691-60e2-451f-8855-45053ede5980",
      "name": "1_DefySec_Extractor",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        -384,
        -208
      ],
      "parameters": {
        "text": "=CloudProvider:  {{ $json.cloudProvider}}\nTechnology: {{ $json.technology }}\nData Source: {{ $json.url }}\nData: {{ $json.sanitizedText }}",
        "prompt": "define",
        "options": {},
        "resource": "assistant",
        "assistantId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('settings').first().json.assistant_extractor_id }}"
        }
      },
      "credentials": {
        "openAiApi": {
          "id": "Vkw9T5qrK2yHZxw7",
          "name": "OpenAi account"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "7278026a-c1ef-49b9-950d-d924e4f2aca9",
      "name": "explode_urls",
      "type": "n8n-nodes-base.code",
      "position": [
        -1280,
        -64
      ],
      "parameters": {
        "jsCode": "const { uuid, cloudprovider, technology, urls } = $json;\n\nreturn urls.map(url => ({\n  json: {\n    uuid,\n    cloudProvider: cloudprovider,\n    technology,\n    url\n  }\n}));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "9e8fecb4-99a5-426f-a09b-245d909be212",
      "name": "process_url",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -1056,
        -64
      ],
      "parameters": {
        "options": {
          "reset": false
        }
      },
      "typeVersion": 3
    },
    {
      "id": "08a7fbde-12c2-4efd-9acf-186a2a61d352",
      "name": "ec_search_files",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        192,
        -208
      ],
      "parameters": {
        "filter": {},
        "options": {
          "fields": [
            "*"
          ]
        },
        "resource": "fileFolder",
        "returnAll": true,
        "queryString": "={{$items(\"settings\")[0].json.uuid}}_extractedControls_"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "id": "tMSvCpu4CSKuDXkh",
          "name": "Google Drive account"
        }
      },
      "typeVersion": 3,
      "alwaysOutputData": true
    },
    {
      "id": "012ae0a1-1bf0-4a67-9c1c-1de9a3758f07",
      "name": "ec_append_create_filter",
      "type": "n8n-nodes-base.if",
      "position": [
        768,
        -208
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "180ca863-e6dc-47ca-a211-02ac41530530",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.action }}",
              "rightValue": "=append"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "53bb7adc-f2c6-4e5d-b6f4-d0d060c9581a",
      "name": "ec_upload_new_file",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        992,
        -112
      ],
      "parameters": {
        "name": "={{ $json.fileName }}",
        "driveId": {
          "__rl": true,
          "mode": "list",
          "value": "My Drive",
          "cachedResultUrl": "https://drive.google.com/drive/my-drive",
          "cachedResultName": "My Drive"
        },
        "options": {},
        "folderId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('settings').first().json.gdrive_target }}"
        }
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "id": "tMSvCpu4CSKuDXkh",
          "name": "Google Drive account"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "1f9e5eb6-bad4-430e-9998-82a7322dcddf",
      "name": "ec_update_existing_file",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        1440,
        -208
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.fileId }}"
        },
        "options": {},
        "operation": "update",
        "changeFileContent": true
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "id": "tMSvCpu4CSKuDXkh",
          "name": "Google Drive account"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "df8ef747-d3e1-4d5e-a77d-da693f5baf38",
      "name": "ec_download_existing_file",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        992,
        -304
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.fileId }}"
        },
        "options": {},
        "operation": "download"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "id": "tMSvCpu4CSKuDXkh",
          "name": "Google Drive account"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "42a8de07-049e-4038-b942-d27f7e425bfa",
      "name": "ec_merge_data",
      "type": "n8n-nodes-base.code",
      "position": [
        1216,
        -304
      ],
      "parameters": {
        "jsCode": "// === ec_merge_data — append determinístico (.txt) ===\n\n// 1) Lê o texto atual (prioriza binary.data.data do item corrente; fallback no nó de download)\nfunction readPrevText() {\n  // fonte 1: item atual\n  let b64 = ($binary?.data && typeof $binary.data.data === 'string') ? $binary.data.data : '';\n\n  // fonte 2: nó de download (caso o item atual esteja sem payload)\n  if (!b64) {\n    try {\n      const dl = $items('ec_download_existing_file')[0];\n      if (dl?.binary?.data?.data && typeof dl.binary.data.data === 'string') {\n        b64 = dl.binary.data.data;\n      }\n    } catch {}\n  }\n\n  // fonte 3: raros casos de texto no JSON\n  if (!b64) {\n    if (typeof $json?.data === 'string')    return $json.data.replace(/\\r\\n/g, '\\n');\n    if (typeof $json?.body === 'string')    return $json.body.replace(/\\r\\n/g, '\\n');\n    if (typeof $json?.content === 'string') return $json.content.replace(/\\r\\n/g, '\\n');\n    return '';\n  }\n\n  try { return Buffer.from(b64, 'base64').toString('utf8').replace(/\\r\\n/g, '\\n'); }\n  catch { return ''; }\n}\n\nconst prevText = readPrevText();\n\n// 2) Texto novo direto do \"1_DefySec_Extractor\"\nlet ext;\ntry { ext = $items('1_DefySec_Extractor')[0]?.json; } catch {}\nif (!ext) { try { ext = $node['1_DefySec_Extractor']?.json; } catch {} }\n\nlet newText = ext?.output ?? ext?.data ?? ext?.text ?? '';\nnewText = String(newText)\n  .replace(/^```(?:txt|text|json)?\\s*/i, '')   // remove cercas, se vierem\n  .replace(/\\s*```$/, '')\n  .replace(/\\r\\n/g, '\\n');\n\n// 3) Append (1 linha em branco entre blocos quando já existe conteúdo)\nconst combined = prevText\n  ? prevText.replace(/\\s*$/, '') + '\\n\\n' + newText.replace(/^\\s+/, '')\n  : newText;\n\n// 4) Empacota binário para o Update (sempre em binary.data)\nconst outB64 = Buffer.from(combined, 'utf8').toString('base64');\n\n// 5) Garante fileId/fileName para o Update\nlet { fileId, fileName } = $json;\nif (!fileId || !fileName) {\n  try {\n    const src = $items('extracted_controls_append_or_create')[0]?.json;\n    fileId   = fileId   || src?.fileId;\n    fileName = fileName || src?.fileName;\n  } catch {}\n}\n\n// 6) **RETORNA UM ARRAY** com 1 item (é isso que o n8n exige)\nreturn [{\n  json: {\n    fileId,\n    fileName,\n    prevBytes: prevText.length,\n    newBytes: newText.length,\n    mergedBytes: combined.length\n  },\n  binary: {\n    data: {\n      data: outB64,\n      mimeType: 'text/plain',\n      fileName\n    }\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "ce20266e-90b4-4d75-b3d4-6777729e7e5f",
      "name": "ec_extract_file_info",
      "type": "n8n-nodes-base.code",
      "position": [
        480,
        -208
      ],
      "parameters": {
        "jsCode": "// === Code: decide append/create e prepara dados ===\n\n// util\nfunction safe(node) {\n  try { const a = $items(node); if (a?.[0]?.json) return a[0].json; } catch {}\n  try { const j = $node[node]?.json; if (j) return j; } catch {}\n  return {};\n}\n\n// 1) Contexto\nconst settings = safe('settings');\nconst uuid = String(settings.uuid || '').trim();\nconst folderId = String(settings.gdrive_target || '').trim();\nif (!uuid) throw new Error('uuid ausente no nó \"settings\".');\nif (!folderId) throw new Error('gdrive_target (folderId) ausente no nó \"settings\".');\n\nconst canonicalName = `${uuid}_extractedControls.txt`;\n\n// 2) Normaliza retorno do List para array de arquivos\nconst files = [];\nfor (const it of items) {\n  const j = it?.json;\n  if (Array.isArray(j)) files.push(...j);\n  else if (j && (j.name || j.id)) files.push(j);\n}\nconst existing = files.find(f => f.name === canonicalName);\n\n// 3) Pega TEXTO do \"1_DefySec_Extractor\"\nconst ext = safe('1_DefySec_Extractor');\nlet newText = ext.output ?? ext.data ?? ext.text ?? $json.output ?? '';\nnewText = String(newText)\n  .replace(/^```(?:txt|text|json)?\\s*/i, '')\n  .replace(/\\s*```$/, '')\n  .replace(/\\r\\n/g, '\\n');\n\n// 4) Decide ação\nconst out = {\n  action: existing ? 'append' : 'create',\n  fileId: existing?.id || null,\n  fileName: canonicalName,\n  folderId,\n  newText,\n};\n\n// 5) Se for criação, já emite binário pra Upload; se for append, só metadados.\nif (!existing) {\n  const base64 = Buffer.from(newText, 'utf8').toString('base64');\n  return [{\n    json: out,\n    binary: {\n      data: {\n        data: base64,\n        mimeType: 'text/plain',\n        fileName: canonicalName\n      }\n    }\n  }];\n}\n\nreturn [{ json: out }];"
      },
      "typeVersion": 2
    },
    {
      "id": "cad3694b-fbc6-4d97-a0b5-02f8f2601f89",
      "name": "cc_search_files",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        -832,
        -784
      ],
      "parameters": {
        "filter": {},
        "options": {
          "fields": [
            "*"
          ]
        },
        "resource": "fileFolder",
        "returnAll": true,
        "queryString": "={{$items(\"settings\")[0].json.uuid}}_extractedControls.txt"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "id": "tMSvCpu4CSKuDXkh",
          "name": "Google Drive account"
        }
      },
      "typeVersion": 3,
      "alwaysOutputData": true
    },
    {
      "id": "1c259737-687a-4862-8052-24290b3f4557",
      "name": "cc_extract_file_info",
      "type": "n8n-nodes-base.code",
      "position": [
        -608,
        -784
      ],
      "parameters": {
        "jsCode": "// === ec_extract_content — lê do Google Drive (ou do item atual) e repassa ===\n// Saída: [{ json: { content, fileId, fileName, mimeType, length } }]\n\nfunction readFromCurrentItem() {\n  const b = $binary?.data;\n  if (b?.data && typeof b.data === 'string') {\n    return {\n      b64: b.data,\n      fileName: b.fileName,\n      mimeType: b.mimeType,\n    };\n  }\n  return null;\n}\n\nfunction readFromDownloadNode() {\n  try {\n    const dl = $items('ec_download_existing_file')[0];\n    const b = dl?.binary?.data;\n    if (b?.data && typeof b.data === 'string') {\n      // fileId/fileName podem também estar em dl.json dependendo do seu fluxo\n      return {\n        b64: b.data,\n        fileName: b.fileName || dl?.json?.fileName,\n        mimeType: b.mimeType || dl?.json?.mimeType,\n        fileId: dl?.json?.fileId,\n      };\n    }\n  } catch {}\n  return null;\n}\n\nfunction readTextFallback() {\n  // Casos raros em que veio texto no JSON\n  if (typeof $json?.data === 'string')    return $json.data;\n  if (typeof $json?.body === 'string')    return $json.body;\n  if (typeof $json?.content === 'string') return $json.content;\n  return '';\n}\n\nfunction b64ToUtf8(b64) {\n  try {\n    return Buffer.from(b64, 'base64').toString('utf8');\n  } catch {\n    return '';\n  }\n}\n\nfunction normalize(text) {\n  return String(text)\n    .replace(/^\\uFEFF/, '')           // remove BOM, se houver\n    .replace(/^```(?:txt|text|json)?\\s*/i, '') // remove cercas de código no início\n    .replace(/\\s*```$/, '')           // remove cerca de fechamento\n    .replace(/\\r\\n/g, '\\n');          // normaliza quebras de linha\n}\n\n// --- Coleta do conteúdo ---\nlet meta = readFromCurrentItem();\nif (!meta) meta = readFromDownloadNode();\n\nlet text = '';\nlet fileName, mimeType, fileId;\n\nif (meta?.b64) {\n  text = b64ToUtf8(meta.b64);\n  fileName = meta.fileName;\n  mimeType = meta.mimeType;\n  fileId   = meta.fileId;\n} else {\n  text = readTextFallback();\n}\n\n// Normaliza e prepara saída\ntext = normalize(text);\n\n// Tenta herdar fileId/fileName do JSON atual se não vieram do download\nif (!fileId)   fileId   = $json?.fileId;\nif (!fileName) fileName = $json?.fileName;\nif (!mimeType) mimeType = $json?.mimeType || 'text/plain';\n\nreturn [{\n  json: {\n    content: text,\n    fileId,\n    fileName,\n    mimeType,\n    length: text.length,\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "2f8fb3d1-5bbd-4d3b-affc-b2090abf83a0",
      "name": "2_DefySec_Control_Composer",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        -384,
        -784
      ],
      "parameters": {
        "text": "=CloudProvider:  {{ $('settings').first().json.cloudprovider }}\nTechnology: {{ $('settings').first().json.technology }}\n\n{{ $json.content }}",
        "prompt": "define",
        "options": {},
        "resource": "assistant",
        "assistantId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('settings').first().json.assistant_composer_id }}"
        }
      },
      "credentials": {
        "openAiApi": {
          "id": "Vkw9T5qrK2yHZxw7",
          "name": "OpenAi account"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "8c37cafd-51d0-419b-ba2c-ae0b3b54a8c9",
      "name": "ec_controls_check",
      "type": "n8n-nodes-base.if",
      "position": [
        -32,
        -208
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f1eb6945-e389-44de-b327-d93afef4a987",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.output }}",
              "rightValue": "NO_CONTROLS_FOUND"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "cb935c32-d1b2-4566-baf1-316f95ac26aa",
      "name": "cc_controls_router",
      "type": "n8n-nodes-base.switch",
      "position": [
        -32,
        -784
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "69d835f1-aa34-4931-8b40-91088a9cf68a",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.output }}",
                    "rightValue": "NO_CONTROLS_FOUND"
                  }
                ]
              }
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "typeVersion": 3.2
    },
    {
      "id": "f1d5fd0e-e481-4b91-af07-804d02098c07",
      "name": "cc_no_controls_answer",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        192,
        -784
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "{\n  \"result\": \"NO_CONTROLS_FOUND\",\n  \"message\": \"Nenhum controle válido foi identificado. O arquivo está vazio ou não contém blocos no padrão esperado (Description, Reference, SecurityObjective) ou o cabeçalho CloudProvider/Technology está ausente.\",\n  \"next_steps\": [\n    \"Garanta as duas primeiras linhas: 'CloudProvider:' e 'Technology:'.\",\n    \"Inclua ao menos um bloco válido com Description, Reference (URL) e SecurityObjective.\",\n    \"Remova textos/JSONs fora do padrão entre os blocos.\"\n  ]\n}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "c7e68f8c-abe4-443f-a65f-efef1a5e3b6f",
      "name": "3_DefySec Baseline Builder",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        416,
        -592
      ],
      "parameters": {
        "text": "=CloudProvider:  {{ $('settings').first().json.cloudprovider }}\nTechnology: {{ $('settings').first().json.technology }}\n\n{{ $json.output }}",
        "prompt": "define",
        "options": {},
        "resource": "assistant",
        "assistantId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('settings').first().json.assistant_baseline_id }}"
        }
      },
      "credentials": {
        "openAiApi": {
          "id": "Vkw9T5qrK2yHZxw7",
          "name": "OpenAi account"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "56553247-2b0d-44ae-a8df-d3c2f42f10ef",
      "name": "cc_controls_check",
      "type": "n8n-nodes-base.code",
      "position": [
        192,
        -592
      ],
      "parameters": {
        "jsCode": "// === cc_route_on_no_controls ===\n// Se output === \"NO_CONTROLS_TO_BE_CONSOLIDATED\", substitui o payload pelo json de cc_extract_file_info.\n// Senão, apenas repassa o item original.\n//\n// Compatível com:\n// 1) { json: { output: \"NO_CONTROLS_TO_BE_CONSOLIDATED\", threadId: \"...\" } }\n// 2) { json: [ { output: \"NO_CONTROLS_TO_BE_CONSOLIDATED\", threadId: \"...\" } ] }\n\nfunction getOutputValue(payload) {\n  if (payload == null) return '';\n  if (Array.isArray(payload)) {\n    const first = payload[0];\n    return typeof first?.output === 'string' ? first.output.trim() : '';\n  }\n  if (typeof payload === 'object') {\n    return typeof payload.output === 'string' ? payload.output.trim() : '';\n  }\n  if (typeof payload === 'string') return payload.trim();\n  return '';\n}\n\nfunction getThreadId(payload) {\n  if (payload == null) return undefined;\n  if (Array.isArray(payload)) return payload[0]?.threadId;\n  if (typeof payload === 'object') return payload.threadId;\n  return undefined;\n}\n\nfunction getCcInfo() {\n  try {\n    const n = $items('cc_extract_file_info')[0];\n    const j = n?.json ?? {};\n\n    // settings e uuid (uuid pode estar dentro de settings ou na raiz, por segurança)\n    const settings = j.settings ?? {};\n    const uuid = String((settings.uuid ?? j.uuid ?? '')).trim();\n\n    // opcionalmente preserva alguns metadados úteis se existirem\n    const meta = {};\n    for (const k of ['fileId', 'fileName', 'mimeType', 'path', 'size']) {\n      if (j[k] !== undefined) meta[k] = j[k];\n    }\n\n    return { settings, uuid, ...meta };\n  } catch {\n    return null;\n  }\n}\n\nlet itemsIn;\ntry {\n  itemsIn = $input.all(); // n8n Code node novo\n} catch {\n  // fallback (algumas versões)\n  itemsIn = [{ json: $json, binary: $binary }];\n}\n\nconst itemsOut = itemsIn.map((item) => {\n  const outVal = getOutputValue(item.json);\n  const outValUC = outVal.toUpperCase();\n\n  if (outValUC === 'NO_CONTROLS_TO_BE_CONSOLIDATED') {\n    const info = getCcInfo();\n    // Se não conseguir ler cc_extract_file_info, mantém o item original para não quebrar o fluxo\n    if (!info) return item;\n\n    // opcional: mantém o threadId original (se existir) para rastreabilidade\n    const threadId = getThreadId(item.json);\n    if (threadId) info.threadId = threadId;\n\n    return { json: info };\n  }\n\n  // Qualquer outra resposta: apenas passa adiante sem alterações\n  return item;\n});\n\nreturn itemsOut;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "7509b484-dbb7-416e-b7a5-c1fc02faf16a",
      "name": "bb_controls_check",
      "type": "n8n-nodes-base.if",
      "position": [
        768,
        -592
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f1eb6945-e389-44de-b327-d93afef4a987",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.output }}",
              "rightValue": "NO_CONTROLS_FOUND"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "1f9c0594-b3e0-41e2-9a9e-003bcc60c828",
      "name": "bb_no_controls_answer",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        992,
        -496
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "{\n  \"result\": \"NO_CONTROLS_FOUND\",\n  \"message\": \"Nenhum controle válido foi identificado. O arquivo está vazio ou não contém blocos no padrão esperado (Description, Reference, SecurityObjective) ou o cabeçalho CloudProvider/Technology está ausente.\",\n  \"next_steps\": [\n    \"Garanta as duas primeiras linhas: 'CloudProvider:' e 'Technology:'.\",\n    \"Inclua ao menos um bloco válido com Description, Reference (URL) e SecurityObjective.\",\n    \"Remova textos/JSONs fora do padrão entre os blocos.\"\n  ]\n}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "5ea33aaf-7f97-4ceb-97e0-1023405aad4b",
      "name": "bb_data_prep",
      "type": "n8n-nodes-base.code",
      "position": [
        992,
        -688
      ],
      "parameters": {
        "jsCode": "// === make_file_from_output ===\n// Lê o(s) \"output(s)\" recebido(s) e gera 1 arquivo texto em binary.data\n// Saída: [{ json: { fileName, mimeType, bytes }, binary: { data: { data, mimeType, fileName } } }]\n\nfunction extractOutputFromJson(j) {\n  if (j == null) return '';\n  if (typeof j === 'string') return j;\n\n  if (Array.isArray(j)) {\n    // junta todos os outputs de um array (se houver mais de um)\n    const parts = j\n      .map(o => (o && typeof o.output === 'string') ? o.output : '')\n      .filter(Boolean);\n    if (parts.length) return parts.join('\\n\\n---\\n\\n');\n    // fallback: se o array não tem \"output\", tenta serializar\n    try { return JSON.stringify(j, null, 2); } catch { return ''; }\n  }\n\n  if (typeof j === 'object') {\n    if (typeof j.output === 'string')   return j.output;\n    if (typeof j.content === 'string')  return j.content;\n    if (typeof j.data === 'string')     return j.data;\n    if (typeof j.body === 'string')     return j.body;\n    try { return JSON.stringify(j, null, 2); } catch {}\n  }\n  return '';\n}\n\nfunction stripCodeFences(s) {\n  return String(s)\n    .replace(/^\\s*```[a-z]*\\s*/i, '') // remove ```txt / ```json / ``` etc no início\n    .replace(/\\s*```[\\s\\r\\n]*$/i, '') // remove ``` no fim\n    .replace(/^\\uFEFF/, '')           // remove BOM se houver\n    .replace(/\\r\\n/g, '\\n');          // normaliza quebras\n}\n\nfunction detectTechnology(s) {\n  const m = s.match(/Technology:\\s*([^\\n]+)/i);\n  return m ? m[1].trim().replace(/[^\\w.-]+/g, '_') : null;\n}\n\nconst itemsIn = $input.all();\n\n// Coleta e normaliza conteúdos\nconst contents = [];\nfor (const it of itemsIn) {\n  const raw = extractOutputFromJson(it.json);\n  if (!raw) continue;\n  contents.push(stripCodeFences(raw));\n}\n\nconst content = (contents.join('\\n\\n---\\n\\n') || '').trim();\n\n// Gera nome de arquivo\nconst tech = detectTechnology(content) || 'output';\nconst ts = new Date().toISOString().replace(/[:.]/g, '-'); // seguro para filename\nconst fileName = `controls_${tech}_${ts}.txt`;\nconst mimeType = 'text/plain';\n\n// Se não houver conteúdo, ainda retornamos um arquivo com nota (evita quebrar o fluxo)\nconst finalText = content || 'NO_CONTENT_EXTRACTED';\nconst bytes = Buffer.byteLength(finalText, 'utf8');\nconst b64 = Buffer.from(finalText, 'utf8').toString('base64');\n\n// Retorno no formato esperado pelo n8n\nreturn [{\n  json: { fileName, mimeType, bytes },\n  binary: {\n    data: {\n      data: b64,\n      mimeType,\n      fileName,\n    },\n  },\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "c1e047f4-83f6-45e9-a550-3e54c10eb919",
      "name": "get_gdrive_id",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        -2176,
        -64
      ],
      "parameters": {
        "filter": {},
        "options": {},
        "resource": "fileFolder",
        "queryString": "n8n_defysec"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "id": "tMSvCpu4CSKuDXkh",
          "name": "Google Drive account"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "6a21ef73-0859-4b51-8dc9-11b70af0a8d9",
      "name": "resolve_assistants",
      "type": "n8n-nodes-base.code",
      "position": [
        -1728,
        -64
      ],
      "parameters": {
        "jsCode": "// Lê todos os itens de entrada do nó anterior\nconst all = $input.all(); // [{json: {...}}, ...]\n\n// Normaliza: pode ser (a) vários itens simples, (b) 1 item com `data[]`, (c) 1 item com array plano\nlet list;\nif (all.length === 1 && Array.isArray(all[0].json?.data)) {\n  list = all[0].json.data;                // caso: { data: [...] }\n} else if (all.length === 1 && Array.isArray(all[0].json)) {\n  list = all[0].json;                     // caso: [{id,name,model}, ...] dentro do json\n} else {\n  list = all.map(i => i.json);            // caso: cada item já é {id,name,model}\n}\n\n// Helpers: escolha por nome (ajuste os regex se renomear seus assistants)\nconst pick = (re) => list.find(a => re.test(String(a.name || '')));\n\n// Heurística pelos seus nomes:\nconst extractor = pick(/(^|[\\s_-])1[\\s_-]*DefySec[\\s_-]*Extractor\\b/i) || pick(/\\bExtractor\\b/i);\nconst composer  = pick(/(^|[\\s_-])2[\\s_-]*DefySec[\\s_-]*Control[\\s_-]*Composer\\b/i) || pick(/\\bComposer\\b/i);\nconst baseline  = pick(/(^|[\\s_-])3[\\s_-]*DefySec[\\s_-]*Baseline\\b/i) || pick(/\\bBaseline\\b/i);\n\n// Saída única com os IDs resolvidos\nreturn [\n  {\n    json: {\n      assistant_extractor_id: extractor?.id || '',\n      assistant_composer_id:  composer?.id  || '',\n      assistant_baseline_id:  baseline?.id  || ''\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "85305d1a-d4de-4917-9f16-09000677a767",
      "name": "OpenAI_Assistants_List",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        -1952,
        -64
      ],
      "parameters": {
        "resource": "assistant",
        "operation": "list"
      },
      "credentials": {
        "openAiApi": {
          "id": "Vkw9T5qrK2yHZxw7",
          "name": "OpenAi account"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "08f933aa-5892-442a-83c6-5e5d365ca0e0",
      "name": "bb_data_respond",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1216,
        -688
      ],
      "parameters": {
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Disposition",
                "value": "=attachment; filename=\"{{ $binary.data.fileName }}\""
              }
            ]
          }
        },
        "respondWith": "binary"
      },
      "typeVersion": 1.4
    },
    {
      "id": "bfd45e14-84e0-44d5-84f8-c90527f2efed",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2928,
        -672
      ],
      "parameters": {
        "color": 5,
        "width": 608,
        "height": 336,
        "content": "## Overview\nThis template turns provider docs (URLs) into an **auditable security baseline**:\n1) POST **/create** (Basic Auth) → validate & generate `uuid`\n2) Resolve Google Drive folder (search-or-create)\n3) Download & sanitize each URL (no scripts/styles/headers)\n4) AI pipeline: **Extractor → Composer → Baseline Builder** (TXT-only contracts)\n5) Append/create file in Drive and return a downloadable **.txt**\n"
      },
      "typeVersion": 1
    },
    {
      "id": "c7496163-e016-4565-ab9e-adf1ce42e0a5",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2288,
        -672
      ],
      "parameters": {
        "color": 5,
        "width": 608,
        "height": 336,
        "content": "## Setup & Credentials\n- **OpenAI**: select your credential (no API keys in HTTP headers)\n- **Google Drive OAuth2**: read/write file\n- **Basic Auth**: protects `/create` endpoint\n\n**Drive folder**\n- Auto-resolves folder `n8n_defysec` in **root** (search-or-create).\n- Optional override in POST: `\"gdriveTargetId\": \"<folderId>\"`.\n\n**Assistants**\n- Resolved dynamically from your account by *name*:\n  `1_DefySec_Extractor`, `2_DefySec_Control_Composer`, `3_DefySec Baseline Builder`.\n- Optional overrides in POST:\n  `assistantExtractorId`, `assistantComposerId`, `assistantBaselineId`.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "5a7828b9-f6d5-4636-be1a-a1c69272d0d1",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1648,
        -672
      ],
      "parameters": {
        "color": 5,
        "width": 608,
        "height": 336,
        "content": "## Run & Troubleshooting\n- **Test**: Use “Test Webhook” and POST `{ cloudProvider, technology, urls[] }`.\n- **No results?** Ensure pages return HTML and follow the TXT contracts (3-line Extractor, 7-line Composer).\n- **Drive search**: Queries include `'folderId' in parents`; confirm `gdrive_target` is valid.\n- **Security**: HTTP node has no hardcoded API keys. Keep credentials in n8n’s Credential Manager."
      },
      "typeVersion": 1
    }
  ],
  "pinData": {},
  "connections": {
    "create": {
      "main": [
        [
          {
            "node": "check_mandatory_fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "settings": {
      "main": [
        [
          {
            "node": "explode_urls",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "process_url": {
      "main": [
        [
          {
            "node": "cc_search_files",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "http_get_url",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "bb_data_prep": {
      "main": [
        [
          {
            "node": "bb_data_respond",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "explode_urls": {
      "main": [
        [
          {
            "node": "process_url",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "http_get_url": {
      "main": [
        [
          {
            "node": "html_sanitizer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ec_merge_data": {
      "main": [
        [
          {
            "node": "ec_update_existing_file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "generate_uuid": {
      "main": [
        [
          {
            "node": "get_gdrive_id",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "get_gdrive_id": {
      "main": [
        [
          {
            "node": "OpenAI_Assistants_List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "html_sanitizer": {
      "main": [
        [
          {
            "node": "1_DefySec_Extractor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "cc_search_files": {
      "main": [
        [
          {
            "node": "cc_extract_file_info",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ec_search_files": {
      "main": [
        [
          {
            "node": "ec_extract_file_info",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "bb_controls_check": {
      "main": [
        [
          {
            "node": "bb_data_prep",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "bb_no_controls_answer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "cc_controls_check": {
      "main": [
        [
          {
            "node": "3_DefySec Baseline Builder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ec_controls_check": {
      "main": [
        [
          {
            "node": "ec_search_files",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "process_url",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "cc_controls_router": {
      "main": [
        [
          {
            "node": "cc_no_controls_answer",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "cc_controls_check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ec_upload_new_file": {
      "main": [
        [
          {
            "node": "process_url",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "resolve_assistants": {
      "main": [
        [
          {
            "node": "settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1_DefySec_Extractor": {
      "main": [
        [
          {
            "node": "ec_controls_check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "cc_extract_file_info": {
      "main": [
        [
          {
            "node": "2_DefySec_Control_Composer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ec_extract_file_info": {
      "main": [
        [
          {
            "node": "ec_append_create_filter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI_Assistants_List": {
      "main": [
        [
          {
            "node": "resolve_assistants",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "check_mandatory_fields": {
      "main": [
        [
          {
            "node": "generate_uuid",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No Operation, do nothing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ec_append_create_filter": {
      "main": [
        [
          {
            "node": "ec_download_existing_file",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "ec_upload_new_file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ec_update_existing_file": {
      "main": [
        [
          {
            "node": "process_url",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ec_download_existing_file": {
      "main": [
        [
          {
            "node": "ec_merge_data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2_DefySec_Control_Composer": {
      "main": [
        [
          {
            "node": "cc_controls_router",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3_DefySec Baseline Builder": {
      "main": [
        [
          {
            "node": "bb_controls_check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

相关工作流