DEV Community

Alex Kane
Alex Kane

Posted on

n8n for CyberSec SaaS Vendors: 5 Automations for SOC 2, ISO 27001, CISA CIRCIA, and SEC Cybersecurity Disclosure (Free Workflow JSON)

If you're building a CyberSec SaaS product — SIEM, vulnerability management, EDR/XDR, threat intelligence, GRC, or security awareness training — your own security posture is also your biggest enterprise sales blocker.

Prospects ask: Do you have SOC 2 Type II? What's your ISO 27001 certification status? How do you handle CIRCIA reporting? Are you in scope for our SEC cyber disclosure program?

This post gives you five production-ready n8n workflows to automate your internal compliance operations — and ship the answers prospects expect.

Why Self-Hosted n8n for CyberSec SaaS?

The irony of using Zapier or Make for your security compliance workflows:

Regulation Cloud iPaaS Risk Self-Hosted n8n Advantage
SOC 2 CC9.2 Cloud iPaaS = vendor in scope for your CC9 monitoring program Self-hosted = no additional vendor to monitor; execution logs = native evidence
ISO 27001 Annex A 8.30 Zapier = outsourced service requiring security assessment per A.5.19/5.20 Self-hosted = no outsourced service; Annex A 8.9 config mgmt satisfied by Git-versioned JSON
CISA CIRCIA §2242 Incident response data routed through Zapier = additional ICT sub-processor Self-hosted = incident data stays in your enclave; no Zapier sub-processor disclosure
SEC 17 CFR §229.106 Cloud automation logs = third-party processor for material incident data Self-hosted = no third-party cloud processing of MNPI-adjacent incident records
EU NIS2 Art. 23/28 Cloud iPaaS vendor is an ICT sub-processor your NIS2-covered customers must assess Self-hosted = removes one layer from their third-party risk chain

Workflow 1: SOC 2 Type II Control Evidence Collection & Deadline Monitor

Tracks all SOC 2 controls in a Google Sheet, alerts control owners before evidence collection deadlines, and deduplicates alerts so you don't spam your team.

Trust Service Criteria covered: CC (Common Criteria), A (Availability), C (Confidentiality), PI (Processing Integrity), P (Privacy)

Key feature: alert_sent_date deduplication — runs every weekday but only alerts once per day per control, so control owners get one clear reminder rather than daily noise.

