{
  "id": "QiqRAiXGPRoDxv1q",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Multicloud AI Security Control Baseline Builder",
  "tags": [],
  "nodes": [
    {
      "id": "c76e4bd4-dbdf-4e35-808e-b225e66427e6",
      "name": "check_mandatory_fields",
      "type": "n8n-nodes-base.if",
      "position": [
        -2896,
        112
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "77e24e89-4efa-4d68-b5d3-b8c8252d8834",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.ok }}",
              "rightValue": "true"
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.2
    },
    {
      "id": "1cbc1ca7-e5db-4e5a-a1f7-e774a70e1201",
      "name": "generate_uuid",
      "type": "n8n-nodes-base.code",
      "position": [
        -2384,
        -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": [
        -3344,
        112
      ],
      "parameters": {
        "path": "create",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode",
        "authentication": "basicAuth"
      },
      "credentials": {
        "httpBasicAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "6ede342c-2ad9-41cc-b11c-59186de696ea",
      "name": "settings",
      "type": "n8n-nodes-base.set",
      "position": [
        -1488,
        -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 }}"
            },
            {
              "id": "d7a2904d-c902-4fc4-a324-242fdcbce4f6",
              "name": "assistant_auditor_id",
              "type": "string",
              "value": "={{ $json.assistant_auditor_id }}"
            },
            {
              "id": "fe6338d2-b1eb-4ae8-ab74-009adb8c52f5",
              "name": "assistant_reviewer_id",
              "type": "string",
              "value": "={{ $json.assistant_reviewer_id }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "da40bbec-d7ea-4499-a6d7-09762f81c529",
      "name": "http_get_url",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -816,
        -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": [
        -592,
        -208
      ],
      "parameters": {
        "jsCode": "// 1. Pega o conte\u00fado HTML\nconst htmlRaw = $json.data || $json.body;\n\nif (!htmlRaw || typeof htmlRaw !== 'string') {\n  throw new Error('Campo `data` ou `body` ausente ou inv\u00e1lido.');\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\u00f3 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": [
        -368,
        -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": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "7278026a-c1ef-49b9-950d-d924e4f2aca9",
      "name": "explode_urls",
      "type": "n8n-nodes-base.code",
      "position": [
        -1264,
        -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": [
        -1040,
        -64
      ],
      "parameters": {
        "options": {
          "reset": false
        }
      },
      "typeVersion": 3
    },
    {
      "id": "08a7fbde-12c2-4efd-9acf-186a2a61d352",
      "name": "ec_search_files",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        208,
        -208
      ],
      "parameters": {
        "filter": {},
        "options": {
          "fields": [
            "*"
          ]
        },
        "resource": "fileFolder",
        "returnAll": true,
        "queryString": "={{$items(\"settings\")[0].json.uuid}}_extractedControls_"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 3,
      "alwaysOutputData": true
    },
    {
      "id": "012ae0a1-1bf0-4a67-9c1c-1de9a3758f07",
      "name": "ec_append_create_filter",
      "type": "n8n-nodes-base.if",
      "position": [
        784,
        -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": [
        1008,
        -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": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "1f9e5eb6-bad4-430e-9998-82a7322dcddf",
      "name": "ec_update_existing_file",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        1456,
        -208
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.fileId }}"
        },
        "options": {},
        "operation": "update",
        "changeFileContent": true
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "df8ef747-d3e1-4d5e-a77d-da693f5baf38",
      "name": "ec_download_existing_file",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        1008,
        -304
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.fileId }}"
        },
        "options": {},
        "operation": "download"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "42a8de07-049e-4038-b942-d27f7e425bfa",
      "name": "ec_merge_data",
      "type": "n8n-nodes-base.code",
      "position": [
        1232,
        -304
      ],
      "parameters": {
        "jsCode": "// === ec_merge_data \u2014 append determin\u00edstico (.txt) ===\n\n// 1) L\u00ea o texto atual (prioriza binary.data.data do item corrente; fallback no n\u00f3 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\u00f3 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\u00e1 existe conte\u00fado)\nconst combined = prevText\n  ? prevText.replace(/\\s*$/, '') + '\\n\\n' + newText.replace(/^\\s+/, '')\n  : newText;\n\n// 4) Empacota bin\u00e1rio 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 (\u00e9 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": [
        496,
        -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\u00f3 \"settings\".');\nif (!folderId) throw new Error('gdrive_target (folderId) ausente no n\u00f3 \"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\u00e7\u00e3o\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\u00e7\u00e3o, j\u00e1 emite bin\u00e1rio pra Upload; se for append, s\u00f3 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": [
        -816,
        -992
      ],
      "parameters": {
        "filter": {},
        "options": {
          "fields": [
            "*"
          ]
        },
        "resource": "fileFolder",
        "returnAll": true,
        "queryString": "={{$items(\"settings\")[0].json.uuid}}_extractedControls.txt"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 3,
      "alwaysOutputData": true
    },
    {
      "id": "1c259737-687a-4862-8052-24290b3f4557",
      "name": "cc_extract_file_info",
      "type": "n8n-nodes-base.code",
      "position": [
        -592,
        -992
      ],
      "parameters": {
        "jsCode": "// === ec_extract_content \u2014 l\u00ea do Google Drive (ou do item atual) e repassa ===\n// Sa\u00edda: [{ 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\u00e9m 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\u00f3digo no in\u00edcio\n    .replace(/\\s*```$/, '')           // remove cerca de fechamento\n    .replace(/\\r\\n/g, '\\n');          // normaliza quebras de linha\n}\n\n// --- Coleta do conte\u00fado ---\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\u00edda\ntext = normalize(text);\n\n// Tenta herdar fileId/fileName do JSON atual se n\u00e3o 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": [
        -368,
        -992
      ],
      "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": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "8c37cafd-51d0-419b-ba2c-ae0b3b54a8c9",
      "name": "ec_controls_check",
      "type": "n8n-nodes-base.if",
      "position": [
        -16,
        -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": [
        -16,
        -992
      ],
      "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": [
        208,
        -992
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "{\n  \"result\": \"NO_CONTROLS_FOUND\",\n  \"message\": \"Nenhum controle v\u00e1lido foi identificado. O arquivo est\u00e1 vazio ou n\u00e3o cont\u00e9m blocos no padr\u00e3o esperado (Description, Reference, SecurityObjective) ou o cabe\u00e7alho CloudProvider/Technology est\u00e1 ausente.\",\n  \"next_steps\": [\n    \"Garanta as duas primeiras linhas: 'CloudProvider:' e 'Technology:'.\",\n    \"Inclua ao menos um bloco v\u00e1lido com Description, Reference (URL) e SecurityObjective.\",\n    \"Remova textos/JSONs fora do padr\u00e3o 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": [
        432,
        -800
      ],
      "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": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "56553247-2b0d-44ae-a8df-d3c2f42f10ef",
      "name": "cc_controls_check",
      "type": "n8n-nodes-base.code",
      "position": [
        208,
        -800
      ],
      "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\u00e3o, apenas repassa o item original.\n//\n// Compat\u00edvel 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\u00e7a)\n    const settings = j.settings ?? {};\n    const uuid = String((settings.uuid ?? j.uuid ?? '')).trim();\n\n    // opcionalmente preserva alguns metadados \u00fateis 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\u00f5es)\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\u00e3o conseguir ler cc_extract_file_info, mant\u00e9m o item original para n\u00e3o quebrar o fluxo\n    if (!info) return item;\n\n    // opcional: mant\u00e9m 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\u00e7\u00f5es\n  return item;\n});\n\nreturn itemsOut;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "5ea33aaf-7f97-4ceb-97e0-1023405aad4b",
      "name": "bb_data_prep",
      "type": "n8n-nodes-base.code",
      "position": [
        2144,
        -912
      ],
      "parameters": {
        "jsCode": "// === make_file_from_output ===\n// Fonte \u00fanica: $('controls_transfer_area').first().json.data\n// Salva .json se o conte\u00fado for JSON v\u00e1lido; caso contr\u00e1rio, .txt.\n\nconst SOURCE_NODE = 'controls_transfer_area';\n\n// ---------- helpers ----------\nfunction stripCodeFences(s) {\n  return String(s)\n    .replace(/^\\s*```[a-z]*\\s*/i, '')\n    .replace(/\\s*```[\\s\\r\\n]*$/i, '')\n    .replace(/^\\uFEFF/, '')\n    .replace(/\\r\\n/g, '\\n')\n    .trim();\n}\n\nfunction isValidJsonObject(s) {\n  try {\n    const x = JSON.parse(s);\n    return x && typeof x === 'object';\n  } catch {\n    return false;\n  }\n}\n\n// Aceita objeto ou string; tenta extrair 'technology'\nfunction detectTechnologySmartAny(input) {\n  try {\n    if (input && typeof input === 'object') {\n      if (input.technology) {\n        return String(input.technology).trim().replace(/[^\\w.-]+/g, '_');\n      }\n      if (input.output && typeof input.output === 'object' && input.output.technology) {\n        return String(input.output.technology).trim().replace(/[^\\w.-]+/g, '_');\n      }\n    }\n  } catch {}\n  const s = typeof input === 'string' ? input : JSON.stringify(input ?? '');\n  try {\n    const o = JSON.parse(s);\n    if (o?.technology) return String(o.technology).trim().replace(/[^\\w.-]+/g, '_');\n  } catch {}\n  const m = s.match(/\"technology\"\\s*:\\s*\"([^\"]+)\"/i);\n  if (m) return m[1].trim().replace(/[^\\w.-]+/g, '_');\n  const m2 = s.match(/Technology:\\s*([^\\n]+)/i);\n  return m2 ? m2[1].trim().replace(/[^\\w.-]+/g, '_') : null;\n}\n\n// ---------- coleta somente do n\u00f3-fonte ----------\nlet srcItems = [];\ntry {\n  srcItems = $items(SOURCE_NODE, 0) || [];\n} catch (e) {\n  srcItems = [];\n}\n\n// Se n\u00e3o houver nada no n\u00f3-fonte, retorna arquivo \"vazio\" para n\u00e3o quebrar o fluxo\nif (!Array.isArray(srcItems) || srcItems.length === 0) {\n  const finalText = 'NO_CONTENT_FROM_SOURCE_NODE';\n  const fileName = 'controls_output.txt';\n  const mimeType = 'text/plain';\n  const b64 = Buffer.from(finalText, 'utf8').toString('base64');\n  return [{\n    json: {\n      fileName,\n      mimeType,\n      bytes: Buffer.byteLength(finalText, 'utf8'),\n      sourceNode: SOURCE_NODE,\n      itemsFromSource: 0\n    },\n    binary: { data: { data: b64, mimeType, fileName } }\n  }];\n}\n\n// Primeiro item do controls_transfer_area\nconst first = srcItems[0] || {};\n\n// === alvo principal: .json.data ===\nlet rawCandidate;\nif (first.json && Object.prototype.hasOwnProperty.call(first.json, 'data')) {\n  rawCandidate = first.json.data;\n} else if (first.json && Object.prototype.hasOwnProperty.call(first.json, 'output')) {\n  // fallback amig\u00e1vel se algu\u00e9m ainda estiver usando \"output\"\n  rawCandidate = first.json.output;\n} else {\n  // fallback final: o pr\u00f3prio json\n  rawCandidate = first.json;\n}\n\n// Normaliza para string final e decide se \u00e9 JSON\nlet finalText = 'NO_CONTENT_EXTRACTED';\nlet mimeType = 'text/plain';\nlet ext = 'txt';\n\nif (typeof rawCandidate === 'string') {\n  const cleaned = stripCodeFences(rawCandidate);\n  finalText = cleaned || 'NO_CONTENT_EXTRACTED';\n  if (isValidJsonObject(finalText)) {\n    mimeType = 'application/json';\n    ext = 'json';\n  }\n} else if (rawCandidate && typeof rawCandidate === 'object') {\n  // Caso o .data seja um objeto \u2014 inclusive se tiver { output: ... }\n  if (rawCandidate && typeof rawCandidate.output === 'string') {\n    const cleaned = stripCodeFences(rawCandidate.output);\n    finalText = cleaned || 'NO_CONTENT_EXTRACTED';\n    if (isValidJsonObject(finalText)) {\n      mimeType = 'application/json';\n      ext = 'json';\n    }\n  } else if (rawCandidate && typeof rawCandidate.output === 'object') {\n    finalText = JSON.stringify(rawCandidate.output, null, 2);\n    mimeType = 'application/json';\n    ext = 'json';\n  } else {\n    finalText = JSON.stringify(rawCandidate, null, 2);\n    mimeType = 'application/json';\n    ext = 'json';\n  }\n}\n\n// Nome do arquivo\nlet tech;\ntry {\n  tech = detectTechnologySmartAny(\n    mimeType === 'application/json' ? JSON.parse(finalText) : finalText\n  ) || 'output';\n} catch {\n  tech = 'output';\n}\n\nconst ts = new Date().toISOString().replace(/[:.]/g, '-');\nconst fileName = `controls_${tech}_${ts}.${ext}`;\n\n// Bin\u00e1rio\nconst bytes = Buffer.byteLength(finalText, 'utf8');\nconst b64 = Buffer.from(finalText, 'utf8').toString('base64');\n\n// Retorno para n8n\nreturn [{\n  json: {\n    fileName,\n    mimeType,\n    bytes,\n    sourceNode: SOURCE_NODE,\n    itemsFromSource: srcItems.length\n  },\n  binary: { data: { data: b64, mimeType, fileName } },\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "c1e047f4-83f6-45e9-a550-3e54c10eb919",
      "name": "get_gdrive_id",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        -2160,
        -64
      ],
      "parameters": {
        "filter": {},
        "options": {},
        "resource": "fileFolder",
        "queryString": "n8n_defysec"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "6a21ef73-0859-4b51-8dc9-11b70af0a8d9",
      "name": "resolve_assistants",
      "type": "n8n-nodes-base.code",
      "position": [
        -1712,
        -64
      ],
      "parameters": {
        "jsCode": "// L\u00ea todos os itens de entrada do n\u00f3 anterior\nconst all = $input.all(); // [{json: {...}}, ...]\n\n// Normaliza: (a) v\u00e1rios 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\u00e1 \u00e9 {id,name,model}\n}\n\n// Garante array plano de objetos v\u00e1lidos\nlist = list\n  .flatMap(x => Array.isArray(x) ? x : [x])\n  .filter(x => x && typeof x === 'object');\n\n// Helper: escolher por nome usando uma lista de regex (primeiro que casar)\nconst pick = (...regexes) => {\n  for (const re of regexes) {\n    const hit = list.find(a => re.test(String(a.name || '')));\n    if (hit) return hit;\n  }\n  return undefined;\n};\n\n// 1) Extractor (1_)\nconst extractor = pick(\n  /(^|[\\s_-])1[\\s_-]*DefySec[\\s_-]*Extractor\\b/i,\n  /\\bDefySec[\\s_-]*Extractor\\b/i,\n  /\\bExtractor\\b/i\n);\n\n// 2) Control Composer (2_)\nconst composer  = pick(\n  /(^|[\\s_-])2[\\s_-]*DefySec[\\s_-]*Control[\\s_-]*Composer\\b/i,\n  /\\bDefySec[\\s_-]*Control[\\s_-]*Composer\\b/i,\n  /\\bComposer\\b/i\n);\n\n// 3) Baseline Builder (3_)\nconst baseline  = pick(\n  /(^|[\\s_-])3[\\s_-]*DefySec[\\s_-]*(Baseline[\\s_-]*Builder|Baseline)\\b/i,\n  /\\bDefySec[\\s_-]*Baseline[\\s_-]*Builder\\b/i,\n  /\\bBaseline[\\s_-]*Builder\\b/i\n);\n\n// 4) Auditor (4_)\nconst auditor   = pick(\n  /(^|[\\s_-])4[\\s_-]*DefySec[\\s_-]*Baseline[\\s_-]*Auditor\\b/i,\n  /\\bDefySec[\\s_-]*Baseline[\\s_-]*Auditor\\b/i,\n  /\\bBaseline[\\s_-]*Auditor\\b/i,\n  /\\bAuditor\\b/i\n);\n\n// 5) Reviewer (5_) \u2014 nome oficial atual\nconst reviewer  = pick(\n  /(^|[\\s_-])5[\\s_-]*DefySec[\\s_-]*Baseline[\\s_-]*Reviewer\\b/i,\n  /\\bDefySec[\\s_-]*Baseline[\\s_-]*Reviewer\\b/i,\n  /\\bBaseline[\\s_-]*Reviewer\\b/i,\n  /\\bReviewer\\b/i\n);\n\n// Sa\u00edda \u00fanica apenas com os IDs esperados no fluxo atual\nreturn [\n  {\n    json: {\n      assistant_extractor_id: extractor?.id ?? '',\n      assistant_composer_id:  composer?.id  ?? '',\n      assistant_baseline_id:  baseline?.id  ?? '',\n      assistant_auditor_id:   auditor?.id   ?? '',\n      assistant_reviewer_id:  reviewer?.id  ?? ''\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "85305d1a-d4de-4917-9f16-09000677a767",
      "name": "OpenAI_Assistants_List",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        -1936,
        -64
      ],
      "parameters": {
        "resource": "assistant",
        "operation": "list"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "08f933aa-5892-442a-83c6-5e5d365ca0e0",
      "name": "bb_data_respond",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2320,
        -912
      ],
      "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": [
        -3312,
        -784
      ],
      "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) \u2192 validate & generate `uuid`\n2) Resolve Google Drive folder (search-or-create)\n3) Download & sanitize each URL (no scripts/styles/headers)\n4) AI pipeline: **Extractor \u2192 Composer \u2192 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": [
        -2656,
        -784
      ],
      "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": [
        -2000,
        -784
      ],
      "parameters": {
        "color": 5,
        "width": 608,
        "height": 336,
        "content": "## Run & Troubleshooting\n- **Test**: Use \u201cTest Webhook\u201d 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\u2019s Credential Manager."
      },
      "typeVersion": 1
    },
    {
      "id": "af5efaf9-7e4d-48bb-829f-8a70f479c7d2",
      "name": "input_validation_error",
      "type": "n8n-nodes-base.code",
      "position": [
        -3120,
        112
      ],
      "parameters": {
        "jsCode": "// n8n Code Node (JavaScript) \u2014 Valida\u00e7\u00e3o de payload (com fallback de URL)\n\n// Helpers\nfunction isNonEmptyString(v) {\n  return typeof v === 'string' && v.trim().length > 0;\n}\n\n// Regex simples e eficaz para http/https (sem espa\u00e7os)\nconst HTTP_URL_REGEX = /^https?:\\/\\/\\S+$/i;\n\nfunction validateHttpUrl(value) {\n  const s = String(value ?? '').trim();\n  if (!s) return { ok: false, value: s, reason: 'empty' };\n\n  // 1) Tenta usar global URL se existir\n  const hasURLCtor = (typeof URL !== 'undefined');\n  if (hasURLCtor) {\n    try {\n      const u = new URL(s);\n      if (u.protocol === 'http:' || u.protocol === 'https:') {\n        return { ok: true, value: s, via: 'URL' };\n      }\n      return { ok: false, value: s, reason: 'unsupported_protocol' };\n    } catch {\n      // cai para regex\n    }\n  }\n\n  // 2) Fallback: regex\n  if (HTTP_URL_REGEX.test(s)) {\n    return { ok: true, value: s, via: 'regex_fallback' };\n  }\n  return { ok: false, value: s, reason: hasURLCtor ? 'parse_error' : 'no_URL_ctor_and_regex_failed' };\n}\n\nreturn items.map(item => {\n  const req = item.json ?? {};\n\n  // Body pode vir como string (content-type errado) ou objeto\n  let b = null;\n  if (req && typeof req.body === 'string') {\n    try { b = JSON.parse(req.body); } catch { b = null; }\n  } else if (req && typeof req.body === 'object' && req.body !== null) {\n    b = req.body;\n  }\n\n  const required = ['cloudProvider', 'technology', 'urls'];\n  const missing = [];\n  const invalid = {};\n  const debug = {};\n\n  if (!b) {\n    missing.push(...required);\n  } else {\n    // cloudProvider\n    if (!isNonEmptyString(b.cloudProvider)) missing.push('cloudProvider');\n\n    // technology\n    if (!isNonEmptyString(b.technology)) missing.push('technology');\n\n    // urls\n    if (!Array.isArray(b.urls) || b.urls.length === 0) {\n      missing.push('urls');\n    } else {\n      const checks = b.urls.map(u => validateHttpUrl(u));\n      debug.urlChecks = checks;\n      const bad = checks.filter(c => !c.ok).map(c => ({ value: c.value, reason: c.reason }));\n      if (bad.length) invalid.urls = bad;\n    }\n  }\n\n  const hasIssues = missing.length > 0 || (invalid.urls?.length > 0);\n\n  const normalized = (!hasIssues && b) ? {\n    cloudProvider: String(b.cloudProvider).trim().toLowerCase(),\n    technology: String(b.technology).trim(),\n    urls: b.urls.map(u => String(u).trim()),\n  } : undefined;\n\n  return {\n    json: {\n      ok: !hasIssues,\n      error: hasIssues ? 'ValidationError' : null,\n      message: hasIssues\n        ? 'Payload inv\u00e1lido. Corrija os campos obrigat\u00f3rios antes de reenviar.'\n        : 'Payload v\u00e1lido.',\n      required,\n      missing,\n      invalid: Object.keys(invalid).length ? invalid : undefined,\n      receivedKeys: b ? Object.keys(b) : [],\n      _requestMeta: {\n        headers: req.headers ?? null,\n        params: req.params ?? null,\n        query: req.query ?? null,\n        webhookUrl: req.webhookUrl ?? null,\n        executionMode: req.executionMode ?? null,\n      },\n      normalized,\n      // campo de diagn\u00f3stico para entender POR QUE deu falso (remova em produ\u00e7\u00e3o)\n      _debug: debug,\n    }\n  };\n});\n"
      },
      "typeVersion": 2
    },
    {
      "id": "46c9bb0c-c804-481c-af6e-b9aaf28590b7",
      "name": "validation_failed_answer",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -2672,
        176
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{$json}}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "d0f2d694-cf7b-4b3d-bbd5-665213decf50",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3392,
        -352
      ],
      "parameters": {
        "color": 4,
        "width": 928,
        "height": 800,
        "content": "## Ingress and Validation"
      },
      "typeVersion": 1
    },
    {
      "id": "17fd8f81-3ff6-48af-b909-2d81f8753ebe",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2432,
        -352
      ],
      "parameters": {
        "color": 4,
        "width": 1088,
        "height": 800,
        "content": "## Gathering Information"
      },
      "typeVersion": 1
    },
    {
      "id": "187c586e-f431-442b-9323-ea51e77abbc9",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1312,
        -352
      ],
      "parameters": {
        "color": 4,
        "width": 896,
        "height": 800,
        "content": "## Processing URLs"
      },
      "typeVersion": 1
    },
    {
      "id": "2ea7d88e-ba2d-4b8b-92ac-5e5e24b2649a",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -400,
        -352
      ],
      "parameters": {
        "color": 4,
        "width": 2064,
        "height": 800,
        "content": "## Extracting Controls and Saving in GDrive"
      },
      "typeVersion": 1
    },
    {
      "id": "fea8f4e5-1c62-4460-8cc5-c7d32878c8f0",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1312,
        -1168
      ],
      "parameters": {
        "color": 4,
        "width": 1696,
        "height": 800,
        "content": "## Fetching the Controls File & Consolidation"
      },
      "typeVersion": 1
    },
    {
      "id": "0943865e-4399-4454-8175-bee69dceb660",
      "name": "4_DefySec_Baseline_Auditor",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        960,
        -800
      ],
      "parameters": {
        "text": "={{ $json.data }}",
        "prompt": "define",
        "options": {},
        "resource": "assistant",
        "assistantId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('settings').first().json.assistant_auditor_id }}"
        }
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "0fdb70de-16df-4789-acc5-85503bc85792",
      "name": "ba_controls_check",
      "type": "n8n-nodes-base.if",
      "onError": "continueRegularOutput",
      "position": [
        1312,
        -800
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f1eb6945-e389-44de-b327-d93afef4a987",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.output }}",
              "rightValue": "GOOD_ENOUGH"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "cbb18734-a691-4d10-8410-4f37978679a6",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        400,
        -1168
      ],
      "parameters": {
        "color": 4,
        "width": 1664,
        "height": 800,
        "content": "## Building a Baseline with the Self Evaluation Technique"
      },
      "typeVersion": 1
    },
    {
      "id": "eaea2cf3-2298-46f6-ad86-6ac3ff3b6cc6",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2096,
        -1168
      ],
      "parameters": {
        "color": 4,
        "width": 448,
        "height": 800,
        "content": "## Preparing Data & Providing Answers"
      },
      "typeVersion": 1
    },
    {
      "id": "2a940c07-448e-449b-8909-c9ae1b6283e5",
      "name": "ba_prep_feedback",
      "type": "n8n-nodes-base.code",
      "position": [
        1536,
        -720
      ],
      "parameters": {
        "jsCode": "// n8n Code Node (JavaScript)\n// Sa\u00edda: 1 item com { Original_Data, Last_Version, Data_feedback }\n\nfunction normalize(val) {\n  if (val == null) return '';\n  if (typeof val === 'string') return val.trim();\n  if (Array.isArray(val)) return val.map(v => (typeof v === 'string' ? v : JSON.stringify(v))).join('\\n');\n  if (typeof val === 'object') return JSON.stringify(val, null, 2);\n  return String(val);\n}\n\n// Original_Data: $('3_DefySec Baseline Builder').first()?.json?.output\nlet originalRaw;\ntry {\n  originalRaw = $('3_DefySec Baseline Builder').first()?.json?.output;\n} catch {\n  originalRaw = undefined;\n}\n\n// Last_Version: $('controls_transfer_area').first().json.data\nlet lastVersionRaw;\ntry {\n  lastVersionRaw = $('controls_transfer_area').first()?.json?.data;\n} catch {\n  lastVersionRaw = undefined;\n}\n\n// Data_feedback: $input.first()?.json?.output\nconst feedbackRaw = $input.first()?.json?.output;\n\nreturn [\n  {\n    json: {\n      Original_Data: normalize(originalRaw),\n      Last_Version: normalize(lastVersionRaw),\n      Data_feedback: normalize(feedbackRaw),\n    },\n  },\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "0ef89bc7-48f4-4f8c-a72b-a22e76c5c5ec",
      "name": "5_DefySec Baseline Revisor",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1744,
        -720
      ],
      "parameters": {
        "text": "={{ $json.Original_Data }}\n{{ $json.Last_Version }}\n{{ $json.Data_feedback}}",
        "prompt": "define",
        "options": {},
        "resource": "assistant",
        "assistantId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('settings').first().json.assistant_reviewer_id }}"
        }
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "1ca102f6-6345-4dfd-a55e-113e94ee70d0",
      "name": "controls_transfer_area",
      "type": "n8n-nodes-base.set",
      "position": [
        768,
        -800
      ],
      "parameters": {
        "options": {
          "ignoreConversionErrors": true
        },
        "assignments": {
          "assignments": [
            {
              "id": "5cf97668-357f-49eb-b195-901998298172",
              "name": "data",
              "type": "string",
              "value": "={{ $json.output }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "c5e6276d-77d0-4f50-8f56-b854be4e4084",
  "connections": {
    "create": {
      "main": [
        [
          {
            "node": "input_validation_error",
            "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
          }
        ]
      ]
    },
    "ba_prep_feedback": {
      "main": [
        [
          {
            "node": "5_DefySec Baseline Revisor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ba_controls_check": {
      "main": [
        [
          {
            "node": "bb_data_prep",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "ba_prep_feedback",
            "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": "validation_failed_answer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "controls_transfer_area": {
      "main": [
        [
          {
            "node": "4_DefySec_Baseline_Auditor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "input_validation_error": {
      "main": [
        [
          {
            "node": "check_mandatory_fields",
            "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": "controls_transfer_area",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4_DefySec_Baseline_Auditor": {
      "main": [
        [
          {
            "node": "ba_controls_check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5_DefySec Baseline Revisor": {
      "main": [
        [
          {
            "node": "controls_transfer_area",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}