- Layer 4 EntropyScanner: Shannon entropy, Base32/Base64 detection, CVE-2025-55284 ping/nslookup exfil, EchoLeak markdown pattern, DNS tunneling (iodine/dnscat) - Layer 5 UnicodeScanner: ASCII Smuggling (U+E0000 Tags Block), Variant Selectors, Zero-Width steganography, CamoLeak image-ordering (CVE-2025-53773), homoglyphs, BiDi override, high-entropy URL params - 30 DNS covert channel rules (dns-001 to dns-030) - ATLASMapper: 29 techniques (ATLAS v5.4.0 Feb 2026), added AML.T0062 (Agent Tool Invocation), AML.TA0015 (C2 tactic), memory poisoning, multi-agent trust, CamoLeak, Unicode steganography mappings - Rule count: 72 → 102 - Build: tsup 316ms, zero TypeScript errors
137 lines
4.2 KiB
JavaScript
137 lines
4.2 KiB
JavaScript
/**
|
|
* ShieldX n8n Community Node
|
|
*
|
|
* Scans prompts through the ShieldX proxy before sending to LLMs.
|
|
* Add this as a custom node in n8n.
|
|
*
|
|
* Setup:
|
|
* 1. Copy to ~/.n8n/custom/nodes/ShieldX.node.js
|
|
* 2. Set SHIELDX_PROXY_URL in n8n environment (default: http://localhost:11435)
|
|
* 3. Add ShieldX node BEFORE any AI/LLM node in your workflow
|
|
*
|
|
* The node scans input text and either passes it through (clean)
|
|
* or blocks it (threat detected), based on the configured action.
|
|
*/
|
|
|
|
module.exports = {
|
|
description: {
|
|
displayName: 'ShieldX',
|
|
name: 'shieldX',
|
|
group: ['transform'],
|
|
version: 1,
|
|
description: 'Scan prompts for injection attacks before sending to LLMs',
|
|
defaults: { name: 'ShieldX' },
|
|
inputs: ['main'],
|
|
outputs: ['main'],
|
|
properties: [
|
|
{
|
|
displayName: 'Input Field',
|
|
name: 'inputField',
|
|
type: 'string',
|
|
default: 'text',
|
|
description: 'The field name containing the text to scan',
|
|
},
|
|
{
|
|
displayName: 'Proxy URL',
|
|
name: 'proxyUrl',
|
|
type: 'string',
|
|
default: 'http://localhost:11435',
|
|
description: 'ShieldX proxy URL',
|
|
},
|
|
{
|
|
displayName: 'On Threat',
|
|
name: 'onThreat',
|
|
type: 'options',
|
|
options: [
|
|
{ name: 'Block (stop workflow)', value: 'block' },
|
|
{ name: 'Warn (add flag, continue)', value: 'warn' },
|
|
{ name: 'Log Only', value: 'log' },
|
|
],
|
|
default: 'block',
|
|
description: 'Action when threat is detected',
|
|
},
|
|
{
|
|
displayName: 'Min Threat Level',
|
|
name: 'minThreatLevel',
|
|
type: 'options',
|
|
options: [
|
|
{ name: 'Low', value: 'low' },
|
|
{ name: 'Medium', value: 'medium' },
|
|
{ name: 'High', value: 'high' },
|
|
{ name: 'Critical', value: 'critical' },
|
|
],
|
|
default: 'medium',
|
|
description: 'Minimum threat level to trigger action',
|
|
},
|
|
],
|
|
},
|
|
|
|
async execute() {
|
|
const items = this.getInputData()
|
|
const inputField = this.getNodeParameter('inputField', 0)
|
|
const proxyUrl = this.getNodeParameter('proxyUrl', 0)
|
|
const onThreat = this.getNodeParameter('onThreat', 0)
|
|
const minLevel = this.getNodeParameter('minThreatLevel', 0)
|
|
|
|
const LEVEL_ORDER = ['none', 'low', 'medium', 'high', 'critical']
|
|
const minIdx = LEVEL_ORDER.indexOf(minLevel)
|
|
|
|
const results = []
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
const item = items[i]
|
|
const input = item.json[inputField] || ''
|
|
|
|
if (!input) {
|
|
results.push(item)
|
|
continue
|
|
}
|
|
|
|
try {
|
|
const response = await this.helpers.httpRequest({
|
|
method: 'POST',
|
|
url: `${proxyUrl}/shieldx/scan`,
|
|
body: { input },
|
|
headers: { 'Content-Type': 'application/json' },
|
|
timeout: 5000,
|
|
})
|
|
|
|
const threatIdx = LEVEL_ORDER.indexOf(response.threatLevel || 'none')
|
|
const isAboveThreshold = threatIdx >= minIdx
|
|
|
|
// Add scan metadata to item
|
|
item.json.shieldx = {
|
|
scanned: true,
|
|
detected: response.detected || false,
|
|
threatLevel: response.threatLevel || 'none',
|
|
killChainPhase: response.killChainPhase || 'none',
|
|
action: response.action || 'allow',
|
|
matchedPatterns: response.matchedPatterns || [],
|
|
latencyMs: response.latencyMs || 0,
|
|
}
|
|
|
|
if (response.detected && isAboveThreshold) {
|
|
if (onThreat === 'block') {
|
|
throw new Error(
|
|
`ShieldX blocked: ${response.threatLevel} threat detected ` +
|
|
`(${response.killChainPhase}). Patterns: ${(response.matchedPatterns || []).join(', ')}`
|
|
)
|
|
}
|
|
// warn or log — continue with flag
|
|
item.json.shieldx.blocked = onThreat === 'block'
|
|
}
|
|
|
|
results.push(item)
|
|
} catch (err) {
|
|
if (err.message?.startsWith('ShieldX blocked')) throw err
|
|
// Proxy unavailable — fail open
|
|
console.warn('[ShieldX] Proxy unavailable, failing open:', err.message)
|
|
item.json.shieldx = { scanned: false, error: 'Proxy unavailable' }
|
|
results.push(item)
|
|
}
|
|
}
|
|
|
|
return [results]
|
|
},
|
|
}
|