SNORF — Sequential Network Orchestration & Recon Framework
A fully automated recon pipeline built in n8n that chains together SSH execution, local LLMs via Ollama, persistent memory, and Discord notifications into a single workflow.
SNORF — Sequential Network Orchestration & Recon Framework
Technical Write-up | n8n · Ollama · Kali Linux · Qwen2.5-32b · Discord · HackTheBox
Table of Contents
- Introduction
- Architecture at a Glance
- Phase 1 — Initiation & Primary Recon
- Phase 2 — Tactical Enumeration
- Phase 3 — Data Compaction & State Management
- Phase 4 — Strategic Intelligence & The Memory Loop
- Phase 5 — The Error-Handling Execution Loop
- Why Build This?
- What's Next for SNORF
Introduction
While grinding through a medium-rated HackTheBox machine, I kept hitting the same wall — I'd find an open port, stop what I was doing, manually run a scan against it, wait, then decide what tool to run next. Over and over. I thought: what if the tool figured that out on its own, ran everything in the background, and just told me what actually mattered?
That frustration turned into SNORF — a fully automated recon pipeline built in n8n that chains together SSH execution, local LLMs via Ollama, persistent memory, and Discord notifications into a single workflow. You drop in a target IP. Everything else is handled automatically.
This write-up covers every node in the workflow — what it does, why it's built the way it is, and what design decisions went into the architecture. This isn't a tutorial on n8n basics. It's a deep dive into the engineering behind SNORF.
Architecture at a Glance
SNORF is a linear-branching pipeline. It starts with a form submission, fans out into parallel AI analysis and memory writes, then contracts back into a sequential execution loop.
Pipeline: Form Trigger → Nmap Scan → AI Agent 1 (Parse Ports) → AI Agent 2 (Generate Commands) → Execute All → Aggregate → AI Agent 3 (Compact) → AI Agent 4 (Correlate) → If/Retry Loop
Every phase uses a different Ollama model optimized for that specific job. Critically, every result gets appended to a persistent memory file on the Kali machine — so the system always knows the full history of the engagement, not just the last output.
All AI inference runs locally via Ollama — no data leaves your machine to a third-party API. The only external service is Discord for notifications.
Phase 1 — Initiation & Primary Recon
Phase 1 is the entry point. It's deliberately simple: get a target, run a baseline scan, and hand structured data to everything downstream. The quality of everything that follows depends entirely on what comes out of this phase.
Node: On form submission — Form Trigger
Type: n8n-nodes-base.formTrigger
This is the front door. It exposes a simple web form that takes two fields: IP address and Initial Intel. The IP is self-explanatory — it's the target. Initial Intel is a free-text field for anything you already know going in: credentials you found, a username, a domain name, notes from a previous session.
The reason this is a form trigger rather than a webhook or manual trigger is simplicity. You can pull it up on your phone, on another machine, or share it with a teammate. The initial intel field matters more than it looks — it feeds directly into the memory file and can change how the final strategic agent prioritizes its next steps.
Node: Execute a command — SSH to Kali, Run Nmap
Type: n8n-nodes-base.ssh
This node SSHes into the Kali instance and runs:
nmap -sC -sV {{ $json["ip address"] }}
-sC runs the default NSE scripts — things like grabbing banners, checking for common misconfigurations, and pulling service info. -sV does version detection. Together they give the AI downstream enough to work with: port numbers, service names, version strings, and any script output.
The raw stdout of this command is passed directly to AI Agent 1. No preprocessing, no filtering — the full output including timing data, host info, and all service details. The AI handles the parsing.
Node: AI Agent — Parse & Structure Nmap Output
Type: @n8n/n8n-nodes-langchain.agent + Structured Output Parser (Qwen2.5:32b)
This is the first of four AI agents. It's powered by Qwen2.5:32b via Ollama and has one job: ingest raw Nmap output and produce a clean JSON object.
The system prompt instructs it to act as a headless API endpoint — no conversational text, no markdown fences, just a single raw JSON object. The schema, enforced by the Structured Output Parser, looks like this:
{
"host": "10.129.x.x",
"ports": [
{
"port": 80,
"service": "http",
"version": "Apache httpd 2.4.41",
"tool": "ffuf"
}
]
}
The Structured Output Parser is critical here. Without it, the agent's response is a plain string. With it, n8n enforces the JSON schema and makes the data addressable downstream using $json.output.ports, $json.output.host, etc. It's the difference between passing a string around and passing a real data object.
The system prompt also includes a key rule: if Port 80 is open, generate separate entries for ffuf and nikto. This is intentional — web ports need both directory brute-forcing and vulnerability scanning, and splitting them ensures both run in parallel later.
After this node, the workflow branches into three parallel paths: Send a message (Discord alert), Execute a command2 (write to memory), and AI Agent1 (tactical enumeration). All three fire simultaneously.
Phase 2 — Tactical Enumeration (The Orchestrator)
Phase 2 is where SNORF earns its name. This is the orchestration layer — the part that looks at the attack surface, decides what to do about every open port, generates the commands, executes them, and routes the results. It's driven by a second AI agent with a completely different persona and a much larger toolset.
Node: AI Agent1 — The Spike3y Orchestrator
Type: @n8n/n8n-nodes-langchain.agent (Qwen2.5-Coder-32B-Instruct-abliterated)
This agent runs on a different model than the first: Qwen2.5-Coder-32B-Instruct-abliterated. The coder variant is used here because this agent's primary output is executable bash commands — and a code-tuned model produces more reliable, syntactically correct commands than a general-purpose model.
It receives the structured port array from Agent 1 and applies what the prompt calls "Universal Enumeration Logic" — a port-to-tool mapping baked into the system prompt:
# Web Services (80, 443, 8080, etc.)
gobuster dir -u http://TARGET_IP:PORT -w /usr/share/wordlists/dirb/common.txt -q
curl -Is http://TARGET_IP:PORT
# SMB / Windows (135, 139, 445)
nmap -p PORT --script smb-vuln* TARGET_IP
nxc smb TARGET_IP -u '' -p '' --shares
# LDAP / Active Directory (389, 636, 3268)
nxc ldap TARGET_IP -u '' -p ''
ldapsearch -x -H ldap://TARGET_IP -s base
# Database (1433, 3306, 5432)
nmap -p PORT --script mysql-info,ms-sql-info TARGET_IP
# Infrastructure (21, 22, 53, 161)
snmpwalk -c public -v2c TARGET_IP
dig axfr @TARGET_IP
The key rule baked into this agent: zero data loss. Every single open port must have a corresponding action in the output. If it's in the port array, it gets enumerated. No skipping, no ignoring "boring" ports.
The output is a JSON object with an actions array — one action per command — each containing tool, reason, and command fields. This structured output is what gets split and executed next.
Node: Code in JavaScript — Safe JSON Parsing
Type: n8n-nodes-base.code
LLMs don't always return perfectly formatted JSON — sometimes there's leading text, trailing explanation, or extra whitespace. This node handles that reality:
const jsonMatch = rawOutput.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
item.json.analysis_summary = parsed.analysis_summary;
item.json.priority = parsed.priority;
item.json.actions = parsed.actions;
}
The regex /\{[\s\S]*\}/ grabs everything between the first { and the last }, regardless of what's wrapped around it. This makes the parsing resilient to model chattiness without needing a perfectly compliant response every time. If parsing fails, the error is recorded alongside the raw output for debugging — the pipeline doesn't crash.
Node: Split Out → Execute a command1 — Per-Command Execution
Type: n8n-nodes-base.splitOut + n8n-nodes-base.ssh
The Split Out node takes the actions array and turns each element into its own n8n item. This is the key to parallel execution — instead of running commands sequentially in a loop, n8n fans them out and runs as many simultaneously as the execution engine allows.
Each resulting item contains a command field, which the SSH node fires directly against Kali: ={{ $json.command }}. The stdout of each command flows into the next phase for aggregation.
Phase 3 — Data Compaction & State Management
By the end of Phase 2, there are potentially dozens of tool outputs — a stream of stdout blobs from gobuster, nxc, nmap scripts, ldapsearch, and more. Phase 3 has two jobs: collapse all of that into something usable, and make sure nothing gets lost.
Node: Aggregate — Collecting All Tool Output
Type: n8n-nodes-base.aggregate
After the parallel SSH executions complete, each item has its own stdout field. The Aggregate node collects all of them into a single array field called all_tool_results. This is the unified data blob that everything in Phase 3 and 4 works from.
Without this node, the next AI agent would only see one tool's output at a time. With it, it sees the complete picture — every port, every tool, every finding — in a single pass.
Node: AI Agent2 — The Data Compactor
Type: @n8n/n8n-nodes-langchain.agent (Qwen2.5-coder:32b)
Raw tool output is noisy. Gobuster outputs ANSI escape codes, version headers, and legal disclaimers before showing you a single result. This agent strips all of that and extracts only what matters, under a hard 1500-character budget.
Why 1500 characters? Discord messages have a 2000-character limit. Leaving 500 characters of headroom for formatting ensures the message never gets truncated or rejected by the API.
The compaction priority order baked into the system prompt: Status 200/302 paths first, then CVEs and vulnerabilities, then domain names and credentials, then everything else. If the data doesn't fit, the highest-priority items survive.
Example output:
• Port 80: /admin → 200 OK
• Port 80: /backup → 302 → /login
• SMB: NULL session allowed — shares: [SYSVOL, NETLOGON, Data]
• LDAP: domain confirmed → logging.htb
• nmap: smb-vuln-ms17-010 — VULNERABLE
Node: Execute a command2 & command3 — Persistent Memory Writes
Type: n8n-nodes-base.ssh (Memory Writes)
This is the most important architectural decision in the entire workflow. Rather than relying on n8n's own execution context to carry state, SNORF maintains a flat text file on the Kali machine itself:
# Initial write (command2)
cat << 'EOF' > /tmp/spike_memory_{{ ip }}.txt
=== MISSION START ===
Target: {{ ip }}
Provided Credentials: {{ initialIntel }}
=== INITIAL NMAP SCAN ===
{{ JSON.stringify($json.output, null, 2) }}
EOF
# Append (command3)
cat << 'EOF' >> /tmp/spike_memory_{{ ip }}.txt
=== NEW ENUMERATION DATA ===
{{ $json.output }}
EOF
The first write (>) creates the file with the initial nmap data. Every subsequent write (>>) appends to it. The file grows throughout the engagement. By the end, it's a complete timeline: initial intel, first scan, every tool output, every AI analysis.
The file is named per-IP (spike_memory_10.129.x.x.txt), which means you can run multiple engagements simultaneously without them interfering with each other.
The reason this matters: n8n workflows are stateless between executions. If you re-run the workflow, the execution history is gone. But the Kali file persists. The strategic agent in Phase 4 reads the entire file — not just the last run — which is what gives it real longitudinal context.
Phase 4 — Strategic Intelligence & The Memory Loop
Phase 4 is the brain of SNORF. It reads everything that's been collected, correlates findings across tools, and produces an actionable next-step plan. It's also where SNORF shifts from automated scanner to something that actually reasons about the engagement.
Node: Execute a command4 — Read Full Memory File
Type: n8n-nodes-base.ssh
A simple cat /tmp/spike_memory_{{ ip }}.txt. But what it feeds downstream is substantial — the entire engagement history, from the first nmap scan to the last tool output. This is the full context window that AI Agent3 will reason over.
Node: AI Agent3 — The Strategic Intelligence Agent
Type: @n8n/n8n-nodes-langchain.agent (Qwen2.5-Coder-32B-abliterated)
This is the most complex agent in the pipeline. Its job is to find chainable vulnerabilities — connections between findings that no single tool would surface on its own.
The prompt defines specific correlation logic:
// If LDAP confirmed a domain → use hostname in web commands
// If SMB found usernames → check them against WinRM/SSH
// If credentials found → IMMEDIATELY test against all open services
// If SMB closed but RPC open → query user SIDs via RPC
The Credential-First Logic is particularly important. If the memory file contains any username/password pair — from a web form, a config file, a database dump, anywhere — the agent is instructed to make testing those credentials its first priority. It generates nxc commands against every relevant service before suggesting anything else.
The output is a structured JSON object:
{
"tactical_overview": "2-3 sentence summary of current state",
"correlated_findings": ["LDAP confirmed logging.htb", "..."],
"next_steps": [
{
"tool": "nxc",
"command": "nxc smb 10.129.x.x -u admin -p Password123",
"goal": "Test recovered credentials for SMB access"
}
],
"priority_level": "Critical"
}
Node: Code in JavaScript2 — Placeholder Substitution
Type: n8n-nodes-base.code
The strategic agent uses TARGET_IP and TARGET_DOMAIN as placeholders in its commands — this keeps the agent's prompt generic and reusable across any engagement. This node swaps those placeholders with the actual values before execution:
const realIp = $('On form submission').first().json["ip address"];
let realDomain = $('AI Agent').first().json.output.host;
cmd = cmd.replace(/TARGET_IP/g, realIp);
cmd = cmd.replace(/TARGET_DOMAIN/g, realDomain);
The domain fallback is also handled here — if no domain was discovered yet, TARGET_DOMAIN is replaced with the raw IP instead of breaking the command. Commands remain executable at every stage of the engagement.
Phase 5 — The Error-Handling Execution Loop
The final phase handles the actual execution of the strategic agent's next steps — and handles failures gracefully rather than crashing the workflow.
Node: Split Out1 → Code in JavaScript2 → Execute a command5
The same pattern as Phase 2: split the next_steps array into individual items, run placeholder substitution on each command, then execute them via SSH. Each command's result feeds into the If node.
Node: If → AI Agent4 — The Retry Loop
Type: n8n-nodes-base.if + langchain.agent
This is SNORF's self-healing mechanism. The If node checks two conditions on every executed command:
- Did the exit code come back non-zero? (
$json.code !== 0) - Was there anything on stderr? (
$json.stderr !== "")
If either condition is true, the execution is considered a failure and gets routed to AI Agent4 — a recovery agent powered by Qwen2.5-Coder-32B. This agent receives the failed command and its error output, diagnoses what went wrong, and generates a corrected command to retry.
The corrected command loops back through Execute a command5 and hits the If node again. If it passes, execution continues normally. This prevents the entire pipeline from stalling because gobuster couldn't find a wordlist or nxc timed out on a closed port.
Note: The retry loop doesn't have a hard cap in the current build. A command that consistently fails will loop indefinitely. Adding a retry counter is a planned improvement.
Why Build This?
The honest answer is: because I was tired of babysitting a terminal. But there's a deeper point about how AI tooling should actually work in a pentesting context.
Most "AI pentesting tools" are wrappers around a single LLM call. You ask it something, it tells you something, you manually go do it. SNORF is different because the AI isn't an advisor — it's an executor. It doesn't just suggest running gobuster; it runs gobuster, reads the output, decides what that means, writes it to memory, and figures out what to do next. The human stays in the loop through Discord, but they don't have to drive.
The memory architecture is the part I'm most proud of. Using a flat file on the Kali machine instead of trying to maintain state in n8n's execution context is simple, durable, and completely inspectable. You can cat the file at any point and see exactly what the system knows. No black box.
The multi-model approach also matters. Different phases use different models for a reason: the coder variant handles command generation better; the base 32b model is better at natural language parsing of raw nmap output; the abliterated model handles the strategic correlation layer where the system prompt is most complex. Matching the model to the task makes the whole thing more reliable than using one model for everything.
What's Next for SNORF
The current build is a proof of concept that actually works — I've run it against real HTB machines. Clear next steps: adding a retry limit to the error loop, integrating web fuzzing results back into the memory file more granularly, building a proper front-end dashboard instead of Discord, and experimenting with smaller quantized models to reduce inference time per agent call. I also want to train a model on kali tools specifically so the syntax is near perfect.
If you want to talk about the build, have questions about the n8n workflow, or want to see the full workflow file — reach out.