{
  "name": "Attack-Path Digital Twin + Auto-Remediation",
  "nodes": [
    {
      "id": "186d1910-e079-4455-931b-22d3dbf1830d",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        0
      ],
      "parameters": {
        "width": 480,
        "height": 992,
        "content": "## Attack-Path Digital Twin + Auto-Remediation\n\n### How it works\n\nThis workflow runs on a schedule or manually to query a Neo4j security graph, extract candidate attack paths, and rank them by risk. When a critical path exceeds the configured threshold, it retrieves the affected infrastructure-as-code file from GitHub, asks an AI agent to generate a remediation plan, and prepares operational payloads. It then opens a Jira issue, creates a remediation branch and pull request in GitHub, and comments back on the Jira ticket with the PR link.\n\n### Setup steps\n\n- Configure Neo4j access in the security graph HTTP request, including the Neo4j transaction endpoint and any required authentication credentials.\n- Set the values in the config node, especially neo4jUrl, riskScoreThreshold, jiraBaseUrl, and jiraProjectKey.\n- Add valid GitHub API credentials or headers for reading files, creating branches, updating files, and opening pull requests in the target repository.\n- Add Jira API credentials with permission to create issues and add comments in the configured project.\n- Configure the OpenAI Chat Model credentials used by the AI remediation agent.\n- Verify the repository, branch, file path, Jira project, and payload expressions used in the HTTP Request nodes before enabling the scheduled trigger.\n\n### Customization\n\nAdjust the risk score threshold, Cypher query, attack-path scoring logic, AI remediation prompt, Jira issue fields, and GitHub branch/PR naming conventions to match the organization’s security and engineering workflow."
      },
      "typeVersion": 1
    },
    {
      "id": "610bc892-44cd-4108-a2c9-9e315d253068",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        560,
        48
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 512,
        "content": "## Start and configure scan\n\nProvides scheduled and manual entry points, then initializes shared workflow settings such as Neo4j, Jira, and risk threshold values."
      },
      "typeVersion": 1
    },
    {
      "id": "a0a6b6ea-aae2-4a54-8c45-dfd0a6aa9076",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1008,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 624,
        "height": 320,
        "content": "## Query attack graph\n\nQueries the Neo4j security graph and parses the transaction response into candidate attack-path objects, with a short wait before downstream scoring."
      },
      "typeVersion": 1
    },
    {
      "id": "6a001b55-ee53-4846-86af-942716a42f6d",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1664,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 320,
        "content": "## Score and branch\n\nRanks the extracted attack paths and decides whether a critical path exists that should trigger automated remediation."
      },
      "typeVersion": 1
    },
    {
      "id": "e5cfc7d2-cc20-4cc5-adfd-36c7c5e2de18",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2176,
        48
      ],
      "parameters": {
        "color": 7,
        "width": 688,
        "height": 528,
        "content": "## Generate remediation plan\n\nFetches the relevant IaC file from GitHub, uses the AI agent and OpenAI chat model to propose a remediation, and parses the structured output into Jira and GitHub payloads."
      },
      "typeVersion": 1
    },
    {
      "id": "3bddac61-d9db-4f0a-8772-5b374da7cc33",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2896,
        0
      ],
      "parameters": {
        "color": 7,
        "height": 368,
        "content": "## Create tracking issue\n\nCreates the Jira issue that records the critical attack path and planned remediation work before code changes are prepared."
      },
      "typeVersion": 1
    },
    {
      "id": "2eb0923b-cb98-4281-aa54-6df49533d0b5",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3168,
        80
      ],
      "parameters": {
        "color": 7,
        "width": 864,
        "height": 304,
        "content": "## Prepare GitHub fix\n\nRetrieves the base branch reference, creates a new remediation branch, updates the IaC file, and opens a pull request with the proposed fix."
      },
      "typeVersion": 1
    },
    {
      "id": "2d2172ab-871b-42b9-b271-2e846dd50f2b",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4064,
        48
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 320,
        "content": "## Publish PR result\n\nWaits for the pull request result to be available, then adds a Jira comment containing the PR link for traceability."
      },
      "typeVersion": 1
    },
    {
      "id": "ef3df9dd-705f-4274-84b1-1f907adb6063",
      "name": "When Daily Graph Scan Starts",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        608,
        208
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 5 * * *"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "0f091c86-9ddf-4f04-bd15-d1cd796ca5fe",
      "name": "When Test Run Initiated",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        608,
        400
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "94e6b52a-442a-474c-9fbe-fe550aff3496",
      "name": "Set Configuration Parameters",
      "type": "n8n-nodes-base.set",
      "position": [
        832,
        288
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "name": "neo4jUrl",
              "type": "string",
              "value": "https://your-neo4j-host:7474"
            },
            {
              "name": "riskScoreThreshold",
              "type": "number",
              "value": 50
            },
            {
              "name": "jiraBaseUrl",
              "type": "string",
              "value": "https://yourcompany.atlassian.net"
            },
            {
              "name": "jiraProjectKey",
              "type": "string",
              "value": "SEC"
            },
            {
              "name": "jiraIssueType",
              "type": "string",
              "value": "Bug"
            },
            {
              "name": "repoOwner",
              "type": "string",
              "value": "your-org"
            },
            {
              "name": "repoName",
              "type": "string",
              "value": "infra-terraform"
            },
            {
              "name": "baseBranch",
              "type": "string",
              "value": "main"
            },
            {
              "name": "defaultIacPath",
              "type": "string",
              "value": "environments/prod/security-groups.tf"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "4c6b81e0-1466-4439-b4eb-9f96f705e4c9",
      "name": "Post to Security Graph API",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1056,
        288
      ],
      "parameters": {
        "url": "={{ $json.neo4jUrl }}/db/neo4j/tx/commit",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ statements: [ { statement: \"MATCH path = shortestPath((src:Asset {exposure: 'internet-facing'})-[:CAN_ACCESS|TRUSTS|ASSUMES_ROLE|CONNECTS_TO*1..6]->(dst:Asset)) WHERE dst.criticality IN ['crown-jewel','critical'] RETURN path, src.name AS sourceName, dst.name AS targetName, dst.criticality AS targetCriticality, length(path) AS hopCount LIMIT 25\", resultDataContents: ['graph', 'row'] } ] }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth"
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "0b156ec6-c9a5-47bc-b82b-c77cd8dc1012",
      "name": "Parse and Extract Attack Paths",
      "type": "n8n-nodes-base.code",
      "position": [
        1280,
        288
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Parse the Neo4j transactional response and build one candidate attack-path object per row.\n// Assumes Asset nodes carry: name, exposure, criticality, exposedPorts, iacResourceAddress,\n// iacFilePath, resourceType — see the setup sticky note for schema notes.\nconst config = $('Set Configuration Parameters').item.json;\nconst response = $input.item.json;\n\nconst results = (response.results && response.results[0]) || {};\nconst rows = results.data || [];\n\nconst paths = rows.map((entry, idx) => {\n  const row = entry.row || [];\n  const graph = entry.graph || {};\n  const nodes = graph.nodes || [];\n\n  const sourceName = row[1];\n  const targetName = row[2];\n  const targetCriticality = row[3];\n  const hopCount = row[4];\n\n  const misconfigNode = nodes.find(n => n.properties && n.properties.iacResourceAddress) || {};\n  const exposedNode = nodes.find(n => (n.properties || {}).exposure === 'internet-facing') || nodes[0] || {};\n  const targetNode = nodes[nodes.length - 1] || {};\n\n  return {\n    pathId: `PATH-${idx + 1}-${Date.now()}`,\n    sourceAsset: sourceName || (exposedNode.properties || {}).name || 'unknown',\n    targetAsset: targetName || (targetNode.properties || {}).name || 'unknown',\n    targetCriticality: targetCriticality || (targetNode.properties || {}).criticality || 'unknown',\n    hopCount: hopCount != null ? hopCount : nodes.length,\n    exposedPorts: (exposedNode.properties || {}).exposedPorts || [],\n    misconfigResourceAddress: (misconfigNode.properties || {}).iacResourceAddress || null,\n    misconfigResourceType: (misconfigNode.properties || {}).resourceType || null,\n    misconfigFilePath: (misconfigNode.properties || {}).iacFilePath || config.defaultIacPath,\n    nodeCount: nodes.length\n  };\n});\n\nreturn paths.map(p => ({ json: { ...config, ...p } }));"
      },
      "typeVersion": 2
    },
    {
      "id": "c5c90cd8-cb1c-4764-aed5-4f2f61cf8138",
      "name": "Analyze and Rank Attack Paths",
      "type": "n8n-nodes-base.code",
      "position": [
        1712,
        288
      ],
      "parameters": {
        "language": "python",
        "pythonCode": "# Composite risk scoring across all candidate attack paths; select the single highest-risk one\n# Score = target criticality (40) + port exposure/exploitability (25) + hop proximity (20) + blast-radius density (15)\ncriticality_weights = {'crown-jewel': 40, 'critical': 32, 'high': 24, 'medium': 12, 'low': 4}\nhigh_risk_ports = {22, 3389, 445, 1433, 3306, 5432, 6379, 9200, 27017}\n\nscored = []\nfor i in _input.all():\n    item = i['json']\n    criticality = str(item.get('targetCriticality', '')).lower()\n    criticality_score = criticality_weights.get(criticality, 10)\n\n    exposed_ports = item.get('exposedPorts') or []\n    port_hits = 0\n    for p in exposed_ports:\n        try:\n            if int(p) in high_risk_ports:\n                port_hits += 1\n        except (ValueError, TypeError):\n            continue\n    exposure_score = min(10 + port_hits * 5, 25)\n\n    try:\n        hop_count = int(item.get('hopCount') or 10)\n    except (ValueError, TypeError):\n        hop_count = 10\n    proximity_score = max(20 - (hop_count - 1) * 3, 2)\n\n    node_count = item.get('nodeCount') or hop_count\n    density_score = min(node_count * 1.5, 15)\n\n    risk_score = round(criticality_score + exposure_score + proximity_score + density_score, 2)\n\n    if risk_score >= 70:\n        severity = 'Critical'\n    elif risk_score >= 50:\n        severity = 'High'\n    elif risk_score >= 30:\n        severity = 'Medium'\n    else:\n        severity = 'Low'\n\n    scored.append({**item, 'riskScore': risk_score, 'severity': severity})\n\nif not scored:\n    return [{'isCriticalPath': False, 'riskScore': 0, 'severity': 'None'}]\n\nscored.sort(key=lambda s: s['riskScore'], reverse=True)\ntop = scored[0]\nthreshold = float(top.get('riskScoreThreshold', 50))\ntop['isCriticalPath'] = top['riskScore'] >= threshold\n\nreturn [top]"
      },
      "typeVersion": 2
    },
    {
      "id": "e59d816e-cff9-4d85-9eaa-d9e07594b295",
      "name": "If Critical Path Detected",
      "type": "n8n-nodes-base.if",
      "position": [
        1936,
        288
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.isCriticalPath }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "86e660cc-2324-4c07-b4e8-3872c96f2936",
      "name": "Retrieve IaC File from GitHub",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2224,
        208
      ],
      "parameters": {
        "url": "=https://api.github.com/repos/{{ $json.repoOwner }}/{{ $json.repoName }}/contents/{{ $json.misconfigFilePath }}?ref={{ $json.baseBranch }}",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "githubApi"
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "3779f241-f0d7-40fa-9f0b-328e0aae0202",
      "name": "Remediation Strategy Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2448,
        208
      ],
      "parameters": {
        "text": "=You are a cloud security remediation expert. An attack path was found in the security graph and needs a concrete fix.\n\nAttack Path:\n- Source (internet-facing): {{ $('Analyze and Rank Attack Paths').item.json.sourceAsset }}\n- Target ({{ $('Analyze and Rank Attack Paths').item.json.targetCriticality }}): {{ $('Analyze and Rank Attack Paths').item.json.targetAsset }}\n- Hop count: {{ $('Analyze and Rank Attack Paths').item.json.hopCount }}\n- Exposed ports on the entry point: {{ $('Analyze and Rank Attack Paths').item.json.exposedPorts }}\n- Risk score: {{ $('Analyze and Rank Attack Paths').item.json.riskScore }}/100 ({{ $('Analyze and Rank Attack Paths').item.json.severity }})\n- Misconfigured resource: {{ $('Analyze and Rank Attack Paths').item.json.misconfigResourceAddress }} ({{ $('Analyze and Rank Attack Paths').item.json.misconfigResourceType }})\n\nCurrent IaC file ({{ $('Analyze and Rank Attack Paths').item.json.misconfigFilePath }}), base64-encoded content:\n{{ $json.content }}\n\nDecode the base64 content mentally, identify the specific resource block causing this attack path (overly permissive security group rule, wildcard IAM policy, public S3/network ACL, missing MFA/role trust condition, etc.), and:\n1. Write a remediationSummary (2-3 sentences) explaining why this path exists.\n2. Write remediationSteps: an array of 3-6 short, concrete steps.\n3. Produce updatedFileContent: the COMPLETE corrected file content (plain text, not base64) applying least-privilege / closing the exposure — keep all unrelated content unchanged.\n4. cvssEstimate: a number 0-10 estimating severity.\n5. jiraSummary: a short ticket title.\n6. jiraDescription: a 2-4 sentence ticket description.\n7. prBranchName: a short kebab-case branch name.\n8. prTitle: a concise PR title.\n9. prBody: a markdown PR description including the attack path, the fix, and the risk score above.\n10. commitMessage: a concise commit message.\n\nReturn ONLY a JSON object with keys: remediationSummary, remediationSteps, updatedFileContent, cvssEstimate, jiraSummary, jiraDescription, prBranchName, prTitle, prBody, commitMessage. No markdown fences, no commentary.",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 1.6
    },
    {
      "id": "6e15485f-f6c2-40df-b402-3963618fb407",
      "name": "OpenAI GPT-4.1 Mini Agent",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        2448,
        432
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini"
        },
        "options": {},
        "builtInTools": {}
      },
      "typeVersion": 1.3
    },
    {
      "id": "d40e6d9e-a2eb-4805-85c9-76139e4c953a",
      "name": "Parse AI and Build Payloads",
      "type": "n8n-nodes-base.code",
      "position": [
        2720,
        208
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Parse the AI's structured remediation output and prepare Jira + GitHub payloads\nconst item = $input.item.json;\nconst raw = item.output || item.text || item.response || '{}';\n\nlet parsed = {};\ntry {\n  const cleaned = String(raw).replace(/```json|```/g, '').trim();\n  parsed = JSON.parse(cleaned);\n} catch (e) {\n  parsed = {\n    remediationSummary: 'AI response could not be parsed automatically. Manual review required.',\n    remediationSteps: [],\n    updatedFileContent: null,\n    cvssEstimate: null,\n    jiraSummary: `Attack path risk detected: ${item.sourceAsset} -> ${item.targetAsset}`,\n    jiraDescription: String(raw),\n    prBranchName: 'attack-path-fix-' + Date.now(),\n    prTitle: 'Remediate attack path finding',\n    prBody: String(raw),\n    commitMessage: 'Remediate attack path finding'\n  };\n}\n\nconst fileInfo = $('Retrieve IaC File from GitHub').item.json;\n\nreturn {\n  json: {\n    ...item,\n    remediationSummary: parsed.remediationSummary || '',\n    remediationSteps: parsed.remediationSteps || [],\n    updatedFileContentBase64: parsed.updatedFileContent ? Buffer.from(parsed.updatedFileContent, 'utf-8').toString('base64') : null,\n    cvssEstimate: parsed.cvssEstimate || null,\n    jiraSummary: parsed.jiraSummary || `Attack path risk: ${item.sourceAsset} -> ${item.targetAsset}`,\n    jiraDescription: parsed.jiraDescription || parsed.remediationSummary || '',\n    prBranchName: parsed.prBranchName || ('attack-path-fix-' + Date.now()),\n    prTitle: parsed.prTitle || ('Remediate attack path: ' + item.sourceAsset + ' -> ' + item.targetAsset),\n    prBody: parsed.prBody || parsed.remediationSummary || '',\n    commitMessage: parsed.commitMessage || 'Remediate attack path finding',\n    currentFileSha: fileInfo.sha,\n    hasFix: !!parsed.updatedFileContent\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "2a5c6004-c3cf-45ee-a19e-d65c6f587449",
      "name": "Submit Jira Issue API",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2944,
        208
      ],
      "parameters": {
        "url": "=https://{{ $json.jiraBaseUrl.replace('https://', '') }}/rest/api/3/issue",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ fields: { project: { key: $json.jiraProjectKey }, summary: $json.jiraSummary, issuetype: { name: $json.jiraIssueType }, description: { type: 'doc', version: 1, content: [ { type: 'paragraph', content: [ { type: 'text', text: $json.jiraDescription } ] }, { type: 'paragraph', content: [ { type: 'text', text: 'Attack path: ' + $json.sourceAsset + ' -> ' + $json.targetAsset + ' (' + $json.hopCount + ' hops, risk score ' + $json.riskScore + '/100, severity ' + $json.severity + ')' } ] } ] }, labels: ['attack-path', 'auto-remediation'] } }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "jiraSoftwareCloudApi"
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "472f790a-443d-4f9e-bf36-9c6b65d10da6",
      "name": "Fetch GitHub Base Branch Reference",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3216,
        208
      ],
      "parameters": {
        "url": "=https://api.github.com/repos/{{ $('Parse AI and Build Payloads').item.json.repoOwner }}/{{ $('Parse AI and Build Payloads').item.json.repoName }}/git/ref/heads/{{ $('Parse AI and Build Payloads').item.json.baseBranch }}",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "githubApi"
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "7967a8c5-a19c-4ff8-9e70-f32f53b3db0f",
      "name": "Create New GitHub Branch",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3440,
        208
      ],
      "parameters": {
        "url": "=https://api.github.com/repos/{{ $('Parse AI and Build Payloads').item.json.repoOwner }}/{{ $('Parse AI and Build Payloads').item.json.repoName }}/git/refs",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ ref: 'refs/heads/' + $('Parse AI and Build Payloads').item.json.prBranchName, sha: $json.object.sha }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "githubApi"
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "e64ed562-2e5e-4d88-a2e3-245e98a5fad2",
      "name": "Update IaC File on Branch",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3664,
        208
      ],
      "parameters": {
        "url": "=https://api.github.com/repos/{{ $('Parse AI and Build Payloads').item.json.repoOwner }}/{{ $('Parse AI and Build Payloads').item.json.repoName }}/contents/{{ $('Parse AI and Build Payloads').item.json.misconfigFilePath }}",
        "method": "PUT",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ message: $('Parse AI and Build Payloads').item.json.commitMessage, content: $('Parse AI and Build Payloads').item.json.updatedFileContentBase64, sha: $('Parse AI and Build Payloads').item.json.currentFileSha, branch: $('Parse AI and Build Payloads').item.json.prBranchName }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "githubApi"
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "50c8e8e6-1360-4474-9c8e-3fb4fd2c0d98",
      "name": "Propose Pull Request on GitHub",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3888,
        208
      ],
      "parameters": {
        "url": "=https://api.github.com/repos/{{ $('Parse AI and Build Payloads').item.json.repoOwner }}/{{ $('Parse AI and Build Payloads').item.json.repoName }}/pulls",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ title: $('Parse AI and Build Payloads').item.json.prTitle, head: $('Parse AI and Build Payloads').item.json.prBranchName, base: $('Parse AI and Build Payloads').item.json.baseBranch, body: $('Parse AI and Build Payloads').item.json.prBody + '\\n\\nJira ticket: ' + $('Parse AI and Build Payloads').item.json.jiraBaseUrl + '/browse/' + $('Submit Jira Issue API').item.json.key }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "githubApi"
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "b369c4d3-8b16-4b17-bd9e-fa0aa267c899",
      "name": "Post PR Link to Jira",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        4192,
        208
      ],
      "parameters": {
        "url": "=https://{{ $('Parse AI and Build Payloads').item.json.jiraBaseUrl.replace('https://', '') }}/rest/api/3/issue/{{ $('Submit Jira Issue API').item.json.key }}/comment",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ body: { type: 'doc', version: 1, content: [ { type: 'paragraph', content: [ { type: 'text', text: 'Auto-remediation PR opened: ' + $json.html_url } ] } ] } }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "jiraSoftwareCloudApi"
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "18e9b3f9-c8c3-49a5-a1e1-92d3d1d207e0",
      "name": "Wait for Data Processing",
      "type": "n8n-nodes-base.wait",
      "position": [
        1488,
        288
      ],
      "parameters": {},
      "typeVersion": 1.1
    }
  ],
  "connections": {
    "Submit Jira Issue API": {
      "main": [
        [
          {
            "node": "Fetch GitHub Base Branch Reference",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Test Run Initiated": {
      "main": [
        [
          {
            "node": "Set Configuration Parameters",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create New GitHub Branch": {
      "main": [
        [
          {
            "node": "Update IaC File on Branch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for Data Processing": {
      "main": [
        [
          {
            "node": "Analyze and Rank Attack Paths",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Critical Path Detected": {
      "main": [
        [
          {
            "node": "Retrieve IaC File from GitHub",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI GPT-4.1 Mini Agent": {
      "ai_languageModel": [
        [
          {
            "node": "Remediation Strategy Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Update IaC File on Branch": {
      "main": [
        [
          {
            "node": "Propose Pull Request on GitHub",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post to Security Graph API": {
      "main": [
        [
          {
            "node": "Parse and Extract Attack Paths",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remediation Strategy Agent": {
      "main": [
        [
          {
            "node": "Parse AI and Build Payloads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI and Build Payloads": {
      "main": [
        [
          {
            "node": "Submit Jira Issue API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Configuration Parameters": {
      "main": [
        [
          {
            "node": "Post to Security Graph API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Daily Graph Scan Starts": {
      "main": [
        [
          {
            "node": "Set Configuration Parameters",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze and Rank Attack Paths": {
      "main": [
        [
          {
            "node": "If Critical Path Detected",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retrieve IaC File from GitHub": {
      "main": [
        [
          {
            "node": "Remediation Strategy Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse and Extract Attack Paths": {
      "main": [
        [
          {
            "node": "Wait for Data Processing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Propose Pull Request on GitHub": {
      "main": [
        [
          {
            "node": "Post PR Link to Jira",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch GitHub Base Branch Reference": {
      "main": [
        [
          {
            "node": "Create New GitHub Branch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false
}
