From 5432426443fe438bd46da32d081bb634d948f631 Mon Sep 17 00:00:00 2001 From: "Abliteration.ai" Date: Sat, 1 Aug 2026 11:16:03 -0700 Subject: [PATCH] Add standalone script LlmAlertExplainer.js Sends ZAP alerts to an OpenAI-compatible chat completions endpoint and prints an explanation plus remediation guidance for each alert, to help triage scan results or draft report text. Endpoint, API key env var, and model are configurable at the top of the script. Signed-off-by: Abliteration.ai --- CHANGELOG.md | 1 + standalone/LlmAlertExplainer.js | 152 ++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 standalone/LlmAlertExplainer.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a202252..6c4733ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Added +- Standalone script 'LlmAlertExplainer.js' - Sends alerts to an OpenAI-compatible chat completions endpoint and prints explanations plus remediation guidance. - Standalone script 'PrivateMethodAccess.js' - Variant script 'AddUrlParams.js' - Extender script 'ScanMonitor.js' diff --git a/standalone/LlmAlertExplainer.js b/standalone/LlmAlertExplainer.js new file mode 100644 index 00000000..2a74f5ea --- /dev/null +++ b/standalone/LlmAlertExplainer.js @@ -0,0 +1,152 @@ +// Description: Sends ZAP alerts to an OpenAI-compatible chat completions endpoint and +// prints an explanation plus remediation guidance for each alert. Useful for triaging +// scan results or drafting report text. Run from the Script Console after a scan +// (or after opening a session that has alerts). +// Author: Abliteration.ai (https://abliteration.ai) + +// Configuration ---------------------------------------------------------------- + +// Base URL of any OpenAI-compatible API (no trailing slash). +// Default: Abliteration.ai (https://docs.abliteration.ai). Other examples: +// OpenAI: https://api.openai.com/v1 +// Ollama: http://localhost:11434/v1 +// LM Studio: http://localhost:1234/v1 +var BASE_URL = "https://api.abliteration.ai/v1"; + +// Name of the environment variable holding the API key (never hardcode the key here). +// For OpenAI use "OPENAI_API_KEY"; local servers usually accept any non-empty value. +var API_KEY_ENV = "ABLIT_KEY"; + +// Model to request. For Abliteration.ai see https://docs.abliteration.ai/models +var MODEL = "abliterated-model"; + +// Only explain alerts whose URI starts with this value; empty string means all sites. +var SITE_FILTER = ""; + +// Maximum number of alerts to explain per run (each alert is one API call). +var MAX_ALERTS = 20; + +// ------------------------------------------------------------------------------- + +var System = Java.type("java.lang.System"); +var ExtensionAlert = Java.type( + "org.zaproxy.zap.extension.alert.ExtensionAlert", +); +var Alert = Java.type("org.parosproxy.paros.core.scanner.Alert"); +var HttpRequestHeader = Java.type( + "org.parosproxy.paros.network.HttpRequestHeader", +); +var HttpMessage = Java.type("org.parosproxy.paros.network.HttpMessage"); +var HttpSender = Java.type("org.parosproxy.paros.network.HttpSender"); + +var sender = new HttpSender(HttpSender.MANUAL_REQUEST_INITIATOR); + +var SYSTEM_PROMPT = + "You are a senior application security engineer. " + + "For the given finding from an OWASP ZAP scan, explain in plain language: " + + "what the issue is, how an attacker could exploit it, and how likely the " + + "finding is to be a false positive. Then give concrete remediation guidance " + + "with code or configuration examples where applicable. Be concise."; + +function explain(alert) { + var details = + "Alert: " + + alert.name + + "\nRisk: " + + Alert.MSG_RISK[alert.risk] + + "\nConfidence: " + + Alert.MSG_CONFIDENCE[alert.confidence] + + "\nURL: " + + alert.uri; + if (alert.param) { + details += "\nParameter: " + alert.param; + } + if (alert.attack) { + details += "\nAttack: " + alert.attack; + } + if (alert.evidence) { + details += "\nEvidence: " + alert.evidence; + } + details += "\nDescription: " + alert.description; + if (alert.solution) { + details += "\nSolution suggested by the scan rule: " + alert.solution; + } + + var header = new HttpRequestHeader( + "POST " + BASE_URL + "/chat/completions HTTP/1.1", + ); + header.setHeader("Content-Type", "application/json"); + header.setHeader("Accept", "application/json"); + header.setHeader("Authorization", "Bearer " + apiKey); + + var msg = new HttpMessage(header); + msg.setRequestBody( + JSON.stringify({ + model: MODEL, + messages: [ + { role: "system", content: SYSTEM_PROMPT }, + { role: "user", content: details }, + ], + }), + ); + msg.getRequestHeader().setContentLength(msg.getRequestBody().length()); + + sender.sendAndReceive(msg); + + var status = msg.getResponseHeader().getStatusCode(); + var responseStr = msg.getResponseBody().toString(); + if (status !== 200) { + return "Request failed with HTTP " + status + ": " + responseStr; + } + var json = JSON.parse(responseStr); + if (!json.choices || !json.choices.length) { + return "Unexpected response: " + responseStr; + } + return json.choices[0].message.content; +} + +var apiKey = System.getenv(API_KEY_ENV); +if (!apiKey) { + print( + "API key not found. Set the " + + API_KEY_ENV + + " environment variable (or change API_KEY_ENV in the script) and re-run.", + ); +} else { + var extAlert = control.getExtensionLoader().getExtension(ExtensionAlert.NAME); + if (extAlert == null) { + print("Could not access the alerts extension."); + } else { + var alerts = extAlert.getAllAlerts(); + var count = 0; + for (var i = 0; i < alerts.length && count < MAX_ALERTS; i++) { + var alert = alerts[i]; + if (SITE_FILTER && alert.uri.indexOf(SITE_FILTER) !== 0) { + continue; + } + count++; + print( + "=== " + + alert.name + + " [" + + Alert.MSG_RISK[alert.risk] + + "] " + + alert.uri + + " ===", + ); + try { + print(explain(alert)); + } catch (e) { + print("Error requesting explanation: " + e); + } + print(""); + } + print( + "Explained " + + count + + " of " + + alerts.length + + " alert(s). Tune SITE_FILTER and MAX_ALERTS at the top of the script.", + ); + } +}