This workflow corresponds to n8n.io template #7529 — we link there as the canonical source.
This workflow follows the Google Drive → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"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
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
googleDriveOAuth2ApihttpBasicAuthopenAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Transforms provider documentation (URLs) into an auditable, enforceable multicloud security control baseline. It: Fetches and sanitizes HTML Uses AI to extract security requirements* (strict 3-line TXT blocks) Composes enforceable controls* (strict 7-line TXT blocks with…
Source: https://n8n.io/workflows/7529/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Listens for completed Fireflies transcripts, qualifies whether a proposal is needed using OpenAI, drafts structured proposal content, populates a Google Doc template, converts to PDF, and sends it to
This system meticulously guides each lead through a fully automated journey, from initial contact to a personalized follow-up and CRM integration.
A smart, fully automated coding pipeline built inside n8n that leverages Cursor AI to write, refactor, review, and optimize code projects — triggered by a webhook, schedule, or manual prompt. Every ou
Build a fully automated music generation workflow in n8n using Suno to create and store AI-generated songs.
Automatically creates complete videos from a text prompt—script, voiceover, stock footage, and subtitles all assembled and ready.