{
  "id": "cybersec-saas-soc2-control-monitor-001",
  "name": "CyberSec SaaS \u2014 SOC 2 Type II Control Evidence Collection & Deadline Monitor",
  "nodes": [
    {
      "id": "n1",
      "name": "Weekday 7AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1,
      "position": [
        200,
        300
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 7 * * 1-5"
            }
          ]
        }
      }
    },
    {
      "id": "n2",
      "name": "Load SOC2 Controls",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        420,
        300
      ],
      "parameters": {
        "operation": "read",
        "sheetId": "YOUR_SHEET_ID",
        "range": "SOC2Controls!A:J",
        "options": {
          "headerRow": true
        }
      }
    },
    {
      "id": "n3",
      "name": "Classify Control Status",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        640,
        300
      ],
      "parameters": {
        "jsCode": "\nconst today = new Date();\nconst results = [];\nfor (const row of $input.all()) {\n  const d = row.json;\n  const due = new Date(d.evidence_due_date);\n  const daysLeft = Math.floor((due - today) / 86400000);\n  const alertSent = d.alert_sent_date || '';\n  const todayStr = today.toISOString().split('T')[0];\n  if (alertSent === todayStr) continue;\n\n  let severity = null;\n  if (daysLeft < 0) severity = 'OVERDUE';\n  else if (daysLeft <= 7) severity = 'CRITICAL';\n  else if (daysLeft <= 21) severity = 'URGENT';\n  else if (daysLeft <= 45) severity = 'WARNING';\n\n  if (severity) {\n    results.push({ ...d, daysLeft, severity, todayStr, trust_service_criteria: d.trust_service_criteria || 'CC' });\n  }\n}\nreturn results.length ? results.map(r => ({ json: r })) : [{ json: { severity: null } }];\n"
      }
    },
    {
      "id": "n4",
      "name": "Alert Required?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        860,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false
          },
          "conditions": [
            {
              "leftValue": "={{ $json.severity }}",
              "operator": {
                "type": "string",
                "operation": "isNotEmpty"
              }
            }
          ]
        }
      }
    },
    {
      "id": "n5",
      "name": "Route Severity",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3,
      "position": [
        1080,
        300
      ],
      "parameters": {
        "mode": "rules",
        "rules": {
          "values": [
            {
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.severity }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "OVERDUE"
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "OVERDUE"
            },
            {
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.severity }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "CRITICAL"
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "CRITICAL"
            },
            {
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.severity }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "URGENT"
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "URGENT"
            }
          ],
          "fallbackOutput": "extra"
        }
      }
    },
    {
      "id": "n6",
      "name": "Slack OVERDUE",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2,
      "position": [
        1300,
        200
      ],
      "parameters": {
        "channel": "#soc2-compliance-critical",
        "text": "={{ ':rotating_light: SOC 2 OVERDUE: ' + $json.control_id + ' | ' + $json.control_name + ' | TSC: ' + $json.trust_service_criteria + ' | Owner: ' + $json.control_owner + ' | Due: ' + $json.evidence_due_date + ' (' + Math.abs($json.daysLeft) + ' days overdue) | Evidence type: ' + $json.evidence_type }}"
      }
    },
    {
      "id": "n7",
      "name": "Gmail Control Owner",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2,
      "position": [
        1300,
        340
      ],
      "parameters": {
        "sendTo": "={{ $json.control_owner_email }}",
        "subject": "={{ '[SOC 2 ' + $json.severity + '] ' + $json.control_id + ': Evidence due ' + $json.evidence_due_date }}",
        "emailType": "html",
        "message": "={{ '<p>Your SOC 2 Type II control <strong>' + $json.control_id + ' \u2014 ' + $json.control_name + '</strong> requires evidence collection by ' + $json.evidence_due_date + '.</p><p>Trust Service Criteria: ' + $json.trust_service_criteria + '</p><p>Evidence required: ' + $json.evidence_type + '</p><p>Upload to: trust.flowkitai.com/evidence</p><p><em>Note: This control is in scope for your external auditor review. Late evidence = qualified opinion risk.</em></p>' }}"
      }
    },
    {
      "id": "n8",
      "name": "Mark Alert Sent",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1520,
        300
      ],
      "parameters": {
        "operation": "update",
        "sheetId": "YOUR_SHEET_ID",
        "range": "SOC2Controls!A:J",
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "control_id": "={{ $json.control_id }}",
            "alert_sent_date": "={{ $json.todayStr }}"
          }
        },
        "options": {
          "cellFormat": "RAW"
        }
      }
    }
  ],
  "connections": {
    "Weekday 7AM": {
      "main": [
        [
          {
            "node": "Load SOC2 Controls",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load SOC2 Controls": {
      "main": [
        [
          {
            "node": "Classify Control Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Control Status": {
      "main": [
        [
          {
            "node": "Alert Required?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Alert Required?": {
      "main": [
        [
          {
            "node": "Route Severity",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Route Severity": {
      "main": [
        [
          {
            "node": "Slack OVERDUE",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Gmail Control Owner",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Gmail Control Owner",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack OVERDUE": {
      "main": [
        [
          {
            "node": "Mark Alert Sent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail Control Owner": {
      "main": [
        [
          {
            "node": "Mark Alert Sent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Google Sheet columns: control_id, control_name, trust_service_criteria, evidence_type, evidence_due_date, control_owner, control_owner_email, alert_sent_date, status

Self-hosting note: n8n execution logs for this workflow become SOC 2 evidence themselves — they show the control monitoring process is operating effectively. A SOC 2 auditor can verify the workflow ran on schedule from the execution history.


Workflow 2: ISO 27001 ISMS Deadline & Surveillance Audit Tracker

Manages the full ISO 27001 certification lifecycle: initial certification, Year 1 and Year 2 surveillance audits, 3-year recertification, and all 12 ISMS maintenance deadlines.

12 deadline types tracked:

  • ISO27001_INITIAL_CERTIFICATION — Stage 1 + Stage 2 audit scheduling
  • ISO27001_SURVEILLANCE_AUDIT_1 — Year 1 surveillance (Clause 9.1/9.2)
  • ISO27001_SURVEILLANCE_AUDIT_2 — Year 2 surveillance
  • ISO27001_RECERTIFICATION — 3-year full scope review
  • ISO27001_ANNUAL_MANAGEMENT_REVIEW — Clause 9.3 management review
  • ISO27001_RISK_ASSESSMENT_ANNUAL — Clause 6.1.2 risk assessment + SoA
  • ISO27001_SOA_REVIEW — Statement of Applicability control changes
  • ISO27001_INTERNAL_AUDIT — Clause 9.2 internal audit
  • ISO27001_PENETRATION_TEST — Annex A 8.8 annual pen test
  • ISO27001_SUPPLIER_REVIEW — Annex A 5.19/5.20 vendor security review
  • ISO27001_BCMS_TEST — Annex A 5.30 BCP/DR test
  • ISO27001_SECURITY_AWARENESS_TRAINING — Annex A 6.3 training completion
{
  "id": "cybersec-saas-iso27001-deadline-002",
  "name": "CyberSec SaaS \u2014 ISO 27001 ISMS Deadline & Surveillance Audit Tracker",
  "nodes": [
    {
      "id": "n1",
      "name": "Weekday 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1,
      "position": [
        200,
        300
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1-5"
            }
          ]
        }
      }
    },
    {
      "id": "n2",
      "name": "Load ISO27001 Deadlines",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        420,
        300
      ],
      "parameters": {
        "operation": "read",
        "sheetId": "YOUR_SHEET_ID",
        "range": "ISO27001Deadlines!A:H",
        "options": {
          "headerRow": true
        }
      }
    },
    {
      "id": "n3",
      "name": "Classify ISO Deadline",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        640,
        300
      ],
      "parameters": {
        "jsCode": "\nconst today = new Date();\nconst todayStr = today.toISOString().split('T')[0];\nconst TYPES = {\n  ISO27001_INITIAL_CERTIFICATION: { action: 'Submit ISMS scope document, Stage 1 audit scheduled', portal: 'certbody.flowkitai.com' },\n  ISO27001_SURVEILLANCE_AUDIT_1: { action: 'Year 1 surveillance audit \u2014 ensure all Annex A controls documented', portal: 'certbody.flowkitai.com/surveillance' },\n  ISO27001_SURVEILLANCE_AUDIT_2: { action: 'Year 2 surveillance audit \u2014 verify corrective actions from Surveillance 1', portal: 'certbody.flowkitai.com/surveillance' },\n  ISO27001_RECERTIFICATION: { action: '3-year recertification audit \u2014 full ISMS scope review', portal: 'certbody.flowkitai.com/recert' },\n  ISO27001_ANNUAL_MANAGEMENT_REVIEW: { action: 'Clause 9.3 management review \u2014 document ISMS performance metrics', portal: 'isms.flowkitai.com/mgmt-review' },\n  ISO27001_RISK_ASSESSMENT_ANNUAL: { action: 'Clause 6.1.2 annual risk assessment \u2014 update risk register and SoA', portal: 'isms.flowkitai.com/risk' },\n  ISO27001_SOA_REVIEW: { action: 'Statement of Applicability review \u2014 document control applicability changes', portal: 'isms.flowkitai.com/soa' },\n  ISO27001_INTERNAL_AUDIT: { action: 'Clause 9.2 internal audit \u2014 cover all ISMS controls, document nonconformities', portal: 'isms.flowkitai.com/internal-audit' },\n  ISO27001_PENETRATION_TEST: { action: 'Annex A 8.8 annual penetration test \u2014 obtain findings report for ISMS record', portal: 'isms.flowkitai.com/pentest' },\n  ISO27001_SUPPLIER_REVIEW: { action: 'Annex A 5.19/5.20 supplier security review \u2014 assess critical vendors', portal: 'isms.flowkitai.com/suppliers' },\n  ISO27001_BCMS_TEST: { action: 'Annex A 5.30 BCMS/BCP test \u2014 document recovery time objectives achieved', portal: 'isms.flowkitai.com/bcms' },\n  ISO27001_SECURITY_AWARENESS_TRAINING: { action: 'Annex A 6.3 annual security awareness training \u2014 track completion rates', portal: 'isms.flowkitai.com/training' }\n};\nconst results = [];\nfor (const row of $input.all()) {\n  const d = row.json;\n  const due = new Date(d.deadline_date);\n  const daysLeft = Math.floor((due - today) / 86400000);\n  if (d.alert_sent_date === todayStr) continue;\n  let severity = null;\n  if (daysLeft < 0) severity = 'OVERDUE';\n  else if (daysLeft <= 14) severity = 'CRITICAL';\n  else if (daysLeft <= 30) severity = 'URGENT';\n  else if (daysLeft <= 60) severity = 'WARNING';\n  else if (daysLeft <= 90) severity = 'NOTICE';\n  const info = TYPES[d.deadline_type] || { action: 'Review deadline requirements', portal: 'isms.flowkitai.com' };\n  if (severity) results.push({ ...d, daysLeft, severity, todayStr, ...info });\n}\nreturn results.length ? results.map(r => ({ json: r })) : [{ json: { severity: null } }];\n"
      }
    },
    {
      "id": "n4",
      "name": "Alert Required?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        860,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false
          },
          "conditions": [
            {
              "leftValue": "={{ $json.severity }}",
              "operator": {
                "type": "string",
                "operation": "isNotEmpty"
              }
            }
          ]
        }
      }
    },
    {
      "id": "n5",
      "name": "Slack ISMS Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2,
      "position": [
        1080,
        200
      ],
      "parameters": {
        "channel": "#iso27001-compliance",
        "text": "={{ ':lock: [ISO 27001 ' + $json.severity + '] ' + $json.deadline_type + ' | Due: ' + $json.deadline_date + ' (' + $json.daysLeft + 'd) | Action: ' + $json.action + ' | Portal: ' + $json.portal }}"
      }
    },
    {
      "id": "n6",
      "name": "Gmail ISMS Owner",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2,
      "position": [
        1080,
        380
      ],
      "parameters": {
        "sendTo": "={{ $json.isms_owner_email }}",
        "subject": "={{ '[ISO 27001 ' + $json.severity + '] ' + $json.deadline_type + ' \u2014 ' + $json.deadline_date }}",
        "emailType": "html",
        "message": "={{ '<p>ISO 27001 ISMS deadline approaching:</p><ul><li><strong>Type:</strong> ' + $json.deadline_type + '</li><li><strong>Due:</strong> ' + $json.deadline_date + ' (' + $json.daysLeft + ' days)</li><li><strong>Action required:</strong> ' + $json.action + '</li><li><strong>Portal:</strong> ' + $json.portal + '</li></ul><p><em>Missing surveillance/recertification audits = certificate suspension. CB will be notified per ISO/IEC 17021-1 \u00a79.6.</em></p>' }}"
      }
    },
    {
      "id": "n7",
      "name": "Mark Alert Sent",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1300,
        300
      ],
      "parameters": {
        "operation": "update",
        "sheetId": "YOUR_SHEET_ID",
        "range": "ISO27001Deadlines!A:H",
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "deadline_type": "={{ $json.deadline_type }}",
            "alert_sent_date": "={{ $json.todayStr }}"
          }
        },
        "options": {
          "cellFormat": "RAW"
        }
      }
    }
  ],
  "connections": {
    "Weekday 8AM": {
      "main": [
        [
          {
            "node": "Load ISO27001 Deadlines",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load ISO27001 Deadlines": {
      "main": [
        [
          {
            "node": "Classify ISO Deadline",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify ISO Deadline": {
      "main": [
        [
          {
            "node": "Alert Required?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Alert Required?": {
      "main": [
        [
          {
            "node": "Slack ISMS Alert",
            "type": "main",
            "index": 0
          },
          {
            "node": "Gmail ISMS Owner",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Slack ISMS Alert": {
      "main": [
        [
          {
            "node": "Mark Alert Sent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail ISMS Owner": {
      "main": [
        [
          {
            "node": "Mark Alert Sent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Critical warning: Missing a surveillance audit window (typically ±3 months of anniversary date) = certification body can suspend your certificate. The CB notifies accreditation body per ISO/IEC 17021-1 §9.6 — your enterprise prospects see suspended status in the CB's public registry.


Workflow 3: CISA CIRCIA 72-Hour Incident Reporting Pipeline

When a substantial cyber incident occurs, CIRCIA §2242 requires covered entities to report to CISA within 72 hours. Ransomware payments: 24 hours. Non-compliance: CISA has subpoena authority (§2244).

Who is a covered entity? The 16 critical infrastructure sectors (energy, water, healthcare, financial services, transportation, communications, government, defense, food/agriculture, chemical, nuclear, emergency services, IT, dams, manufacturing) — and companies providing technology to those sectors.

Incident types classified:

  • RANSOMWARE — 72h substantial incident + 24h ransom payment (if paid)
  • SUBSTANTIAL_CYBER_INCIDENT — unauthorized access or operational disruption
  • DATA_EXFILTRATION — unauthorized data removal
  • SUPPLY_CHAIN_COMPROMISE — SolarWinds-type notification expectations
  • DDoS_CRITICAL_INFRA — substantial if service disruption results
{
  "id": "cybersec-saas-circia-incident-003",
  "name": "CyberSec SaaS \u2014 CISA CIRCIA 72-Hour Incident Reporting Pipeline",
  "nodes": [
    {
      "id": "n1",
      "name": "Webhook Incident Trigger",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        200,
        300
      ],
      "parameters": {
        "path": "circia-incident",
        "responseMode": "lastNode",
        "httpMethod": "POST"
      }
    },
    {
      "id": "n2",
      "name": "Classify CIRCIA Incident",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        420,
        300
      ],
      "parameters": {
        "jsCode": "\nconst d = $json;\nconst now = new Date();\n\n// CIRCIA covered entities: critical infrastructure sectors (16 sectors per NIST/DHS)\nconst CIRCIA_SECTORS = ['energy', 'water', 'healthcare', 'financial_services', 'transportation', 'communications', 'government', 'defense', 'food_agriculture', 'chemical', 'nuclear', 'emergency_services', 'information_technology', 'dams', 'manufacturing'];\n\nconst incidentType = (d.incident_type || 'UNKNOWN').toUpperCase();\nconst affectedSector = (d.affected_sector || '').toLowerCase();\n\nconst INCIDENT_MAP = {\n  RANSOMWARE: { circia_reportable: true, severity: 'CRITICAL', reporting_window_h: 72, ransom_report_window_h: 24, description: 'Ransomware attack \u2014 both substantial cyber incident (72h) and ransom payment (24h) reporting required' },\n  SUBSTANTIAL_CYBER_INCIDENT: { circia_reportable: true, severity: 'CRITICAL', reporting_window_h: 72, ransom_report_window_h: null, description: 'Substantial cyber incident per CIRCIA \u00a72240(a)(12) \u2014 disruption to operations or unauthorized access' },\n  DATA_EXFILTRATION: { circia_reportable: true, severity: 'CRITICAL', reporting_window_h: 72, ransom_report_window_h: null, description: 'Unauthorized data exfiltration \u2014 likely substantial cyber incident' },\n  SUPPLY_CHAIN_COMPROMISE: { circia_reportable: true, severity: 'CRITICAL', reporting_window_h: 72, ransom_report_window_h: null, description: 'Supply chain compromise \u2014 CIRCIA reportable, CISA SolarWinds-type notification expectations' },\n  DDoS_CRITICAL_INFRA: { circia_reportable: true, severity: 'HIGH', reporting_window_h: 72, ransom_report_window_h: null, description: 'DDoS against critical infrastructure \u2014 substantial cyber incident if service disruption results' },\n  INSIDER_THREAT_DATA: { circia_reportable: false, severity: 'HIGH', reporting_window_h: null, ransom_report_window_h: null, description: 'Insider threat \u2014 assess if substantial, notify legal for CIRCIA applicability determination' },\n  PHISHING_WITH_ACCESS: { circia_reportable: false, severity: 'MEDIUM', reporting_window_h: null, ransom_report_window_h: null, description: 'Phishing with credential access \u2014 assess scope, may become CIRCIA reportable if substantial' }\n};\n\nconst info = INCIDENT_MAP[incidentType] || INCIDENT_MAP['SUBSTANTIAL_CYBER_INCIDENT'];\nconst isCovered = CIRCIA_SECTORS.includes(affectedSector);\n\nconst deadlineTs = info.reporting_window_h ? new Date(now.getTime() + info.reporting_window_h * 3600000).toISOString() : null;\nconst ransomDeadlineTs = info.ransom_report_window_h ? new Date(now.getTime() + info.ransom_report_window_h * 3600000).toISOString() : null;\n\nconst incidentId = 'CIRCIA-' + now.toISOString().slice(0,10).replace(/-/g,'') + '-' + Math.random().toString(36).slice(2,7).toUpperCase();\n\nreturn {\n  ...d,\n  incident_id: incidentId,\n  ...info,\n  is_covered_sector: isCovered,\n  detected_at: now.toISOString(),\n  reporting_deadline: deadlineTs,\n  ransom_deadline: ransomDeadlineTs,\n  cisa_report_url: 'https://www.cisa.gov/report',\n  cisa_phone: '1-888-282-0870'\n};\n"
      }
    },
    {
      "id": "n3",
      "name": "CIRCIA Reportable?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        640,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false
          },
          "conditions": [
            {
              "leftValue": "={{ $json.circia_reportable && $json.is_covered_sector }}",
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ]
        }
      }
    },
    {
      "id": "n4",
      "name": "Slack CISO Immediate",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2,
      "position": [
        860,
        200
      ],
      "parameters": {
        "channel": "#incident-response-critical",
        "text": "={{ ':rotating_light: CIRCIA REPORTABLE INCIDENT: ' + $json.incident_id + '\\nType: ' + $json.incident_type + '\\nSector: ' + $json.affected_sector + '\\n72h Reporting Deadline: ' + $json.reporting_deadline + '\\nCISA Report URL: ' + $json.cisa_report_url + '\\nCISA Phone: ' + $json.cisa_phone + ($json.ransom_deadline ? '\\n:moneybag: RANSOM 24h Deadline: ' + $json.ransom_deadline : '') + '\\nDescription: ' + $json.description }}"
      }
    },
    {
      "id": "n5",
      "name": "Gmail CISO + Legal",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2,
      "position": [
        860,
        380
      ],
      "parameters": {
        "sendTo": "ciso@company.com",
        "ccList": "legal@company.com, compliance@company.com",
        "subject": "={{ '[CIRCIA CRITICAL] ' + $json.incident_id + ' \u2014 72h reporting clock started' }}",
        "emailType": "html",
        "message": "={{ '<h2>CIRCIA Substantial Cyber Incident \u2014 Immediate Action Required</h2><p><strong>Incident ID:</strong> ' + $json.incident_id + '</p><p><strong>Type:</strong> ' + $json.incident_type + '</p><p><strong>Affected Sector:</strong> ' + $json.affected_sector + '</p><p><strong>72-Hour CISA Reporting Deadline:</strong> ' + $json.reporting_deadline + '</p>' + ($json.ransom_deadline ? '<p><strong>24-Hour Ransom Payment Deadline:</strong> ' + $json.ransom_deadline + '</p>' : '') + '<p><strong>CISA Report Portal:</strong> <a href=\"' + $json.cisa_report_url + '\">' + $json.cisa_report_url + '</a></p><p><strong>CISA 24/7 Phone:</strong> ' + $json.cisa_phone + '</p><p><em>CIRCIA \u00a72242: Covered entities must report substantial cyber incidents within 72 hours of reasonable belief. Ransom payments: 24 hours. Non-compliance: CISA subpoena authority (\u00a72244).</em></p>' }}"
      }
    },
    {
      "id": "n6",
      "name": "Postgres Log Incident",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2,
      "position": [
        1080,
        200
      ],
      "parameters": {
        "operation": "insert",
        "schema": "public",
        "table": "circia_incidents",
        "columns": "incident_id, incident_type, affected_sector, severity, detected_at, reporting_deadline, circia_reportable, created_at",
        "values": "={{ $json.incident_id }}, {{ $json.incident_type }}, {{ $json.affected_sector }}, {{ $json.severity }}, {{ $json.detected_at }}, {{ $json.reporting_deadline }}, {{ $json.circia_reportable }}, {{ new Date().toISOString() }}"
      }
    },
    {
      "id": "n7",
      "name": "ACK 200",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        1080,
        380
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ received: true, incident_id: $json.incident_id, reporting_deadline: $json.reporting_deadline }) }}",
        "responseCode": 200
      }
    }
  ],
  "connections": {
    "Webhook Incident Trigger": {
      "main": [
        [
          {
            "node": "Classify CIRCIA Incident",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify CIRCIA Incident": {
      "main": [
        [
          {
            "node": "CIRCIA Reportable?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CIRCIA Reportable?": {
      "main": [
        [
          {
            "node": "Slack CISO Immediate",
            "type": "main",
            "index": 0
          },
          {
            "node": "Gmail CISO + Legal",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Postgres Log Incident",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack CISO Immediate": {
      "main": [
        [
          {
            "node": "Postgres Log Incident",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail CISO + Legal": {
      "main": [
        [
          {
            "node": "ACK 200",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Postgres Log Incident": {
      "main": [
        [
          {
            "node": "ACK 200",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Self-hosting note: Your incident response automation platform itself is an ICT system in your CIRCIA scope. If Zapier processes your incident data, Zapier is an additional covered entity sub-processor. Self-hosted n8n keeps incident data in your own enclave with no additional CISA notification surface.


Workflow 4: SEC Cybersecurity Disclosure 4-Business-Day Clock Monitor

SEC Rule 13p-1 / 17 CFR §229.106 (effective December 18, 2023) requires public companies to disclose material cybersecurity incidents via Form 8-K Item 1.05 within 4 business days of determining the incident is material.

The 4-business-day clock starts at determination, not discovery — documenting when you determined materiality is as important as the incident itself.

{
  "id": "cybersec-saas-sec-disclosure-004",
  "name": "CyberSec SaaS \u2014 SEC Cybersecurity Disclosure 4-Business-Day Clock Monitor",
  "nodes": [
    {
      "id": "n1",
      "name": "Webhook Material Incident",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        200,
        300
      ],
      "parameters": {
        "path": "sec-cyber-incident",
        "responseMode": "lastNode",
        "httpMethod": "POST"
      }
    },
    {
      "id": "n2",
      "name": "Calculate SEC 4-Day Deadline",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        420,
        300
      ],
      "parameters": {
        "jsCode": "\nconst d = $json;\nconst now = new Date();\n\n// SEC Rule 13p-1 / 17 CFR \u00a7229.106 \u2014 material cybersecurity incident = Form 8-K Item 1.05\n// 4 business days from determination that incident is material\n// Effective: Dec 18, 2023 (large accelerated filers), Jun 15, 2024 (smaller reporting companies)\n// Delay available: DOJ determination of substantial national security/public safety risk\n\nfunction addBusinessDays(date, n) {\n  let d = new Date(date);\n  let added = 0;\n  while (added < n) {\n    d.setDate(d.getDate() + 1);\n    const day = d.getDay();\n    if (day !== 0 && day !== 6) added++;\n  }\n  return d;\n}\n\nconst determinationDate = d.material_determination_date ? new Date(d.material_determination_date) : now;\nconst form8kDeadline = addBusinessDays(determinationDate, 4);\nconst warningDeadline = addBusinessDays(determinationDate, 2); // 2-biz-day internal warning\n\nconst incidentId = 'SEC-' + now.toISOString().slice(0,10).replace(/-/g,'') + '-' + Math.random().toString(36).slice(2,6).toUpperCase();\n\nconst MATERIALITY_INDICATORS = [\n  'Unauthorized access to material nonpublic information (MNPI)',\n  'Disruption to business operations affecting revenue or customer contracts',\n  'Compromise of systems processing financial transactions or SEC filings',\n  'Ransomware preventing system access for significant duration',\n  'Exposure of customer PII at scale with reputational/legal consequence'\n];\n\nreturn {\n  ...d,\n  incident_id: incidentId,\n  determination_date: determinationDate.toISOString(),\n  form_8k_deadline: form8kDeadline.toISOString(),\n  internal_warning_deadline: warningDeadline.toISOString(),\n  sec_filing_url: 'https://efts.sec.gov/LATEST/search-index?q=8-K&dateRange=custom',\n  edgar_url: 'https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&type=8-K',\n  rule_cite: '17 CFR \u00a7229.106(a)(1), SEC Release No. 33-11216 (Dec 2023)',\n  materiality_indicators: MATERIALITY_INDICATORS,\n  delay_available: 'DOJ determination of substantial national security or public safety risk (\u00a7229.106(a)(2))'\n};\n"
      }
    },
    {
      "id": "n3",
      "name": "Slack GC + CISO Immediate",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2,
      "position": [
        640,
        200
      ],
      "parameters": {
        "channel": "#sec-incident-response",
        "text": "={{ ':scales: SEC MATERIAL CYBER INCIDENT: ' + $json.incident_id + '\\nDetermination Date: ' + $json.determination_date + '\\nForm 8-K Deadline (4 biz days): ' + $json.form_8k_deadline + '\\n2-Day Internal Warning: ' + $json.internal_warning_deadline + '\\nRule: ' + $json.rule_cite + '\\nEDGAR Filing: ' + $json.edgar_url + '\\nDelay option: ' + $json.delay_available }}"
      }
    },
    {
      "id": "n4",
      "name": "Gmail GC + CFO + CISO",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2,
      "position": [
        640,
        380
      ],
      "parameters": {
        "sendTo": "gc@company.com",
        "ccList": "cfo@company.com, ciso@company.com, ir@company.com",
        "subject": "={{ '[SEC MATERIAL INCIDENT] ' + $json.incident_id + ' \u2014 Form 8-K due ' + $json.form_8k_deadline.slice(0,10) }}",
        "emailType": "html",
        "message": "={{ '<h2>SEC Cybersecurity Disclosure \u2014 Material Incident Determination</h2><p><strong>Incident ID:</strong> ' + $json.incident_id + '</p><p><strong>Material determination date:</strong> ' + $json.determination_date + '</p><p><strong>Form 8-K Item 1.05 deadline:</strong> <strong>' + $json.form_8k_deadline + '</strong> (4 business days)</p><p><strong>Internal escalation deadline:</strong> ' + $json.internal_warning_deadline + ' (2 business days)</p><p><strong>Rule:</strong> ' + $json.rule_cite + '</p><p><strong>EDGAR filing portal:</strong> <a href=\"' + $json.edgar_url + '\">' + $json.edgar_url + '</a></p><p><strong>Disclosure delay option:</strong> ' + $json.delay_available + '</p><h3>Materiality indicators to document:</h3><ul>' + $json.materiality_indicators.map(i => '<li>' + i + '</li>').join('') + '</ul><p><em>Note: Aggregate immaterial incidents that become material in total must also be disclosed per \u00a7229.106(a)(1)(ii).</em></p>' }}"
      }
    },
    {
      "id": "n5",
      "name": "Postgres Log Incident",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2,
      "position": [
        860,
        200
      ],
      "parameters": {
        "operation": "insert",
        "schema": "public",
        "table": "sec_cyber_incidents",
        "columns": "incident_id, incident_description, determination_date, form_8k_deadline, rule_cite, created_at",
        "values": "={{ $json.incident_id }}, {{ $json.incident_description || 'Material cybersecurity incident' }}, {{ $json.determination_date }}, {{ $json.form_8k_deadline }}, {{ $json.rule_cite }}, {{ new Date().toISOString() }}"
      }
    },
    {
      "id": "n6",
      "name": "ACK 200",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        860,
        380
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ received: true, incident_id: $json.incident_id, form_8k_deadline: $json.form_8k_deadline }) }}",
        "responseCode": 200
      }
    }
  ],
  "connections": {
    "Webhook Material Incident": {
      "main": [
        [
          {
            "node": "Calculate SEC 4-Day Deadline",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate SEC 4-Day Deadline": {
      "main": [
        [
          {
            "node": "Slack GC + CISO Immediate",
            "type": "main",
            "index": 0
          },
          {
            "node": "Gmail GC + CFO + CISO",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack GC + CISO Immediate": {
      "main": [
        [
          {
            "node": "Postgres Log Incident",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail GC + CFO + CISO": {
      "main": [
        [
          {
            "node": "ACK 200",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Postgres Log Incident": {
      "main": [
        [
          {
            "node": "ACK 200",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Materiality indicators documented in the workflow:

  • Unauthorized access to material nonpublic information (MNPI)
  • Disruption to business operations affecting revenue or customer contracts
  • Compromise of systems processing financial transactions or SEC filings
  • Ransomware preventing system access for significant duration
  • Exposure of customer PII at scale with reputational/legal consequence

Disclosure delay: DOJ can grant delay if disclosure would create substantial national security or public safety risk (§229.106(a)(2)).

Note for CyberSec SaaS vendors: If your platform processes MNPI-adjacent data for public company customers, your incident may trigger their 4-day clock — and they will ask whether your automation toolchain (Zapier, Make) also processed the incident data. Self-hosted n8n removes that sub-processor disclosure question.


Workflow 5: Weekly CyberSec SaaS Vendor Compliance KPI Dashboard

Monday morning executive report covering all active compliance programs: SOC 2, ISO 27001, CIRCIA incidents, SEC 8-K status, and NIS2 incidents.

{
  "id": "cybersec-saas-kpi-dashboard-005",
  "name": "CyberSec SaaS \u2014 Weekly Vendor Compliance KPI Dashboard",
  "nodes": [
    {
      "id": "n1",
      "name": "Monday 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1,
      "position": [
        200,
        300
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      }
    },
    {
      "id": "n2",
      "name": "Query Compliance Metrics",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2,
      "position": [
        420,
        300
      ],
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT (SELECT COUNT(*) FROM soc2_controls WHERE evidence_due_date < NOW() AND status != 'COMPLETED') AS soc2_overdue, (SELECT COUNT(*) FROM soc2_controls WHERE evidence_due_date BETWEEN NOW() AND NOW() + INTERVAL '14 days' AND status != 'COMPLETED') AS soc2_critical, (SELECT COUNT(*) FROM iso27001_deadlines WHERE deadline_date < NOW() AND status != 'COMPLETED') AS iso_overdue, (SELECT COUNT(*) FROM circia_incidents WHERE detected_at > NOW() - INTERVAL '7 days') AS circia_incidents_7d, (SELECT COUNT(*) FROM circia_incidents WHERE detected_at > NOW() - INTERVAL '7 days' AND severity = 'CRITICAL') AS circia_critical_7d, (SELECT COUNT(*) FROM sec_cyber_incidents WHERE form_8k_deadline < NOW() AND filed_at IS NULL) AS sec_overdue_8k, (SELECT COUNT(*) FROM nis2_incidents WHERE detected_at > NOW() - INTERVAL '7 days') AS nis2_incidents_7d"
      }
    },
    {
      "id": "n3",
      "name": "Build KPI Report",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        640,
        300
      ],
      "parameters": {
        "jsCode": "\nconst m = $json;\nconst now = new Date();\nconst weekStr = now.toISOString().slice(0,10);\n\nconst rows = [\n  { label: 'SOC 2 Controls Overdue', value: m.soc2_overdue, flag: m.soc2_overdue > 0, action: 'Upload evidence at trust.flowkitai.com/evidence' },\n  { label: 'SOC 2 Controls Critical (\u226414d)', value: m.soc2_critical, flag: m.soc2_critical > 0, action: 'Schedule evidence collection calls' },\n  { label: 'ISO 27001 Deadlines Overdue', value: m.iso_overdue, flag: m.iso_overdue > 0, action: 'Contact certification body immediately' },\n  { label: 'CIRCIA Incidents (7d)', value: m.circia_incidents_7d, flag: m.circia_incidents_7d > 0, action: 'Verify all CISA reports filed' },\n  { label: 'CIRCIA Critical Incidents (7d)', value: m.circia_critical_7d, flag: m.circia_critical_7d > 0, action: 'IMMEDIATE: Check 72h deadlines' },\n  { label: 'SEC 8-K Overdue (Unfiled)', value: m.sec_overdue_8k, flag: m.sec_overdue_8k > 0, action: 'IMMEDIATE: Contact General Counsel + file via EDGAR' },\n  { label: 'NIS2 Incidents (7d)', value: m.nis2_incidents_7d, flag: m.nis2_incidents_7d > 0, action: 'Verify 24h/72h EU NCA notifications filed' }\n];\n\nconst hasOverdue = m.soc2_overdue > 0 || m.iso_overdue > 0;\nconst hasSEC = m.sec_overdue_8k > 0;\nconst hasCIRCIA = m.circia_critical_7d > 0;\n\nconst subjectFlags = [];\nif (hasSEC) subjectFlags.push('[SEC 8-K OVERDUE]');\nif (hasCIRCIA) subjectFlags.push('[CIRCIA CRITICAL]');\nif (hasOverdue) subjectFlags.push('[COMPLIANCE OVERDUE]');\nconst subject = (subjectFlags.length ? subjectFlags.join(' ') + ' ' : '') + 'CyberSec SaaS Compliance KPI \u2014 Week of ' + weekStr;\n\nconst tableRows = rows.map(r => '<tr style=\"background:' + (r.flag ? '#fff3cd' : '#f8f9fa') + '\"><td>' + r.label + '</td><td style=\"font-weight:bold;color:' + (r.flag ? '#dc3545' : '#28a745') + '\">' + (r.value || 0) + '</td><td>' + r.action + '</td></tr>').join('');\nconst html = '<h2>CyberSec SaaS Compliance KPI Dashboard</h2><p>Week of ' + weekStr + '</p><table border=\"1\" cellpadding=\"6\" style=\"border-collapse:collapse;width:100%\"><tr style=\"background:#343a40;color:white\"><th>Metric</th><th>Value</th><th>Action</th></tr>' + tableRows + '</table><p><small>Self-hosted n8n | Automation execution logs = SOC 2 evidence | Git-versioned workflows = ISO 27001 Annex A 8.9 configuration management | No cloud iPaaS = no additional CIRCIA/NIS2 ICT third-party scope</small></p>';\n\nreturn { subject, html, hasOverdue, hasSEC, hasCIRCIA, weekStr };\n"
      }
    },
    {
      "id": "n4",
      "name": "Gmail CEO + CISO",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2,
      "position": [
        860,
        300
      ],
      "parameters": {
        "sendTo": "ceo@company.com",
        "ccList": "cfo@company.com",
        "bccList": "ciso@company.com",
        "subject": "={{ $json.subject }}",
        "emailType": "html",
        "message": "={{ $json.html }}"
      }
    },
    {
      "id": "n5",
      "name": "Slack Exec Summary",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2,
      "position": [
        860,
        450
      ],
      "parameters": {
        "channel": "#exec-cybersec-kpis",
        "text": "={{ ':bar_chart: CyberSec Compliance KPI ' + $json.weekStr + ' | SOC2 overdue: ' + ($json.soc2_overdue || 0) + ' | ISO overdue: ' + ($json.iso_overdue || 0) + ' | CIRCIA (7d): ' + ($json.circia_incidents_7d || 0) + ' | SEC 8-K overdue: ' + ($json.sec_overdue_8k || 0) }}"
      }
    }
  ],
  "connections": {
    "Monday 8AM": {
      "main": [
        [
          {
            "node": "Query Compliance Metrics",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query Compliance Metrics": {
      "main": [
        [
          {
            "node": "Build KPI Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build KPI Report": {
      "main": [
        [
          {
            "node": "Gmail CEO + CISO",
            "type": "main",
            "index": 0
          },
          {
            "node": "Slack Exec Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

KPI metrics reported:

  • SOC 2 controls overdue + critical (≤14 days)
  • ISO 27001 deadlines overdue
  • CIRCIA incidents (7d) + critical count
  • SEC Form 8-K overdue (unfiled material incidents)
  • NIS2 incidents (7d)

Subject line flags: [SEC 8-K OVERDUE], [CIRCIA CRITICAL], [COMPLIANCE OVERDUE] — so executives see critical status before opening the email.


EU NIS2 Article 23/28 — What CyberSec SaaS Vendors Need to Know

If your customers are EU entities in scope for NIS2 (effective October 2024), you are potentially an ICT third-party service provider under Article 28:

  • Art. 23: Essential and important entities must report significant incidents to their CSIRT/NCA within 24 hours (early warning), 72 hours (incident notification), and 1 month (final report)
  • Art. 28: Your customers must include you in their third-party ICT risk management framework — they will ask for your NIS2 questionnaire
  • Cloud iPaaS risk: If Zapier or Make is in your automation stack, each becomes an additional ICT sub-processor your customers must assess and include in their Art. 28 inventory

Self-hosted n8n removes your automation layer from their Art. 28 sub-processor chain — a concrete procurement advantage in EU enterprise sales.


Get All 5 Workflows + 10 More

All five workflows above are import-ready (copy the JSON, import into n8n via Settings → Import from URL or paste directly).

If you want the full CyberSec SaaS automation library — including NIS2 incident pipeline, SOC 2 evidence collector with Notion integration, ISO 27001 risk register automation, and more — the complete FlowKit bundle is at stripeai.gumroad.com.

Questions or customization requests: drop them in the comments below.

Top comments (0)