├── AUTHORS ├── gradle.properties ├── settings.gradle ├── img ├── demo.gif ├── ai_chat.png └── quick_actions.png ├── .gitignore ├── Dockerfile ├── BappManifest.bmf ├── src └── main │ ├── java │ └── com │ │ └── hopla │ │ ├── ai │ │ ├── AIProviderType.java │ │ ├── AIProviderFactory.java │ │ ├── LLMConfig.java │ │ ├── BurpProvider.java │ │ ├── AIChats.java │ │ ├── AIProvider.java │ │ ├── OpenAIProvider.java │ │ ├── GeminiProvider.java │ │ ├── AIConfiguration.java │ │ ├── OllamaProvider.java │ │ └── AIQuickAction.java │ │ ├── ContextMenu.java │ │ ├── Constants.java │ │ ├── PayloadMenu.java │ │ ├── PayloadDefinition.java │ │ ├── Utils.java │ │ ├── CommonMenu.java │ │ ├── MenuBar.java │ │ ├── LocalPayloadsManager.java │ │ ├── Completer.java │ │ ├── AutoCompleteMenu.java │ │ ├── PayloadManager.java │ │ ├── HopLa.java │ │ ├── SearchReplaceWindow.java │ │ └── AIChatPanel.java │ └── resources │ ├── style.css │ ├── ai-bapp-configuration-sample.yaml │ └── ai-configuration-sample.yaml ├── LICENSE ├── BappDescription.html └── README.md /AUTHORS: -------------------------------------------------------------------------------- 1 | * Alexis Danizan -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.caching=true -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'HopLa' -------------------------------------------------------------------------------- /img/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synacktiv/HopLa/HEAD/img/demo.gif -------------------------------------------------------------------------------- /img/ai_chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synacktiv/HopLa/HEAD/img/ai_chat.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build 3 | releases 4 | gradle 5 | gradlew 6 | gradlew.bat 7 | .idea -------------------------------------------------------------------------------- /img/quick_actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synacktiv/HopLa/HEAD/img/quick_actions.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/gradle:8.14.1-jdk21 2 | WORKDIR /data 3 | COPY src . 4 | COPY build.gradle . 5 | COPY settings.gradle . 6 | CMD "gradle build --debug --warning-mode all" -------------------------------------------------------------------------------- /BappManifest.bmf: -------------------------------------------------------------------------------- 1 | Uuid: 48006894b97966581660047517ea3f42 2 | ExtensionType: 1 3 | Name: HopLa 4 | RepoName: HopLa 5 | ScreenVersion: 2.0 6 | SerialVersion: 0 7 | MinPlatformVersion: 4 8 | ProOnly: False 9 | Author: Alexis Danizan 10 | ShortDescription: Enables autocompletion for payloads and AI features. 11 | EntryPoint: releases/HopLa.jar 12 | BuildCommand: gradle build 13 | SupportedProducts: Pro, Community -------------------------------------------------------------------------------- /src/main/java/com/hopla/ai/AIProviderType.java: -------------------------------------------------------------------------------- 1 | package com.hopla.ai; 2 | 3 | public enum AIProviderType { 4 | OLLAMA("Ollama"), 5 | GEMINI("Gemini"), 6 | OPENAI("OpenAI"), 7 | BURP("Burp"); 8 | 9 | private final String displayName; 10 | 11 | AIProviderType(String displayName) { 12 | this.displayName = displayName; 13 | } 14 | 15 | @Override 16 | public String toString() { 17 | return displayName; 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/java/com/hopla/ai/AIProviderFactory.java: -------------------------------------------------------------------------------- 1 | package com.hopla.ai; 2 | 3 | public class AIProviderFactory { 4 | public static AIProvider createProvider(AIProviderType type, LLMConfig config, LLMConfig.Provider providerConfig) { 5 | switch (type) { 6 | case GEMINI: 7 | return new GeminiProvider(config, providerConfig); 8 | case OLLAMA: 9 | return new OllamaProvider(config, providerConfig); 10 | case OPENAI: 11 | return new OpenAIProvider(config, providerConfig); 12 | case BURP: 13 | return new BurpProvider(config, providerConfig); 14 | default: 15 | throw new IllegalArgumentException("Provider unsupported: " + type); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/resources/style.css: -------------------------------------------------------------------------------- 1 | body * { 2 | font-family: monospace; 3 | font-size: 12px; 4 | word-wrap: break-word; 5 | overflow-wrap: break-word; 6 | margin: 0 7 | } 8 | 9 | 10 | .user, .assistant { 11 | margin-bottom: 10px; 12 | } 13 | 14 | .user .role { 15 | color: #3b5998; 16 | font-weight: bold; 17 | } 18 | 19 | .assistant .role { 20 | color: #8b9dc3; 21 | font-weight: bold; 22 | } 23 | 24 | * { 25 | white-space: pre-wrap; 26 | word-break: break-word; 27 | } 28 | 29 | pre { 30 | color: #DC2525; 31 | white-space: pre-wrap; 32 | word-break: break-word; 33 | } 34 | 35 | p { 36 | white-space: pre-wrap; 37 | word-break: break-word; 38 | } 39 | 40 | ul { 41 | margin-left: 16px; 42 | } 43 | 44 | a { 45 | color: #1a73e8; 46 | text-decoration: none; 47 | } 48 | -------------------------------------------------------------------------------- /src/main/resources/ai-bapp-configuration-sample.yaml: -------------------------------------------------------------------------------- 1 | shortcut_ai_chat: Ctrl+J 2 | shortcut_quick_action: Ctrl+Alt+O 3 | 4 | defaults: 5 | timeout_sec: 60 6 | 7 | providers: 8 | BURP: 9 | enabled: true 10 | #chat_system_prompt: REPLACE_ME 11 | chat_params: 12 | temperature: 0.0 13 | #quick_action_system_prompt: REPLACE_ME 14 | quick_action_params: 15 | temperature: 0.0 16 | 17 | prompts: 18 | - name: technologies 19 | description: "Fingerprint web technologies" 20 | content: | 21 | Analyze the following HTTP response and identify the web technologies used. 22 | List your reasoning for each technology detected. 23 | 24 | quick_actions: 25 | - name: multipart 26 | description: "Transform request to multipart" 27 | content: | 28 | Transform the following HTTP POST request into a multipart/form-data request: 29 | - name: json 30 | description: "Transform request to json" 31 | content: | 32 | Transform the following HTTP POST request into a JSON request: 33 | - name: headers_name 34 | description: "Extract HTTP header names" 35 | content: | 36 | From the HTTP request below, extract only the unique header names. List each name on a separate line. Do not include header values. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Synacktiv 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /src/main/java/com/hopla/ContextMenu.java: -------------------------------------------------------------------------------- 1 | package com.hopla; 2 | 3 | import burp.api.montoya.MontoyaApi; 4 | import burp.api.montoya.ui.contextmenu.ContextMenuEvent; 5 | import burp.api.montoya.ui.contextmenu.ContextMenuItemsProvider; 6 | import burp.api.montoya.ui.contextmenu.MessageEditorHttpRequestResponse; 7 | 8 | import javax.swing.*; 9 | import java.awt.*; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public class ContextMenu implements ContextMenuItemsProvider { 14 | 15 | private final PayloadManager payloadManager; 16 | private final CommonMenu commonMenu; 17 | 18 | 19 | public ContextMenu(MontoyaApi api, PayloadManager payloadManager) { 20 | this.payloadManager = payloadManager; 21 | this.commonMenu = new CommonMenu(api); 22 | 23 | } 24 | 25 | @Override 26 | public List provideMenuItems(ContextMenuEvent event) { 27 | if (event.messageEditorRequestResponse().isEmpty()) { 28 | return null; 29 | } 30 | 31 | MessageEditorHttpRequestResponse messageEditor = event.messageEditorRequestResponse().get(); 32 | 33 | List items = new ArrayList<>(); 34 | 35 | for (Component c : payloadManager.getPayloads().buildMenu((payload) -> { 36 | Utils.insertPayload(messageEditor, payload.value, event.inputEvent()); 37 | })) { 38 | items.add(c); 39 | } 40 | 41 | JMenu customKeywordsMenu = HopLa.localPayloadsManager.buildMenu((payload) -> { 42 | Utils.insertPayload(messageEditor, payload, event.inputEvent()); 43 | }); 44 | items.add(customKeywordsMenu); 45 | 46 | 47 | for (Component c : this.commonMenu.buildMenu(messageEditor, event.inputEvent(), () -> { 48 | })) { 49 | items.add(c); 50 | } 51 | 52 | return items; 53 | } 54 | } -------------------------------------------------------------------------------- /BappDescription.html: -------------------------------------------------------------------------------- 1 |

HopLa extension enhances Burp Suite with intelligent autocompletion and built-in payloads to simplify intrusion testing. 2 | It supports integration with AI providers like Ollama, OpenAI, and Gemini to offer advanced features such as chat and 3 | content generation/transformation. You can also add your own payloads to tailor it to your needs! 4 |

5 | 6 |

Features

7 | 18 | 19 |

Basic usage

20 | 21 |

By default, HopLa comes with a built-in set of payloads. You can extend them by loading your own custom YAML file via the top menu. 22 | (See the Default payloads file for reference.)

23 | 24 |

AI providers can be configured by importing your YAML configuration (see Configure AI providers). 25 | HopLa supports multiple AI providers (OpenAI, Gemini, Ollama), but AI-powered autocompletion is only available with Ollama. 26 |

27 | 28 |

Several keyboard shortcuts are predefined by default and can be customized through configuration files.

29 | 37 | 38 |

If you're using i3, add the following line to your $HOME/.config/i3/config to enable floating mode for the frame:

39 |

40 |     for_window [class=".*burp-StartBurp.*" title="^ $"] floating enable
41 | 
42 | 43 |

For more information, please refer to the documentation at GitHub HopLa.

-------------------------------------------------------------------------------- /src/main/java/com/hopla/ai/LLMConfig.java: -------------------------------------------------------------------------------- 1 | package com.hopla.ai; 2 | 3 | import java.net.Proxy; 4 | import java.util.ArrayList; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | public class LLMConfig { 10 | public String shortcut_ai_chat; 11 | public String shortcut_quick_action; 12 | public int autocompletion_min_chars = 1; 13 | public Map providers; 14 | public Defaults defaults; 15 | public List prompts = new ArrayList<>(); 16 | public List quick_actions = new ArrayList<>(); 17 | 18 | public static class ProxyConfig { 19 | public boolean enabled = false; 20 | public String host = "127.0.0.1"; 21 | public int port = 8080; 22 | public Proxy.Type type = Proxy.Type.DIRECT; 23 | public String username = ""; 24 | public String password = ""; 25 | 26 | public String toString() { 27 | return this.type + " " + this.host + " " + this.port; 28 | } 29 | } 30 | 31 | public static class Provider { 32 | public String name; 33 | public boolean enabled; 34 | public String chat_model; 35 | public String completion_model; 36 | public String quick_action_model; 37 | public String chat_system_prompt = ""; 38 | public String completion_system_prompt = ""; 39 | public String quick_action_system_prompt = ""; 40 | public String api_key; 41 | public String chat_endpoint; 42 | public String completion_endpoint; 43 | public String quick_action_endpoint; 44 | public String completion_prompt; 45 | public Map headers = new HashMap<>(); 46 | public Map completion_params = new HashMap<>(); 47 | public Map chat_params = new HashMap<>(); 48 | public Map quick_action_params = new HashMap<>(); 49 | public List completion_stops = new ArrayList<>(); 50 | public List chat_stops = new ArrayList<>(); 51 | public List quick_action_stops = new ArrayList<>(); 52 | public ProxyConfig proxy = new ProxyConfig(); 53 | } 54 | 55 | public static class Defaults { 56 | public String chat_provider; 57 | public String completion_provider; 58 | public String quick_action_provider; 59 | public int timeout_sec; 60 | } 61 | 62 | public static class Prompt { 63 | public String name; 64 | public String description; 65 | public String content; 66 | 67 | @Override 68 | public String toString() { 69 | return name + ": " + description; 70 | } 71 | } 72 | 73 | public static class QuickAction { 74 | public String name; 75 | public String description; 76 | public String content; 77 | 78 | @Override 79 | public String toString() { 80 | return name + ": " + description; 81 | } 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /src/main/java/com/hopla/Constants.java: -------------------------------------------------------------------------------- 1 | package com.hopla; 2 | 3 | public final class Constants { 4 | public static final String VERSION = "2.1.0"; 5 | public static final String EXTENSION_NAME = "HopLa"; 6 | public static final String INIT_MESSAGE = "HopLa initialized v" + VERSION + "\n\nFor i3, add the following line to $HOME/.config/i3/config for floating frame:\n" + 7 | " for_window [class=\".*burp-StartBurp.*\" title=\"^ $\"] floating enable\n\nHappy hacking !\n@alexisdanizan\n--------------"; 8 | public static final String PREFERENCE_CUSTOM_PATH = "HOPLA_PAYLOAD_PATH"; 9 | public static final String PREFERENCE_AUTOCOMPLETION = "HOPLA_AUTOCOMPLETION"; 10 | public static final String PREFERENCE_SHORTCUTS = "HOPLA_SHORTCUTS"; 11 | public static final String PREFERENCE_AI = "HOPLA_AI"; 12 | public static final String PREFERENCE_LOCAL_DICT = "HOPLA_LOCAL_DICT"; 13 | public static final String PREFERENCE_AI_CONFIGURATION = "HOPLA_AI_CONFIGURATION"; 14 | public static final String PREFERENCE_AI_CHATS = "HOPLA_AI_CHATS"; 15 | public static final String DEFAULT_PAYLOAD_RESOURCE_PATH = "/default-payloads.enc.yaml"; 16 | public static final String DEFAULT_AI_CONFIGURATION_PATH = "/ai-configuration-sample.yaml"; 17 | public static final String DEFAULT_BAPP_AI_CONFIGURATION_PATH = "/ai-bapp-configuration-sample.yaml"; 18 | public static final String MENU_ITEM_CHOOSE_PAYLOAD = "Choose payloads file"; 19 | public static final String MENU_ITEM_CHOOSE_AI_CONFIGURATION = "Choose AI configuration file"; 20 | public static final String MENU_ITEM_RELOAD_PAYLOADS = "Reload Payloads"; 21 | public static final String MENU_ITEM_RELOAD_AI_CONFIGURATION = "Reload AI Configuration"; 22 | public static final String MENU_ITEM_AUTOCOMPLETION = "Enable Autocompletion"; 23 | public static final String MENU_ITEM_SHORTCUTS = "Enable Shortcuts"; 24 | public static final String MENU_ITEM_AI_AUTOCOMPLETION = "Enable AI autocompletion"; 25 | public static final String MENU_ITEM_EXPORT_DEFAULT_AI_CONF = "Export default AI configuration"; 26 | public static final String MENU_ITEM_CLEAR_PREFERENCES = "Clear preferences"; 27 | public static final String MENU_ITEM_EXPORT_DEFAULT_PAYLOADS = "Export default payloads"; 28 | public static final String ERROR_INVALID_FILE_EXTENSION = "Please select a .yaml or .yml file."; 29 | public static final String ERROR_EMPTY_FILE = "The selected YAML file is empty or invalid."; 30 | public static final String ERROR_INVALID_FILE = "Failed to load the file:\n"; 31 | public static final String ERROR_BURP_AI_DISABLED = "This feature is only available on the AI version of Burp."; 32 | public static final String FILE_LOADED = "Payloads file loaded"; 33 | public static final String CONFIGURATION_FILE_LOADED = "AI configuration file loaded"; 34 | public static final String ERROR_TITLE = "HopLa Error"; 35 | public static final String DEFAULT_RESOURCE_ENCRYPT_KEY = "1234567890123456"; 36 | // Allow external AI providers 37 | public static final boolean EXTERNAL_AI = true; 38 | public static boolean DEBUG = false; 39 | public static boolean DEBUG_AI = false; 40 | public static boolean AWT_DEBUG = false; 41 | 42 | private Constants() { 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/hopla/PayloadMenu.java: -------------------------------------------------------------------------------- 1 | package com.hopla; 2 | 3 | import burp.api.montoya.MontoyaApi; 4 | import burp.api.montoya.ui.contextmenu.MessageEditorHttpRequestResponse; 5 | 6 | import javax.swing.*; 7 | import java.awt.*; 8 | import java.awt.event.FocusEvent; 9 | import java.awt.event.FocusListener; 10 | import java.awt.event.InputEvent; 11 | 12 | import static com.hopla.Utils.generateJWindow; 13 | 14 | public class PayloadMenu { 15 | private static final int MARGIN_PAYLOAD_MENU = 20; 16 | private static final int PAYLOAD_MENU_WIDTH = 250; 17 | private static final int PAYLOAD_MENU_HEIGHT = 300; 18 | private final PayloadManager payloadManager; 19 | private final CommonMenu commonMenu; 20 | private JWindow frame; 21 | 22 | public PayloadMenu(PayloadManager payloadManager, MontoyaApi api) { 23 | this.payloadManager = payloadManager; 24 | this.commonMenu = new CommonMenu(api); 25 | } 26 | 27 | public void show(MessageEditorHttpRequestResponse messageEditor, InputEvent event) { 28 | if (frame != null && frame.isDisplayable()) { 29 | frame.dispose(); 30 | frame = null; 31 | return; 32 | } 33 | 34 | frame = generateJWindow(); 35 | frame.setMinimumSize(new Dimension(PAYLOAD_MENU_WIDTH, PAYLOAD_MENU_HEIGHT)); 36 | 37 | JMenuBar menuBar = new JMenuBar(); 38 | 39 | menuBar.setLayout(new GridLayout(0, 1)); 40 | frame.setLayout(new BorderLayout()); 41 | frame.add(menuBar); 42 | 43 | PayloadDefinition payloads = payloadManager.getPayloads(); 44 | for (Component c : payloads.buildMenu((payload) -> { 45 | Utils.insertPayload(messageEditor, payload.value, event); 46 | if (frame != null) { 47 | frame.dispose(); 48 | } 49 | })) { 50 | menuBar.add(c); 51 | } 52 | 53 | JMenu customKeywordsMenu = HopLa.localPayloadsManager.buildMenu((payload) -> { 54 | Utils.insertPayload(messageEditor, payload, event); 55 | if (frame != null) { 56 | frame.dispose(); 57 | } 58 | }); 59 | menuBar.add(customKeywordsMenu); 60 | 61 | for (Component c : this.commonMenu.buildMenu(messageEditor, event, () -> { 62 | if (frame != null) { 63 | frame.dispose(); 64 | } 65 | })) { 66 | menuBar.add(c); 67 | } 68 | 69 | event.getComponent().addFocusListener(new FocusListener() { 70 | @Override 71 | public void focusGained(FocusEvent e) { 72 | } 73 | 74 | @Override 75 | public void focusLost(FocusEvent e) { 76 | if (frame != null) { 77 | frame.dispose(); 78 | } 79 | } 80 | }); 81 | 82 | frame.pack(); 83 | 84 | Point mousePos = MouseInfo.getPointerInfo().getLocation(); 85 | mousePos.x -= MARGIN_PAYLOAD_MENU; 86 | mousePos.y -= frame.getHeight() / 2; 87 | 88 | frame.setLocation(mousePos); 89 | 90 | frame.setVisible(true); 91 | 92 | 93 | } 94 | 95 | public void dispose() { 96 | if (frame != null) { 97 | frame.dispose(); 98 | } 99 | 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/resources/ai-configuration-sample.yaml: -------------------------------------------------------------------------------- 1 | shortcut_ai_chat: Ctrl+J 2 | shortcut_quick_action: Ctrl+Alt+O 3 | #autocompletion_min_chars: 1 # Minimum input length for AI-powered autocompletion (default: 1) 4 | 5 | providers: 6 | OPENAI: 7 | enabled: true 8 | chat_model: gpt-4.1 9 | chat_endpoint: https://api.openai.com/v1/chat/completions 10 | #chat_model_system_prompt: REPLACE_ME 11 | quick_action_model: gpt-4.1 12 | #quick_action_system_prompt: REPLACE_ME 13 | quick_action_endpoint: https://api.openai.com/v1/chat/completions 14 | headers: 15 | Authorization: "Bearer REPLACE_ME" 16 | proxy: 17 | enabled: true 18 | host: 127.0.0.1 19 | port: 5555 20 | username: user123 21 | password: pass123 22 | type: SOCKS # SOCKS or HTTP 23 | GEMINI: 24 | enabled: true 25 | chat_model: gemini-2.0-flash 26 | chat_endpoint: https://generativelanguage.googleapis.com/v1beta/models/@model:streamGenerateContent?alt=sse&key=@key #HopLa replace @key with api_key value 27 | #chat_model_system_prompt: REPLACE_ME 28 | quick_action_endpoint: https://generativelanguage.googleapis.com/v1beta/models/@model:streamGenerateContent?alt=sse&key=@key #HopLa replace @key with api_key value 29 | #quick_action_system_prompt: REPLACE_ME 30 | api_key: REPLACE_ME 31 | proxy: 32 | enabled: true 33 | host: 127.0.0.1 34 | port: 5555 35 | username: user123 36 | password: pass123 37 | type: SOCKS # SOCKS or HTTP 38 | 39 | OLLAMA: 40 | enabled: true 41 | completion_model: qwen2.5-coder:3b 42 | completion_endpoint: http://localhost:11434/api/generate 43 | #completion_model_system_prompt: REPLACE_ME 44 | completion_prompt: "<|fim_prefix|>@before<|fim_suffix|>@after<|fim_middle|>" # @input, @section, @before, @after 45 | completion_params: 46 | seed: 42 47 | temperature: 0.0 48 | top_p: 1.0 49 | top_k: 0 50 | num_predict: 15 51 | completion_stops: 52 | - "\n" 53 | - "<|fim_middle|>" 54 | chat_model: qwen2.5-coder:3b 55 | #chat_system_prompt: REPLACE_ME 56 | chat_endpoint: http://localhost:11434/api/chat 57 | #chat_stops: 58 | # - "\n" 59 | #chat_params: 60 | # temperature: 0.0 61 | quick_action_model: qwen2.5-coder:7b 62 | quick_action_endpoint: http://localhost:11434/api/generate 63 | #quick_action_system_prompt: REPLACE_ME 64 | #quick_action_stops: 65 | # - "\n" 66 | #quick_action_params: 67 | # temperature: 0.0 68 | BURP: 69 | enabled: true 70 | #chat_system_prompt: REPLACE_ME 71 | chat_params: 72 | temperature: 0.0 73 | #quick_action_system_prompt: REPLACE_ME 74 | quick_action_params: 75 | temperature: 0.0 76 | 77 | defaults: 78 | chat_provider: OLLAMA # OLLAMA, OPENAI, GEMINI, BURP 79 | completion_provider: OLLAMA # OLLAMA, OPENAI, GEMINI, BURP 80 | quick_action_provider: OLLAMA # OLLAMA, OPENAI, GEMINI, BURP 81 | timeout_sec: 60 82 | 83 | prompts: 84 | - name: technologies 85 | description: "Fingerprint web technologies" 86 | content: | 87 | Analyze the following HTTP response and identify the web technologies used. 88 | List your reasoning for each technology detected. 89 | 90 | quick_actions: 91 | - name: multipart 92 | description: "Transform request to multipart" 93 | content: | 94 | Transform the following HTTP POST request into a multipart/form-data request: 95 | - name: json 96 | description: "Transform request to json" 97 | content: | 98 | Transform the following HTTP POST request into a JSON request: 99 | - name: headers_name 100 | description: "Extract HTTP header names" 101 | content: | 102 | From the HTTP request below, extract only the unique header names. List each name on a separate line. Do not include header values. 103 | -------------------------------------------------------------------------------- /src/main/java/com/hopla/ai/BurpProvider.java: -------------------------------------------------------------------------------- 1 | package com.hopla.ai; 2 | 3 | import burp.api.montoya.ai.chat.Message; 4 | import burp.api.montoya.ai.chat.PromptOptions; 5 | import burp.api.montoya.ai.chat.PromptResponse; 6 | import com.hopla.Completer; 7 | import com.hopla.Constants; 8 | import com.hopla.HopLa; 9 | import com.hopla.Utils; 10 | 11 | import java.io.IOException; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | import static com.hopla.Constants.DEBUG_AI; 16 | 17 | public class BurpProvider extends AIProvider { 18 | 19 | public BurpProvider(LLMConfig config, LLMConfig.Provider providerConfig) { 20 | super(AIProviderType.BURP, AIProviderType.BURP.toString(), config, providerConfig); 21 | } 22 | 23 | @Override 24 | public void instruct(String prompt, StreamingCallback callback) throws IOException { 25 | if (!HopLa.montoyaApi.ai().isEnabled() && !Constants.EXTERNAL_AI) { 26 | Utils.alert(Constants.ERROR_BURP_AI_DISABLED); 27 | return; 28 | } 29 | 30 | List messages = new ArrayList<>(); 31 | PromptOptions options = PromptOptions.promptOptions(); 32 | 33 | if (!providerConfig.quick_action_system_prompt.isEmpty()) { 34 | messages.add(Message.systemMessage(providerConfig.quick_action_system_prompt)); 35 | } 36 | 37 | if (!providerConfig.quick_action_params.isEmpty() && providerConfig.quick_action_params.containsKey("temperature")) { 38 | options = options.withTemperature((double) providerConfig.quick_action_params.get("temperature")); 39 | } 40 | messages.add(Message.userMessage(prompt)); 41 | 42 | sendRequest(messages, options, callback); 43 | } 44 | 45 | @Override 46 | public List autoCompletion(Completer.CaretContext caretContext) throws IOException { 47 | return new ArrayList<>(); 48 | } 49 | 50 | @Override 51 | public void chat(AIChats.Chat chat, StreamingCallback callback) { 52 | if (!HopLa.montoyaApi.ai().isEnabled() && !Constants.EXTERNAL_AI) { 53 | Utils.alert(Constants.ERROR_BURP_AI_DISABLED); 54 | return; 55 | } 56 | 57 | List messages = new ArrayList<>(); 58 | 59 | PromptOptions options = PromptOptions.promptOptions(); 60 | 61 | if (!providerConfig.chat_system_prompt.isEmpty()) { 62 | messages.add(Message.systemMessage(providerConfig.chat_system_prompt)); 63 | } 64 | 65 | if (!providerConfig.chat_params.isEmpty() && providerConfig.chat_params.containsKey("temperature")) { 66 | options = options.withTemperature((double) providerConfig.chat_params.get("temperature")); 67 | } 68 | 69 | 70 | for (AIChats.Message message : chat.getMessages().subList(0, chat.getMessages().size() - 1)) { 71 | 72 | if (message.role == AIChats.MessageRole.USER) { 73 | messages.add(Message.userMessage(message.getContent())); 74 | } 75 | if (message.role == AIChats.MessageRole.ASSISTANT) { 76 | messages.add(Message.assistantMessage(message.getContent())); 77 | } 78 | if (message.role == AIChats.MessageRole.SYSTEM) { 79 | messages.add(Message.systemMessage(message.getContent())); 80 | } 81 | } 82 | sendRequest(messages, options, callback); 83 | } 84 | 85 | private void sendRequest(List messages, PromptOptions options, StreamingCallback callback) { 86 | new Thread(() -> { 87 | try { 88 | PromptResponse response = HopLa.montoyaApi.ai().prompt().execute(options, messages.toArray(new Message[0])); 89 | if (DEBUG_AI) { 90 | HopLa.montoyaApi.logging().logToOutput("AI chat streaming response: " + response.content()); 91 | } 92 | callback.onData(response.content()); 93 | callback.onDone(); 94 | } catch (Exception e) { 95 | callback.onError(e.getMessage()); 96 | } 97 | 98 | }).start(); 99 | } 100 | } -------------------------------------------------------------------------------- /src/main/java/com/hopla/PayloadDefinition.java: -------------------------------------------------------------------------------- 1 | package com.hopla; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.util.ArrayList; 6 | import java.util.HashSet; 7 | import java.util.List; 8 | import java.util.Set; 9 | import java.util.function.Consumer; 10 | 11 | public class PayloadDefinition { 12 | public List categories; 13 | public List keywords; 14 | public String shortcut_search_and_replace; 15 | public String shortcut_add_custom_keyword; 16 | public String shortcut_payload_menu; 17 | public String shortcut_collaborator; 18 | 19 | public Boolean isEmpty() { 20 | return this.categories == null || this.categories.isEmpty(); 21 | } 22 | 23 | public List buildMenu(Consumer actionHandler) { 24 | List items = new ArrayList<>(); 25 | 26 | if (categories != null) { 27 | for (Category cat : categories) { 28 | items.add(buildCategoryMenu(cat, actionHandler)); 29 | } 30 | } 31 | return items; 32 | } 33 | 34 | private JMenuItem buildCategoryMenu(Category category, Consumer actionHandler) { 35 | JMenu menu = new JMenu(category.name); 36 | menu.setAlignmentX(Component.LEFT_ALIGNMENT); 37 | MenuScroller.setScrollerFor(menu, 30); 38 | 39 | if (category.categories != null) { 40 | for (Category subcat : category.categories) { 41 | menu.add(buildCategoryMenu(subcat, actionHandler)); 42 | } 43 | } 44 | 45 | if (category.payloads != null) { 46 | for (Payload payload : category.payloads) { 47 | String itemName; 48 | 49 | if (payload.name != null && !payload.name.isEmpty()) { 50 | itemName = payload.name + ": " + payload.value; 51 | } else { 52 | itemName = payload.value; 53 | } 54 | 55 | if (itemName.length() > 80) { 56 | itemName = itemName.substring(0, 77) + "..."; 57 | } 58 | 59 | JMenuItem item = new JMenuItem(itemName); 60 | item.addActionListener(e -> actionHandler.accept(payload)); 61 | menu.add(item); 62 | } 63 | } 64 | return menu; 65 | } 66 | 67 | public Set flattenPayloadValues() { 68 | Set values = new HashSet<>(); 69 | if (categories == null) return values; 70 | 71 | for (PayloadDefinition.Category category : categories) { 72 | collectValues(category, values); 73 | } 74 | return values; 75 | } 76 | 77 | public Set flattenKeywordsValues() { 78 | Set values = new HashSet<>(); 79 | for (PayloadDefinition.KeywordCategory category : keywords) { 80 | values.addAll(category.values); 81 | } 82 | return values; 83 | } 84 | 85 | private void collectValues(PayloadDefinition.Category category, Set collector) { 86 | if (category.payloads != null) { 87 | for (PayloadDefinition.Payload payload : category.payloads) { 88 | if (payload != null && payload.value != null) { 89 | collector.add(payload.value); 90 | } 91 | } 92 | } 93 | 94 | if (category.categories != null) { 95 | for (PayloadDefinition.Category sub : category.categories) { 96 | collectValues(sub, collector); 97 | } 98 | } 99 | } 100 | 101 | public static class Category { 102 | public String name; 103 | public List payloads; 104 | public List categories; 105 | 106 | public boolean isEmpty() { 107 | boolean noPayloads = (payloads == null || payloads.isEmpty()); 108 | boolean noSubs = (categories == null || categories.isEmpty()); 109 | return noPayloads && noSubs; 110 | } 111 | } 112 | 113 | public static class Payload { 114 | public String name; 115 | public String value; 116 | public String shortcut; // could be null 117 | } 118 | 119 | public static class KeywordCategory { 120 | public String name; 121 | public List values; 122 | } 123 | } -------------------------------------------------------------------------------- /src/main/java/com/hopla/ai/AIChats.java: -------------------------------------------------------------------------------- 1 | package com.hopla.ai; 2 | 3 | 4 | import com.hopla.HopLa; 5 | import org.yaml.snakeyaml.LoaderOptions; 6 | import org.yaml.snakeyaml.Yaml; 7 | import org.yaml.snakeyaml.constructor.Constructor; 8 | import org.yaml.snakeyaml.inspector.TagInspector; 9 | 10 | import java.time.LocalDateTime; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | import static com.hopla.Constants.PREFERENCE_AI_CHATS; 15 | import static com.hopla.Utils.alert; 16 | 17 | 18 | public class AIChats { 19 | private final Yaml yaml; 20 | public Chats chats = new Chats(); 21 | 22 | public AIChats() { 23 | var loaderoptions = new LoaderOptions(); 24 | TagInspector taginspector = 25 | tag -> tag.getClassName().equals(Chats.class.getName()); 26 | loaderoptions.setTagInspector(taginspector); 27 | this.yaml = new Yaml(new Constructor(Chats.class, loaderoptions)); 28 | load(); 29 | } 30 | 31 | public List getChats() { 32 | return chats.chats; 33 | } 34 | 35 | public void load() { 36 | String config = HopLa.montoyaApi.persistence().preferences().getString(PREFERENCE_AI_CHATS); 37 | if (config == null || config.isEmpty()) { 38 | return; 39 | } 40 | try { 41 | this.chats = yaml.load(config); 42 | if (this.chats == null) { 43 | chats = new Chats(); 44 | } 45 | HopLa.montoyaApi.logging().logToOutput("AI Chats loaded"); 46 | } catch (Exception e) { 47 | HopLa.montoyaApi.logging().logToError("Failed to load AI chats: " + e.getMessage() + "\nAll chats reset"); 48 | alert("Failed to load AI chats: " + e.getMessage() + "\nAll chats reset"); 49 | chats = new Chats(); 50 | save(); 51 | } 52 | } 53 | 54 | public void save() { 55 | String output = yaml.dump(chats); 56 | HopLa.montoyaApi.persistence().preferences().setString(PREFERENCE_AI_CHATS, output); 57 | } 58 | 59 | public enum MessageRole { 60 | USER, 61 | ASSISTANT, 62 | SYSTEM 63 | } 64 | 65 | public static class Chats { 66 | public List chats; 67 | 68 | public Chats() { 69 | this.chats = new ArrayList<>(); 70 | } 71 | } 72 | 73 | public static class Chat { 74 | public String timestamp; 75 | public List messages; 76 | 77 | public Chat(String timestamp, List messages) { 78 | this.timestamp = timestamp; 79 | this.messages = messages; 80 | } 81 | 82 | public Chat() { 83 | this.messages = new ArrayList<>(); 84 | this.timestamp = LocalDateTime.now().toString(); 85 | } 86 | 87 | public void addMessage(Message message) { 88 | messages.add(message); 89 | } 90 | 91 | 92 | public List getMessages() { 93 | return messages; 94 | } 95 | 96 | public Message getLastMessage() { 97 | if (messages.isEmpty()) return null; 98 | return messages.getLast(); 99 | } 100 | 101 | public Message getLastUserMessage() { 102 | if (messages.isEmpty() || messages.size() < 2) return null; 103 | return messages.get(messages.size() - 2); 104 | } 105 | 106 | @Override 107 | public String toString() { 108 | StringBuilder sb = new StringBuilder(); 109 | for (Message msg : messages) { 110 | sb.append(msg).append("\n"); 111 | } 112 | return sb.toString(); 113 | } 114 | } 115 | 116 | public static class Message { 117 | public MessageRole role; 118 | public String content; 119 | 120 | public Message() { 121 | } 122 | 123 | public Message(MessageRole role, String content) { 124 | this.role = role; 125 | this.content = content; 126 | } 127 | 128 | public MessageRole getRole() { 129 | return role; 130 | } 131 | 132 | public String getContent() { 133 | return content; 134 | } 135 | 136 | public void appendContent(String content) { 137 | this.content += content; 138 | } 139 | 140 | @Override 141 | public String toString() { 142 | return "**" + role + ":** " + content + "\n"; 143 | 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/com/hopla/Utils.java: -------------------------------------------------------------------------------- 1 | package com.hopla; 2 | 3 | import burp.api.montoya.MontoyaApi; 4 | import burp.api.montoya.ui.contextmenu.MessageEditorHttpRequestResponse; 5 | import com.google.gson.Gson; 6 | import com.google.gson.JsonElement; 7 | import com.google.gson.JsonObject; 8 | 9 | import javax.swing.*; 10 | import javax.swing.text.BadLocationException; 11 | import javax.swing.text.Document; 12 | import java.awt.*; 13 | import java.awt.event.InputEvent; 14 | import java.util.Map; 15 | 16 | import static com.hopla.Constants.ERROR_TITLE; 17 | import static com.hopla.Constants.EXTENSION_NAME; 18 | 19 | public final class Utils { 20 | private Utils() { 21 | } 22 | 23 | public static void alert(String message) { 24 | JOptionPane.showMessageDialog(null, message, ERROR_TITLE, JOptionPane.ERROR_MESSAGE); 25 | } 26 | 27 | public static void success(String message) { 28 | JOptionPane.showMessageDialog(null, message, EXTENSION_NAME, JOptionPane.INFORMATION_MESSAGE); 29 | } 30 | 31 | public static void insertPayload(MessageEditorHttpRequestResponse messageEditor, String payload, InputEvent event) { 32 | Component source = (Component) event.getSource(); 33 | if (!(source instanceof JTextArea textArea)) { 34 | HopLa.montoyaApi.logging().logToError("Invalid component: " + source.getClass().getName()); 35 | return; 36 | } 37 | 38 | int caretPosition = textArea.getCaretPosition(); 39 | int requestLength = textArea.getText().length(); 40 | 41 | int start = Math.min(caretPosition, requestLength); 42 | int end = Math.min(caretPosition, requestLength); 43 | 44 | if (messageEditor.selectionOffsets().isPresent()) { 45 | var selection = messageEditor.selectionOffsets().get(); 46 | start = selection.startIndexInclusive(); 47 | end = selection.endIndexExclusive(); 48 | caretPosition = start; 49 | } 50 | 51 | try { 52 | Document doc = textArea.getDocument(); 53 | doc.remove(start, end - start); 54 | doc.insertString(start, payload, null); 55 | textArea.setCaretPosition(caretPosition + payload.length()); 56 | } catch (BadLocationException e) { 57 | HopLa.montoyaApi.logging().logToError("Inserting payload: " + e.getMessage()); 58 | } 59 | } 60 | 61 | public static String getRequest(MessageEditorHttpRequestResponse messageEditor) { 62 | return messageEditor.requestResponse().request().toString(); 63 | } 64 | 65 | public static String getResponse(MessageEditorHttpRequestResponse messageEditor) { 66 | if (messageEditor.requestResponse().hasResponse()) { 67 | return messageEditor.requestResponse().response().toString(); 68 | } 69 | return ""; 70 | } 71 | 72 | public static boolean isYamlFile(String path) { 73 | return path.toLowerCase().endsWith(".yaml") || path.toLowerCase().endsWith(".yml"); 74 | } 75 | 76 | 77 | public static String getSelectedText(MessageEditorHttpRequestResponse messageEditor) { 78 | String input = ""; 79 | if (messageEditor.selectionOffsets().isPresent()) { 80 | var selection = messageEditor.selectionOffsets().get(); 81 | int start = selection.startIndexInclusive(); 82 | int end = selection.endIndexExclusive(); 83 | String data = ""; 84 | if (messageEditor.selectionContext() == MessageEditorHttpRequestResponse.SelectionContext.REQUEST) { 85 | data = messageEditor.requestResponse().request().toString(); 86 | } else { 87 | data = messageEditor.requestResponse().response().toString(); 88 | } 89 | input = data.substring(start, end); 90 | } 91 | return input; 92 | } 93 | 94 | public static String normalizeShortcut(String shortcut) { 95 | if (shortcut == null || shortcut.isBlank()) { 96 | return null; 97 | } 98 | 99 | String s = shortcut.toLowerCase().trim(); 100 | 101 | s = s.replaceAll("\\s+", " "); 102 | 103 | s = s.replace("ctrl", "Ctrl"); 104 | s = s.replace("control", "Ctrl"); 105 | s = s.replace("command", "Meta"); 106 | s = s.replace("alt", "Alt"); 107 | 108 | String[] parts = s.split("\\+"); 109 | 110 | if (parts.length == 0) { 111 | return null; 112 | } 113 | parts[parts.length - 1] = parts[parts.length - 1].toUpperCase(); 114 | return String.join("+", parts); 115 | } 116 | 117 | public static Boolean isBurpPro(MontoyaApi api) { 118 | return api.burpSuite().version().edition() == burp.api.montoya.core.BurpSuiteEdition.PROFESSIONAL; 119 | } 120 | 121 | public static void InsertCollaboratorPayload(MontoyaApi api, MessageEditorHttpRequestResponse messageEditor, InputEvent event) { 122 | if (!Utils.isBurpPro(api)) { 123 | return; 124 | } 125 | String value = api.collaborator().defaultPayloadGenerator().generatePayload().toString(); 126 | Utils.insertPayload(messageEditor, value, event); 127 | } 128 | 129 | public static JFrame generateJFrame() { 130 | JFrame frame = new JFrame(); 131 | frame.getRootPane().putClientProperty("windowTitle", ""); 132 | frame.setName(""); 133 | frame.setLocationRelativeTo(null); 134 | frame.setAutoRequestFocus(true); 135 | frame.setFocusableWindowState(true); 136 | frame.setAlwaysOnTop(true); 137 | return frame; 138 | } 139 | 140 | public static JWindow generateJWindow() { 141 | JWindow frame = new JWindow(); 142 | frame.getRootPane().putClientProperty("windowTitle", ""); 143 | frame.setName(""); 144 | frame.setLocationRelativeTo(null); 145 | frame.setAutoRequestFocus(false); 146 | frame.setAlwaysOnTop(true); 147 | return frame; 148 | } 149 | 150 | public static JsonObject mapToJson(Map map) { 151 | Gson gson = new Gson(); 152 | JsonElement element = gson.toJsonTree(map); 153 | return element.getAsJsonObject(); 154 | } 155 | } 156 | 157 | -------------------------------------------------------------------------------- /src/main/java/com/hopla/ai/AIProvider.java: -------------------------------------------------------------------------------- 1 | package com.hopla.ai; 2 | 3 | 4 | import com.google.gson.Gson; 5 | import com.hopla.Completer; 6 | import com.hopla.HopLa; 7 | import okhttp3.*; 8 | 9 | import java.io.IOException; 10 | import java.net.InetSocketAddress; 11 | import java.net.Proxy; 12 | import java.util.List; 13 | import java.util.concurrent.TimeUnit; 14 | import java.util.concurrent.atomic.AtomicLong; 15 | 16 | import static com.hopla.Constants.DEBUG_AI; 17 | import static com.hopla.Constants.EXTERNAL_AI; 18 | 19 | public abstract class AIProvider { 20 | protected static Gson gson = new Gson(); 21 | private final AtomicLong lastInvocationTime = new AtomicLong(0); 22 | public AIProviderType type; 23 | protected String providerName; 24 | protected LLMConfig config; 25 | protected LLMConfig.Provider providerConfig; 26 | protected OkHttpClient client; 27 | protected MediaType JSON = MediaType.parse("application/json; charset=utf-8"); 28 | protected Call currentChatcall; 29 | protected Call currentQuickActionCall; 30 | protected Call currentCompletionCall; 31 | 32 | public AIProvider(AIProviderType type, String name, LLMConfig config, LLMConfig.Provider providerConfig) { 33 | this.providerName = name; 34 | this.type = type; 35 | this.config = config; 36 | this.providerConfig = providerConfig; 37 | if (EXTERNAL_AI) { 38 | this.buildClient(); 39 | } 40 | } 41 | 42 | private void buildClient() { 43 | OkHttpClient.Builder builder = new OkHttpClient.Builder(); 44 | builder.connectTimeout(this.config.defaults.timeout_sec, TimeUnit.SECONDS) 45 | .readTimeout(this.config.defaults.timeout_sec, TimeUnit.SECONDS) 46 | .writeTimeout(this.config.defaults.timeout_sec, TimeUnit.SECONDS); 47 | 48 | if (providerConfig.proxy.type != Proxy.Type.DIRECT && providerConfig.proxy.enabled) { 49 | builder.proxy(new Proxy(providerConfig.proxy.type, new InetSocketAddress(providerConfig.proxy.host, providerConfig.proxy.port))); 50 | if (providerConfig.proxy.username != null && providerConfig.proxy.password != null) { 51 | builder.authenticator(new Authenticator() { 52 | @Override 53 | public Request authenticate(Route route, Response response) throws IOException { 54 | String credential = Credentials.basic(providerConfig.proxy.username, providerConfig.proxy.password); 55 | return response.request().newBuilder() 56 | .header("Proxy-Authorization", credential) 57 | .build(); 58 | } 59 | }); 60 | } 61 | HopLa.montoyaApi.logging().logToOutput("Proxy configuration: " + providerConfig.proxy); 62 | 63 | } 64 | 65 | 66 | client = builder.build(); 67 | } 68 | 69 | public String promptReplace(Completer.CaretContext caretContext, String prompt) { 70 | return prompt.replace("@input", caretContext.lineUpToCaret) 71 | .replace("@section", caretContext.section.toString()) 72 | .replace("@before", caretContext.textBeforeCaret) 73 | .replace("@after", caretContext.textAfterCaret); 74 | } 75 | 76 | public void testCompletionConfiguration() { 77 | try { 78 | 79 | this.autoCompletion(new Completer.CaretContext(Completer.HttpSection.UNKNOWN, "", "", "")); 80 | HopLa.montoyaApi.logging().logToOutput("Completion setup OK !"); 81 | } catch (Exception e) { 82 | HopLa.montoyaApi.logging().logToError("AI Invalid completion configuration: " + e.getMessage()); 83 | } 84 | 85 | } 86 | 87 | public void testInstructConfiguration() { 88 | try { 89 | this.instruct("hi", new StreamingCallback() { 90 | @Override 91 | public void onData(String chunk) { 92 | 93 | } 94 | 95 | @Override 96 | public void onDone() { 97 | 98 | } 99 | 100 | @Override 101 | public void onError(String error) { 102 | 103 | } 104 | }); 105 | HopLa.montoyaApi.logging().logToOutput("QuickAction setup OK !"); 106 | } catch (Exception e) { 107 | HopLa.montoyaApi.logging().logToError("AI Invalid quick action configuration: " + e.getMessage()); 108 | } 109 | } 110 | 111 | public void testChatConfiguration() { 112 | try { 113 | this.chat(new AIChats.Chat(), new StreamingCallback() { 114 | @Override 115 | public void onData(String chunk) { 116 | 117 | } 118 | 119 | @Override 120 | public void onDone() { 121 | 122 | } 123 | 124 | @Override 125 | public void onError(String error) { 126 | 127 | } 128 | }); 129 | HopLa.montoyaApi.logging().logToOutput("Chat setup OK !"); 130 | } catch (Exception e) { 131 | HopLa.montoyaApi.logging().logToError("AI Invalid chat configuration: " + e.getMessage()); 132 | } 133 | 134 | } 135 | 136 | public void cancelCurrentChatRequest() { 137 | if (currentChatcall != null && !currentChatcall.isCanceled()) { 138 | if (DEBUG_AI) { 139 | HopLa.montoyaApi.logging().logToOutput("Canceling Chat Request"); 140 | } 141 | currentChatcall.cancel(); 142 | } 143 | } 144 | 145 | public void cancelCurrentQuickActionRequest() { 146 | if (currentQuickActionCall != null && !currentQuickActionCall.isCanceled()) { 147 | if (DEBUG_AI) { 148 | HopLa.montoyaApi.logging().logToOutput("Canceling Instruct Request"); 149 | } 150 | currentQuickActionCall.cancel(); 151 | } 152 | } 153 | 154 | 155 | public abstract List autoCompletion(Completer.CaretContext caretContext) throws IOException; 156 | 157 | public abstract void instruct(String prompt, StreamingCallback callback) throws IOException; 158 | 159 | public abstract void chat(AIChats.Chat chat, StreamingCallback callback) throws IOException; 160 | 161 | public interface StreamingCallback { 162 | void onData(String chunk); 163 | 164 | void onDone(); 165 | 166 | void onError(String error); 167 | } 168 | } -------------------------------------------------------------------------------- /src/main/java/com/hopla/ai/OpenAIProvider.java: -------------------------------------------------------------------------------- 1 | package com.hopla.ai; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonObject; 5 | import com.hopla.Completer; 6 | import com.hopla.HopLa; 7 | import okhttp3.Call; 8 | import okhttp3.Request; 9 | import okhttp3.RequestBody; 10 | import okhttp3.Response; 11 | 12 | import java.io.BufferedReader; 13 | import java.io.IOException; 14 | import java.io.InputStreamReader; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Objects; 19 | 20 | import static com.hopla.Constants.DEBUG_AI; 21 | 22 | public class OpenAIProvider extends AIProvider { 23 | public OpenAIProvider(LLMConfig config, LLMConfig.Provider providerConfig) { 24 | super(AIProviderType.OPENAI, AIProviderType.OPENAI.toString(), config, providerConfig); 25 | } 26 | 27 | 28 | // No fim on openai 29 | @Override 30 | public List autoCompletion(Completer.CaretContext caretContext) throws IOException { 31 | return new ArrayList<>(); 32 | } 33 | 34 | @Override 35 | public void instruct(String prompt, StreamingCallback callback) throws IOException { 36 | 37 | if (providerConfig.quick_action_model == null || providerConfig.quick_action_model.isEmpty()) { 38 | throw new IOException("OpenAI model undefined"); 39 | } 40 | 41 | JsonArray messages = new JsonArray(); 42 | 43 | if (!providerConfig.quick_action_system_prompt.isEmpty()) { 44 | JsonObject userMessage = new JsonObject(); 45 | userMessage.addProperty("role", AIChats.MessageRole.SYSTEM.toString()); 46 | userMessage.addProperty("content", providerConfig.chat_system_prompt); 47 | messages.add(userMessage); 48 | } 49 | 50 | JsonObject userMessage = new JsonObject(); 51 | userMessage.addProperty("role", AIChats.MessageRole.USER.toString()); 52 | userMessage.addProperty("content", prompt); 53 | messages.add(userMessage); 54 | 55 | JsonObject jsonPayload = new JsonObject(); 56 | jsonPayload.addProperty("model", providerConfig.quick_action_model); 57 | jsonPayload.add("messages", messages); 58 | jsonPayload.addProperty("stream", true); 59 | 60 | String jsonString = gson.toJson(jsonPayload); 61 | RequestBody body = RequestBody.create(jsonString, JSON); 62 | Request.Builder builder = new Request.Builder().url(providerConfig.quick_action_endpoint); 63 | 64 | for (Map.Entry entry : providerConfig.headers.entrySet()) { 65 | builder.addHeader(entry.getKey(), String.valueOf(entry.getValue())); 66 | } 67 | 68 | Request request = builder.post(body).build(); 69 | 70 | currentChatcall = client.newCall(request); 71 | 72 | sendStreamingRequest(currentQuickActionCall, callback); 73 | } 74 | 75 | @Override 76 | public void chat(AIChats.Chat chat, StreamingCallback callback) throws IOException { 77 | JsonArray messages = new JsonArray(); 78 | 79 | if (!providerConfig.chat_system_prompt.isEmpty()) { 80 | JsonObject userMessage = new JsonObject(); 81 | userMessage.addProperty("role", AIChats.MessageRole.SYSTEM.toString()); 82 | userMessage.addProperty("content", providerConfig.chat_system_prompt); 83 | messages.add(userMessage); 84 | } 85 | 86 | for (AIChats.Message message : chat.getMessages()) { 87 | JsonObject userMessage = new JsonObject(); 88 | userMessage.addProperty("role", message.getRole().toString().toLowerCase()); 89 | userMessage.addProperty("content", message.getContent()); 90 | messages.add(userMessage); 91 | } 92 | 93 | JsonObject jsonPayload = new JsonObject(); 94 | jsonPayload.addProperty("model", providerConfig.chat_model); 95 | jsonPayload.add("messages", messages); 96 | jsonPayload.addProperty("stream", true); 97 | 98 | String jsonString = gson.toJson(jsonPayload); 99 | RequestBody body = RequestBody.create(jsonString, JSON); 100 | 101 | Request.Builder builder = new Request.Builder().url(providerConfig.chat_endpoint); 102 | 103 | if (DEBUG_AI) { 104 | HopLa.montoyaApi.logging().logToOutput("AI chat request: " + jsonString); 105 | } 106 | 107 | for (Map.Entry entry : providerConfig.headers.entrySet()) { 108 | builder.addHeader(entry.getKey(), String.valueOf(entry.getValue())); 109 | } 110 | 111 | Request request = builder.post(body).build(); 112 | 113 | currentChatcall = client.newCall(request); 114 | 115 | sendStreamingRequest(currentChatcall, callback); 116 | 117 | } 118 | 119 | private void sendStreamingRequest(Call call, StreamingCallback callback) { 120 | new Thread(() -> { 121 | try (Response response = call.execute()) { 122 | if (!response.isSuccessful()) { 123 | callback.onError("AI API error : " + response.code() + "\n" + Objects.requireNonNull(response.body()).string()); 124 | return; 125 | } 126 | 127 | BufferedReader reader = new BufferedReader(new InputStreamReader(response.body().byteStream())); 128 | String line; 129 | while ((line = reader.readLine()) != null) { 130 | if (Thread.currentThread().isInterrupted()) break; 131 | JsonObject responseJson = gson.fromJson(line, JsonObject.class); 132 | 133 | JsonArray choices = responseJson.getAsJsonArray("choices"); 134 | 135 | if (choices != null && !choices.isEmpty()) { 136 | String content = choices.get(0).getAsJsonObject().get("delta").getAsJsonObject().get("content").getAsString(); 137 | if (DEBUG_AI) { 138 | HopLa.montoyaApi.logging().logToOutput("AI chat streaming response: " + content); 139 | } 140 | callback.onData(content); 141 | } 142 | } 143 | callback.onDone(); 144 | } catch (IOException ex) { 145 | callback.onError("Cancelled or error : " + ex.getMessage()); 146 | } catch (Exception ex) { 147 | callback.onError("AI chat error : " + ex.getMessage()); 148 | } 149 | }).start(); 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/com/hopla/ai/GeminiProvider.java: -------------------------------------------------------------------------------- 1 | package com.hopla.ai; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonObject; 5 | import com.hopla.Completer; 6 | import com.hopla.HopLa; 7 | import okhttp3.Call; 8 | import okhttp3.Request; 9 | import okhttp3.RequestBody; 10 | import okhttp3.Response; 11 | 12 | import java.io.BufferedReader; 13 | import java.io.IOException; 14 | import java.io.InputStreamReader; 15 | import java.util.*; 16 | 17 | import static com.hopla.Constants.DEBUG_AI; 18 | 19 | public class GeminiProvider extends AIProvider { 20 | 21 | public GeminiProvider(LLMConfig config, LLMConfig.Provider providerConfig) { 22 | super(AIProviderType.GEMINI, AIProviderType.GEMINI.toString(), config, providerConfig); 23 | } 24 | 25 | 26 | // No fim on gemini 27 | @Override 28 | public List autoCompletion(Completer.CaretContext caretContext) throws IOException { 29 | return new ArrayList<>(); 30 | } 31 | 32 | @Override 33 | public void instruct(String prompt, StreamingCallback callback) throws IOException { 34 | GeminiRequest geminiRequest = new GeminiRequest(new ArrayList<>()); 35 | 36 | 37 | if (!providerConfig.quick_action_system_prompt.isEmpty()) { 38 | geminiRequest.contents.add( 39 | new GeminiRequest.Content( 40 | AIChats.MessageRole.SYSTEM.toString(), 41 | Collections.singletonList(new GeminiRequest.Part(providerConfig.chat_system_prompt)) 42 | ) 43 | ); 44 | } 45 | 46 | geminiRequest.contents.add( 47 | new GeminiRequest.Content( 48 | AIChats.MessageRole.USER.toString(), 49 | Collections.singletonList(new GeminiRequest.Part(prompt)) 50 | ) 51 | ); 52 | 53 | String jsonString = gson.toJson(geminiRequest); 54 | RequestBody body = RequestBody.create(jsonString, JSON); 55 | 56 | Request.Builder builder = new Request.Builder().url(providerConfig.quick_action_endpoint.replace("@model", providerConfig.quick_action_model).replace("@key", providerConfig.api_key)); 57 | 58 | for (Map.Entry entry : providerConfig.headers.entrySet()) { 59 | builder.addHeader(entry.getKey(), String.valueOf(entry.getValue())); 60 | } 61 | 62 | Request request = builder.post(body).build(); 63 | 64 | currentChatcall = client.newCall(request); 65 | 66 | sendStreamingRequest(currentQuickActionCall, callback); 67 | } 68 | 69 | @Override 70 | public void chat(AIChats.Chat chat, StreamingCallback callback) throws IOException { 71 | 72 | GeminiRequest geminiRequest = new GeminiRequest(new ArrayList<>()); 73 | 74 | 75 | if (!providerConfig.chat_system_prompt.isEmpty()) { 76 | geminiRequest.contents.add( 77 | new GeminiRequest.Content( 78 | AIChats.MessageRole.SYSTEM.toString(), 79 | Collections.singletonList(new GeminiRequest.Part(providerConfig.chat_system_prompt)) 80 | ) 81 | ); 82 | } 83 | 84 | for (AIChats.Message message : chat.getMessages().subList(0, chat.getMessages().size() - 1)) { 85 | geminiRequest.contents.add( 86 | new GeminiRequest.Content( 87 | AIChats.MessageRole.USER.toString(), 88 | Collections.singletonList(new GeminiRequest.Part(message.getContent())) 89 | ) 90 | ); 91 | } 92 | 93 | String jsonString = gson.toJson(geminiRequest); 94 | RequestBody body = RequestBody.create(jsonString, JSON); 95 | 96 | Request.Builder builder = new Request.Builder().url(providerConfig.chat_endpoint.replace("@model", providerConfig.chat_model).replace("@key", providerConfig.api_key)); 97 | 98 | for (Map.Entry entry : providerConfig.headers.entrySet()) { 99 | builder.addHeader(entry.getKey(), String.valueOf(entry.getValue())); 100 | } 101 | 102 | Request request = builder.post(body).build(); 103 | if (DEBUG_AI) { 104 | HopLa.montoyaApi.logging().logToOutput("AI chat request: " + jsonString); 105 | } 106 | 107 | currentChatcall = client.newCall(request); 108 | 109 | sendStreamingRequest(currentChatcall, callback); 110 | 111 | } 112 | 113 | private void sendStreamingRequest(Call call, StreamingCallback callback) { 114 | new Thread(() -> { 115 | try (Response response = call.execute()) { 116 | if (!response.isSuccessful()) { 117 | callback.onError("AI API error : " + response.code() + "\n" + Objects.requireNonNull(response.body()).string()); 118 | return; 119 | } 120 | 121 | 122 | BufferedReader reader = new BufferedReader(new InputStreamReader(response.body().byteStream())); 123 | String line; 124 | while ((line = reader.readLine()) != null) { 125 | if (Thread.currentThread().isInterrupted()) break; 126 | 127 | if (!line.startsWith("data: ")) continue; 128 | 129 | String jsonLine = line.substring("data: ".length()); 130 | if (jsonLine.isBlank()) continue; 131 | 132 | JsonObject responseJson = gson.fromJson(jsonLine, JsonObject.class); 133 | JsonArray candidates = responseJson.getAsJsonArray("candidates"); 134 | if (candidates != null && !candidates.isEmpty()) { 135 | JsonObject content = candidates.get(0).getAsJsonObject().getAsJsonObject("content"); 136 | JsonArray parts = content.getAsJsonArray("parts"); 137 | if (parts != null && !parts.isEmpty()) { 138 | String text = parts.get(0).getAsJsonObject().get("text").getAsString(); 139 | if (DEBUG_AI) { 140 | HopLa.montoyaApi.logging().logToOutput("AI chat streaming response: " + text); 141 | } 142 | callback.onData(text); 143 | } 144 | } 145 | } 146 | callback.onDone(); 147 | } catch (IOException ex) { 148 | callback.onError("Cancelled or error : " + ex.getMessage()); 149 | } catch (Exception ex) { 150 | callback.onError("AI chat error : " + ex.getMessage()); 151 | } 152 | }).start(); 153 | } 154 | 155 | static class GeminiRequest { 156 | List contents; 157 | 158 | GeminiRequest(List contents) { 159 | this.contents = contents; 160 | } 161 | 162 | static class Content { 163 | String role; 164 | List parts; 165 | 166 | Content(String role, List parts) { 167 | this.role = role; 168 | this.parts = parts; 169 | } 170 | } 171 | 172 | 173 | static class Part { 174 | String text; 175 | 176 | Part(String text) { 177 | this.text = text; 178 | } 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/main/java/com/hopla/CommonMenu.java: -------------------------------------------------------------------------------- 1 | package com.hopla; 2 | 3 | import burp.api.montoya.MontoyaApi; 4 | import burp.api.montoya.http.message.HttpRequestResponse; 5 | import burp.api.montoya.ui.contextmenu.MessageEditorHttpRequestResponse; 6 | import burp.api.montoya.ui.contextmenu.MessageEditorHttpRequestResponse.SelectionContext; 7 | 8 | import javax.swing.*; 9 | import java.awt.*; 10 | import java.awt.datatransfer.Clipboard; 11 | import java.awt.datatransfer.StringSelection; 12 | import java.awt.event.InputEvent; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | import static com.hopla.Utils.alert; 17 | import static com.hopla.Utils.getSelectedText; 18 | 19 | public class CommonMenu { 20 | private final MontoyaApi api; 21 | 22 | public CommonMenu(MontoyaApi api) { 23 | this.api = api; 24 | } 25 | 26 | 27 | public List buildMenu(MessageEditorHttpRequestResponse messageEditor, InputEvent event, Runnable actionHandler) { 28 | List items = new ArrayList<>(); 29 | 30 | JMenu customKeywordsMenu = new JMenu("Custom Keywords manager"); 31 | items.add(customKeywordsMenu); 32 | 33 | JMenuItem addCustomKeywordMenu = new JMenuItem("Add keyword"); 34 | customKeywordsMenu.add(addCustomKeywordMenu); 35 | addCustomKeywordMenu.addActionListener(e -> { 36 | String input = getSelectedText(messageEditor); 37 | HopLa.localPayloadsManager.add(input); 38 | actionHandler.run(); 39 | }); 40 | 41 | JMenuItem manageCustomKeywordMenu = new JMenuItem("Manage keywords"); 42 | customKeywordsMenu.add(manageCustomKeywordMenu); 43 | manageCustomKeywordMenu.addActionListener(e -> { 44 | HopLa.localPayloadsManager.manage(); 45 | actionHandler.run(); 46 | }); 47 | 48 | JMenu copyPasteRequestResponseMenu = new JMenu("Copy HTTP Request & Response"); 49 | items.add(copyPasteRequestResponseMenu); 50 | 51 | JMenuItem copyPasteRequestResponseFullItem = new JMenuItem("Request (Full) / Response (Full)"); 52 | copyPasteRequestResponseMenu.add(copyPasteRequestResponseFullItem); 53 | copyPasteRequestResponseFullItem.addActionListener(e -> { 54 | copyRequestFullResponseFull(messageEditor); 55 | actionHandler.run(); 56 | }); 57 | 58 | JMenuItem copyPasteRequestResponseFullHeaderItem = new JMenuItem("Request (Full) / Response (Header)"); 59 | copyPasteRequestResponseMenu.add(copyPasteRequestResponseFullHeaderItem); 60 | copyPasteRequestResponseFullHeaderItem.addActionListener(e -> { 61 | copyRequestFullResponseHeader(messageEditor); 62 | actionHandler.run(); 63 | }); 64 | 65 | JMenuItem copyPasteRequestResponseFullSelectedDataItem = new JMenuItem("Request (Full) / Response (Header + Selected Data)"); 66 | copyPasteRequestResponseMenu.add(copyPasteRequestResponseFullSelectedDataItem); 67 | copyPasteRequestResponseFullSelectedDataItem.addActionListener(e -> { 68 | copyRequestFullResponseHeaderData(messageEditor); 69 | actionHandler.run(); 70 | }); 71 | 72 | JMenuItem addCollaboratorMenu = new JMenuItem("Add Collaborator payload [Pro Only]"); 73 | addCollaboratorMenu.setHorizontalTextPosition(SwingConstants.LEFT); 74 | addCollaboratorMenu.addActionListener(e -> { 75 | Utils.InsertCollaboratorPayload(api, messageEditor, event); 76 | actionHandler.run(); 77 | }); 78 | 79 | JMenuItem searchReplaceMenu = new JMenuItem("Search & replace"); 80 | searchReplaceMenu.setHorizontalTextPosition(SwingConstants.LEFT); 81 | items.add(searchReplaceMenu); 82 | searchReplaceMenu.addActionListener(e -> { 83 | String input = getSelectedText(messageEditor); 84 | HopLa.searchReplaceWindow.attach(messageEditor, event, input); 85 | actionHandler.run(); 86 | }); 87 | 88 | items.add(addCollaboratorMenu); 89 | 90 | JMenuItem askAIMenu = new JMenuItem("AI Chat"); 91 | askAIMenu.setHorizontalTextPosition(SwingConstants.LEFT); 92 | items.add(askAIMenu); 93 | askAIMenu.addActionListener(e -> { 94 | 95 | if (!this.api.ai().isEnabled() && !Constants.EXTERNAL_AI) { 96 | Utils.alert(Constants.ERROR_BURP_AI_DISABLED); 97 | return; 98 | } 99 | 100 | String input = getSelectedText(messageEditor); 101 | HopLa.aiChatPanel.show(messageEditor, event, input); 102 | actionHandler.run(); 103 | }); 104 | 105 | JMenuItem aiQuickActionMenu = new JMenuItem("AI Quick Actions"); 106 | aiQuickActionMenu.setHorizontalTextPosition(SwingConstants.LEFT); 107 | items.add(aiQuickActionMenu); 108 | aiQuickActionMenu.addActionListener(e -> { 109 | if (!this.api.ai().isEnabled() && !Constants.EXTERNAL_AI) { 110 | Utils.alert(Constants.ERROR_BURP_AI_DISABLED); 111 | return; 112 | } 113 | 114 | String input = getSelectedText(messageEditor); 115 | HopLa.aiQuickAction.show(messageEditor, event, input); 116 | actionHandler.run(); 117 | }); 118 | 119 | return items; 120 | 121 | } 122 | 123 | 124 | private void copyRequestFullResponseFull(MessageEditorHttpRequestResponse messageEditor) { 125 | HttpRequestResponse reqRes = messageEditor.requestResponse(); 126 | 127 | String output = reqRes.request().toString(); 128 | 129 | if (reqRes.hasResponse()) { 130 | output += "\n-----\n\n" + reqRes.response().toString(); 131 | } 132 | copyToClipboard(output); 133 | } 134 | 135 | private void copyRequestFullResponseHeader(MessageEditorHttpRequestResponse messageEditor) { 136 | HttpRequestResponse reqRes = messageEditor.requestResponse(); 137 | 138 | String output = reqRes.request().toString(); 139 | 140 | if (reqRes.hasResponse()) { 141 | int httpResponseBodyOffset = reqRes.response().bodyOffset(); 142 | output += "\n-----\n\n" + reqRes.response().toString().substring(0, httpResponseBodyOffset) + "[...]"; 143 | } 144 | 145 | copyToClipboard(output); 146 | } 147 | 148 | 149 | private void copyRequestFullResponseHeaderData(MessageEditorHttpRequestResponse messageEditor) { 150 | HttpRequestResponse reqRes = messageEditor.requestResponse(); 151 | String output = reqRes.request().toString(); 152 | 153 | if (reqRes.hasResponse()) { 154 | int httpResponseBodyOffset = reqRes.response().bodyOffset(); 155 | 156 | if (messageEditor.selectionOffsets().isPresent() && messageEditor.selectionContext() == SelectionContext.RESPONSE) { 157 | var selection = messageEditor.selectionOffsets().get(); 158 | int start = selection.startIndexInclusive(); 159 | int end = selection.endIndexExclusive(); 160 | String res = reqRes.response().toString(); 161 | output += "\n-----\n\n" + res.substring(0, httpResponseBodyOffset) + "[...]" + "\n"; 162 | output += res.substring(start, end) + "\n[...]"; 163 | } else { 164 | alert("No selection"); 165 | } 166 | } 167 | copyToClipboard(output); 168 | } 169 | 170 | private void copyToClipboard(String data) { 171 | StringSelection selection = new StringSelection(data); 172 | Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); 173 | clipboard.setContents(selection, null); 174 | } 175 | } 176 | 177 | -------------------------------------------------------------------------------- /src/main/java/com/hopla/MenuBar.java: -------------------------------------------------------------------------------- 1 | package com.hopla; 2 | 3 | import burp.api.montoya.MontoyaApi; 4 | import com.hopla.ai.AIConfiguration; 5 | 6 | import javax.swing.*; 7 | 8 | import static com.hopla.Constants.*; 9 | import static com.hopla.Utils.success; 10 | 11 | public class MenuBar { 12 | 13 | private final MontoyaApi api; 14 | private final PayloadManager payloadManager; 15 | private final AIConfiguration aiConfiguration; 16 | private final HopLa hopla; 17 | 18 | public MenuBar(MontoyaApi api, HopLa hopla, PayloadManager payloadManager, AIConfiguration aiConfiguration) { 19 | this.api = api; 20 | this.hopla = hopla; 21 | this.payloadManager = payloadManager; 22 | this.aiConfiguration = aiConfiguration; 23 | buildAndRegisterMenu(); 24 | } 25 | 26 | private String getPayloadsFilename() { 27 | return payloadManager.getCurrentPath().equals(DEFAULT_PAYLOAD_RESOURCE_PATH) ? "Built-in default payloads" : payloadManager.getCurrentPath(); 28 | } 29 | 30 | private void buildAndRegisterMenu() { 31 | JMenu menu = new JMenu(Constants.EXTENSION_NAME); 32 | JMenuItem pathItem = new JMenuItem("Loaded payloads: " + getPayloadsFilename()); 33 | pathItem.setEnabled(false); 34 | 35 | JMenuItem chooseItem = new JMenuItem(Constants.MENU_ITEM_CHOOSE_PAYLOAD); 36 | chooseItem.addActionListener(e -> { 37 | payloadManager.choosePayloadFile(); 38 | pathItem.setText("Loaded payloads: " + getPayloadsFilename()); 39 | reloadShortcuts(); 40 | }); 41 | 42 | JMenuItem reloadItem = new JMenuItem(Constants.MENU_ITEM_RELOAD_PAYLOADS); 43 | reloadItem.addActionListener(e -> { 44 | payloadManager.loadPayloads(); 45 | api.logging().logToOutput("Payloads file reloaded: " + payloadManager.getCurrentPath()); 46 | success(getPayloadsFilename() + " reloaded"); 47 | api.logging().logToOutput("Reloading shortcuts"); 48 | reloadShortcuts(); 49 | }); 50 | 51 | JMenuItem aiConfigurationPathItem = new JMenuItem("Loaded AI conf: " + aiConfiguration.getCurrentPath()); 52 | aiConfigurationPathItem.setEnabled(false); 53 | 54 | JMenuItem completionProviderItem = new JMenuItem("AI completion provider: " + aiConfiguration.getCompletionProviderName()); 55 | completionProviderItem.setEnabled(false); 56 | 57 | JMenuItem quickActionProviderItem = new JMenuItem("AI quick action provider: " + aiConfiguration.getQuickActionProviderName()); 58 | quickActionProviderItem.setEnabled(false); 59 | 60 | 61 | JMenuItem aiConfigurationChooseItem = new JMenuItem(Constants.MENU_ITEM_CHOOSE_AI_CONFIGURATION); 62 | aiConfigurationChooseItem.addActionListener(e -> { 63 | aiConfiguration.chooseConfigurationFile(); 64 | aiConfigurationPathItem.setText("Loaded AI conf: " + aiConfiguration.getCurrentPath()); 65 | completionProviderItem.setText("AI completion provider: " + aiConfiguration.getCompletionProviderName()); 66 | quickActionProviderItem.setText("AI quick action provider: " + aiConfiguration.getQuickActionProviderName()); 67 | reloadShortcuts(); 68 | }); 69 | 70 | JMenuItem aiConfigurationReloadItem = new JMenuItem(Constants.MENU_ITEM_RELOAD_AI_CONFIGURATION); 71 | aiConfigurationReloadItem.addActionListener(e -> { 72 | if (aiConfiguration.load()) { 73 | api.logging().logToOutput("AI configuration file reloaded: " + aiConfiguration.getCurrentPath()); 74 | success(aiConfiguration.getCurrentPath() + " reloaded"); 75 | api.logging().logToOutput("Reloading shortcuts"); 76 | reloadShortcuts(); 77 | } 78 | }); 79 | 80 | JCheckBoxMenuItem enableAutoCompletionItem = new JCheckBoxMenuItem(Constants.MENU_ITEM_AUTOCOMPLETION, hopla.autocompletionEnabled); 81 | enableAutoCompletionItem.addActionListener(e -> { 82 | if (hopla.autocompletionEnabled) { 83 | hopla.disableAutocompletion(); 84 | } else { 85 | hopla.enableAutocompletion(); 86 | } 87 | api.logging().logToOutput("Autocompletion: " + (hopla.autocompletionEnabled ? "enabled" : "disabled")); 88 | 89 | }); 90 | 91 | JCheckBoxMenuItem enableShortcutsItem = new JCheckBoxMenuItem(Constants.MENU_ITEM_SHORTCUTS, hopla.shortcutsEnabled); 92 | enableShortcutsItem.addActionListener(e -> { 93 | if (hopla.shortcutsEnabled) { 94 | hopla.disableShortcuts(); 95 | } else { 96 | hopla.enableShortcuts(); 97 | } 98 | api.logging().logToOutput("Shortcuts: " + (hopla.shortcutsEnabled ? "enabled" : "disabled")); 99 | }); 100 | 101 | JCheckBoxMenuItem enableAIItem = new JCheckBoxMenuItem(Constants.MENU_ITEM_AI_AUTOCOMPLETION, hopla.aiAutocompletionEnabled); 102 | enableAIItem.addActionListener(e -> { 103 | hopla.aiAutocompletionEnabled = !hopla.aiAutocompletionEnabled; 104 | api.persistence() 105 | .preferences() 106 | .setBoolean(PREFERENCE_AI, hopla.aiAutocompletionEnabled); 107 | api.logging().logToOutput("AI autocompletion: " + (hopla.aiAutocompletionEnabled ? "enabled" : "disabled")); 108 | }); 109 | 110 | 111 | JMenuItem exportDefaultAIConfItem = new JMenuItem(Constants.MENU_ITEM_EXPORT_DEFAULT_AI_CONF); 112 | exportDefaultAIConfItem.addActionListener(e -> { 113 | HopLa.aiConfiguration.export(); 114 | }); 115 | 116 | JMenuItem exportDefaultPayloadsItem = new JMenuItem(Constants.MENU_ITEM_EXPORT_DEFAULT_PAYLOADS); 117 | exportDefaultPayloadsItem.addActionListener(e -> { 118 | payloadManager.export(); 119 | }); 120 | 121 | 122 | JMenuItem clearPreferencesItem = new JMenuItem(Constants.MENU_ITEM_CLEAR_PREFERENCES); 123 | clearPreferencesItem.addActionListener(e -> { 124 | int confirm = JOptionPane.showConfirmDialog(menu, "Clear preferences ?", "Confirm", JOptionPane.YES_NO_OPTION); 125 | if (confirm == JOptionPane.YES_OPTION) { 126 | api.persistence().preferences().deleteBoolean(PREFERENCE_SHORTCUTS); 127 | api.persistence().preferences().deleteBoolean(PREFERENCE_AUTOCOMPLETION); 128 | api.persistence().preferences().deleteString(PREFERENCE_CUSTOM_PATH); 129 | api.persistence().preferences().deleteBoolean(PREFERENCE_AI); 130 | api.persistence().preferences().deleteString(PREFERENCE_LOCAL_DICT); 131 | api.persistence().preferences().deleteString(PREFERENCE_AI_CONFIGURATION); 132 | api.persistence().preferences().deleteString(PREFERENCE_AI_CHATS); 133 | success("Preferences cleared. Please reload the extension"); 134 | } 135 | 136 | }); 137 | if (Constants.EXTERNAL_AI) { 138 | menu.add(enableAIItem); 139 | } 140 | 141 | menu.add(exportDefaultAIConfItem); 142 | menu.add(aiConfigurationChooseItem); 143 | menu.add(aiConfigurationReloadItem); 144 | menu.add(aiConfigurationPathItem); 145 | 146 | if (Constants.EXTERNAL_AI) { 147 | menu.add(completionProviderItem); 148 | menu.add(quickActionProviderItem); 149 | } 150 | 151 | menu.add(new JSeparator()); 152 | menu.add(pathItem); 153 | menu.add(chooseItem); 154 | menu.add(reloadItem); 155 | menu.add(exportDefaultPayloadsItem); 156 | menu.add(new JSeparator()); 157 | menu.add(enableAutoCompletionItem); 158 | menu.add(enableShortcutsItem); 159 | menu.add(new JSeparator()); 160 | menu.add(clearPreferencesItem); 161 | api.userInterface().menuBar().registerMenu(menu); 162 | } 163 | 164 | private void reloadShortcuts() { 165 | hopla.disableShortcuts(); 166 | hopla.enableShortcuts(); 167 | } 168 | 169 | } -------------------------------------------------------------------------------- /src/main/java/com/hopla/LocalPayloadsManager.java: -------------------------------------------------------------------------------- 1 | package com.hopla; 2 | 3 | import burp.api.montoya.MontoyaApi; 4 | import org.yaml.snakeyaml.Yaml; 5 | 6 | import javax.swing.*; 7 | import java.awt.*; 8 | import java.util.HashMap; 9 | import java.util.HashSet; 10 | import java.util.Map; 11 | import java.util.Set; 12 | import java.util.function.Consumer; 13 | 14 | import static com.hopla.AutoCompleteMenu.CUSTOM_KEYWORD_SEPARATOR; 15 | import static com.hopla.Constants.PREFERENCE_LOCAL_DICT; 16 | 17 | public class LocalPayloadsManager { 18 | private final MontoyaApi api; 19 | private final Yaml yaml = new Yaml(); 20 | private JFrame frameAdd; 21 | private JFrame frameManage; 22 | private HashMap localPayloads = new HashMap<>(); 23 | private DefaultListModel listModel; 24 | 25 | public LocalPayloadsManager(MontoyaApi api) { 26 | this.api = api; 27 | this.loadLocalPayloads(); 28 | } 29 | 30 | public Set getPayloads() { 31 | Set concatenatedSet = new HashSet<>(); 32 | 33 | for (Map.Entry entry : localPayloads.entrySet()) { 34 | String combined = entry.getKey() + CUSTOM_KEYWORD_SEPARATOR + entry.getValue(); 35 | concatenatedSet.add(combined); 36 | concatenatedSet.add(entry.getValue()); 37 | } 38 | return concatenatedSet; 39 | } 40 | 41 | private void loadLocalPayloads() { 42 | String payloads = this.api.persistence().preferences().getString(PREFERENCE_LOCAL_DICT); 43 | if (payloads == null) { 44 | return; 45 | } 46 | localPayloads = yaml.load(payloads); 47 | } 48 | 49 | public void saveLocalPayload() { 50 | String output = yaml.dump(localPayloads); 51 | this.api.persistence().preferences().setString(PREFERENCE_LOCAL_DICT, output); 52 | } 53 | 54 | public void add(String input) { 55 | if (frameAdd != null) { 56 | frameAdd.dispose(); 57 | } 58 | frameAdd = new JFrame(); 59 | frameAdd.getRootPane().putClientProperty("windowTitle", ""); 60 | frameAdd.setName(""); 61 | frameAdd.setLocationRelativeTo(null); 62 | frameAdd.setAutoRequestFocus(true); 63 | frameAdd.setFocusableWindowState(true); 64 | frameAdd.setAlwaysOnTop(true); 65 | 66 | 67 | JTextField keyField = new JTextField(50); 68 | JTextField valueField = new JTextField(50); 69 | valueField.setText(input); 70 | JButton addButton = new JButton("Add"); 71 | addButton.setAlignmentX(Component.CENTER_ALIGNMENT); 72 | 73 | JPanel panel = new JPanel(); 74 | panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); 75 | 76 | JPanel row1 = new JPanel(new FlowLayout(FlowLayout.LEFT)); 77 | JLabel label1 = new JLabel("Key:"); 78 | label1.setPreferredSize(new Dimension(80, 20)); 79 | row1.add(label1); 80 | row1.add(keyField); 81 | 82 | JPanel row2 = new JPanel(new FlowLayout(FlowLayout.LEFT)); 83 | JLabel label2 = new JLabel("Value:"); 84 | label2.setPreferredSize(new Dimension(80, 20)); 85 | 86 | row2.add(label2); 87 | row2.add(valueField); 88 | 89 | panel.add(row1); 90 | panel.add(row2); 91 | panel.add(addButton); 92 | 93 | addButton.addActionListener(e -> { 94 | String key = keyField.getText().trim(); 95 | String value = valueField.getText().trim(); 96 | if (localPayloads.containsKey(key)) { 97 | JOptionPane.showMessageDialog(panel, "Key already exists"); 98 | return; 99 | } 100 | if (!key.isEmpty() && !value.isEmpty()) { 101 | localPayloads.put(key, value); 102 | saveLocalPayload(); 103 | 104 | JOptionPane.showMessageDialog(panel, "Added: " + key + " → " + value); 105 | keyField.setText(""); 106 | valueField.setText(""); 107 | frameAdd.dispose(); 108 | updateListModel(localPayloads, listModel); 109 | frameManage.invalidate(); 110 | frameManage.repaint(); 111 | 112 | } else { 113 | JOptionPane.showMessageDialog(panel, "Key and value required"); 114 | } 115 | }); 116 | 117 | frameAdd.getContentPane().add(panel); 118 | frameAdd.pack(); 119 | frameAdd.setVisible(true); 120 | } 121 | 122 | public void manage() { 123 | if (frameManage != null) { 124 | frameManage.dispose(); 125 | } 126 | frameManage = new JFrame(); 127 | frameManage.getRootPane().putClientProperty("windowTitle", ""); 128 | frameManage.setName(""); 129 | frameManage.setLocationRelativeTo(null); 130 | frameManage.setAutoRequestFocus(false); 131 | 132 | frameManage.setSize(400, 300); 133 | 134 | JPanel panel = new JPanel(new BorderLayout()); 135 | 136 | listModel = new DefaultListModel<>(); 137 | JList payloadList = new JList<>(listModel); 138 | JScrollPane scrollPane = new JScrollPane(payloadList); 139 | 140 | updateListModel(localPayloads, listModel); 141 | JButton addButton = new JButton("Add"); 142 | JButton editButton = new JButton("Edit"); 143 | JButton deleteButton = new JButton("Delete"); 144 | JButton deleteAllButton = new JButton("Delete all"); 145 | 146 | JPanel buttonPanel = new JPanel(); 147 | buttonPanel.add(addButton); 148 | buttonPanel.add(editButton); 149 | buttonPanel.add(deleteButton); 150 | buttonPanel.add(deleteAllButton); 151 | 152 | panel.add(scrollPane, BorderLayout.CENTER); 153 | panel.add(buttonPanel, BorderLayout.SOUTH); 154 | 155 | addButton.addActionListener(e -> { 156 | this.add(""); 157 | }); 158 | 159 | editButton.addActionListener(e -> { 160 | int selectedIndex = payloadList.getSelectedIndex(); 161 | if (selectedIndex != -1) { 162 | String selectedEntry = payloadList.getSelectedValue(); 163 | String key = selectedEntry.split(" = ")[0]; 164 | String currentValue = localPayloads.get(key); 165 | 166 | String newValue = JOptionPane.showInputDialog(panel, "New value for \"" + key + "\":", currentValue); 167 | if (newValue != null) { 168 | localPayloads.put(key, newValue); 169 | updateListModel(localPayloads, listModel); 170 | saveLocalPayload(); 171 | } 172 | } 173 | }); 174 | 175 | deleteButton.addActionListener(e -> { 176 | int selectedIndex = payloadList.getSelectedIndex(); 177 | if (selectedIndex != -1) { 178 | String selectedEntry = payloadList.getSelectedValue(); 179 | String key = selectedEntry.split(" = ")[0]; 180 | int confirm = JOptionPane.showConfirmDialog(panel, "Delete \"" + key + "\" ?", "Confirm", JOptionPane.YES_NO_OPTION); 181 | if (confirm == JOptionPane.YES_OPTION) { 182 | localPayloads.remove(key); 183 | updateListModel(localPayloads, listModel); 184 | saveLocalPayload(); 185 | } 186 | } 187 | }); 188 | 189 | deleteAllButton.addActionListener(e -> { 190 | int confirm = JOptionPane.showConfirmDialog(panel, "Delete all entries ?", "Confirm", JOptionPane.YES_NO_OPTION); 191 | if (confirm == JOptionPane.YES_OPTION) { 192 | localPayloads.clear(); 193 | updateListModel(localPayloads, listModel); 194 | saveLocalPayload(); 195 | } 196 | }); 197 | 198 | frameManage.add(panel); 199 | frameManage.setVisible(true); 200 | 201 | } 202 | 203 | public void dispose() { 204 | if (frameManage != null) { 205 | frameManage.dispose(); 206 | 207 | } 208 | if (frameAdd != null) { 209 | frameAdd.dispose(); 210 | 211 | } 212 | } 213 | 214 | private void updateListModel(HashMap map, DefaultListModel model) { 215 | model.clear(); 216 | for (Map.Entry entry : map.entrySet()) { 217 | model.addElement(entry.getKey() + " = " + entry.getValue()); 218 | } 219 | } 220 | 221 | public JMenu buildMenu(Consumer actionHandler) { 222 | 223 | JMenu menu = new JMenu("Custom keywords"); 224 | 225 | for (Map.Entry entry : localPayloads.entrySet()) { 226 | String key = entry.getKey(); 227 | String value = entry.getValue(); 228 | String itemName = key + ": " + value; 229 | 230 | if (itemName.length() > 80) { 231 | itemName = itemName.substring(0, 77) + "..."; 232 | } 233 | 234 | JMenuItem item = new JMenuItem(itemName); 235 | item.addActionListener(e -> actionHandler.accept(value)); 236 | menu.add(item); 237 | 238 | } 239 | return menu; 240 | 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/main/java/com/hopla/ai/AIConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.hopla.ai; 2 | 3 | import burp.api.montoya.MontoyaApi; 4 | import burp.api.montoya.persistence.Preferences; 5 | import com.hopla.Constants; 6 | import com.hopla.PayloadDefinition; 7 | import com.hopla.Utils; 8 | import org.yaml.snakeyaml.LoaderOptions; 9 | import org.yaml.snakeyaml.Yaml; 10 | import org.yaml.snakeyaml.constructor.Constructor; 11 | import org.yaml.snakeyaml.inspector.TagInspector; 12 | 13 | import javax.swing.*; 14 | import javax.swing.filechooser.FileNameExtensionFilter; 15 | import java.io.*; 16 | import java.nio.charset.StandardCharsets; 17 | import java.nio.file.Files; 18 | import java.nio.file.Paths; 19 | import java.util.stream.Collectors; 20 | 21 | import static com.hopla.Constants.DEFAULT_AI_CONFIGURATION_PATH; 22 | import static com.hopla.Constants.DEFAULT_BAPP_AI_CONFIGURATION_PATH; 23 | import static com.hopla.Utils.isYamlFile; 24 | 25 | public class AIConfiguration { 26 | private final MontoyaApi api; 27 | private final Preferences preferences; 28 | private final Yaml yaml; 29 | public LLMConfig config; 30 | public AIProvider defaultChatProvider; 31 | public AIProvider completionProvider; 32 | public AIProvider quickActionProvider; 33 | public boolean isAIConfigured = false; 34 | 35 | public AIConfiguration(MontoyaApi api) { 36 | this.api = api; 37 | this.preferences = api.persistence().preferences(); 38 | var loaderoptions = new LoaderOptions(); 39 | TagInspector taginspector = 40 | tag -> tag.getClassName().equals(PayloadDefinition.class.getName()); 41 | loaderoptions.setTagInspector(taginspector); 42 | 43 | yaml = new Yaml(new Constructor(LLMConfig.class, loaderoptions)); 44 | load(); 45 | } 46 | 47 | public String getCompletionProviderName() { 48 | return completionProvider != null ? completionProvider.providerName : "Not configured"; 49 | } 50 | 51 | public String getQuickActionProviderName() { 52 | return quickActionProvider != null ? quickActionProvider.providerName : "Not configured"; 53 | } 54 | 55 | public String getCurrentPath() { 56 | String path = preferences.getString(Constants.PREFERENCE_AI_CONFIGURATION); 57 | if (Constants.EXTERNAL_AI){ 58 | return (path != null && !path.isEmpty()) ? (path) : "Not configured"; 59 | }else { 60 | return (path != null && !path.isEmpty()) ? (path.equals(DEFAULT_BAPP_AI_CONFIGURATION_PATH) ? "Burp default" : path) : "Not configured"; 61 | } 62 | } 63 | 64 | public boolean load() { 65 | String savedPath = preferences.getString(Constants.PREFERENCE_AI_CONFIGURATION); 66 | 67 | if ((savedPath == null || savedPath.isEmpty()) && !Constants.EXTERNAL_AI) { 68 | savedPath = DEFAULT_BAPP_AI_CONFIGURATION_PATH; 69 | } 70 | 71 | if (savedPath != null && !savedPath.isEmpty() && !DEFAULT_AI_CONFIGURATION_PATH.equals(savedPath)) { 72 | try { 73 | config = loadFromFile(savedPath); 74 | api.logging().logToOutput("Loaded AI configuration from saved path: " + savedPath); 75 | isAIConfigured = true; 76 | return true; 77 | } catch (Exception e) { 78 | api.logging().logToError("Failed to load AI configuration from saved path: " + savedPath + ", loading default"); 79 | Utils.alert(Constants.ERROR_INVALID_FILE + e.getMessage()); 80 | return false; 81 | } 82 | } 83 | return false; 84 | } 85 | 86 | public void export() { 87 | String default_configuration_file = DEFAULT_BAPP_AI_CONFIGURATION_PATH; 88 | 89 | if (Constants.EXTERNAL_AI) { 90 | default_configuration_file = DEFAULT_AI_CONFIGURATION_PATH; 91 | } 92 | 93 | InputStream inputStream = getClass().getResourceAsStream(default_configuration_file); 94 | if (inputStream == null) { 95 | String exc = "Default AI configuration sample not found: " + default_configuration_file; 96 | api.logging().logToError(exc); 97 | Utils.alert(exc); 98 | return; 99 | } 100 | String sample = ""; 101 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { 102 | sample = reader.lines().collect(Collectors.joining("\n")); 103 | } catch (Exception e) { 104 | String exc = "Failed to read AI configuration sample: " + default_configuration_file; 105 | api.logging().logToError(exc); 106 | Utils.alert(exc); 107 | } 108 | 109 | JFileChooser fileChooser = new JFileChooser(); 110 | fileChooser.setAcceptAllFileFilterUsed(false); 111 | FileNameExtensionFilter filter = new FileNameExtensionFilter("YAML files (*.yaml, *.yml)", "yaml", "yml"); 112 | fileChooser.setFileFilter(filter); 113 | fileChooser.setDialogTitle("Choose export location"); 114 | 115 | int userSelection = fileChooser.showSaveDialog(null); 116 | 117 | if (userSelection == JFileChooser.APPROVE_OPTION) { 118 | File fileToSave = fileChooser.getSelectedFile(); 119 | if (!fileToSave.getName().toLowerCase().endsWith(".yaml") || !fileToSave.getName().toLowerCase().endsWith(".yml")) { 120 | fileToSave = new File(fileToSave.getAbsolutePath() + ".yml"); 121 | } 122 | 123 | 124 | try { 125 | Files.writeString(fileToSave.toPath(), sample); 126 | JOptionPane.showMessageDialog(null, "File saved: " + fileToSave.getAbsolutePath()); 127 | } catch (IOException e) { 128 | JOptionPane.showMessageDialog(null, "Write error: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); 129 | } 130 | } 131 | } 132 | 133 | private LLMConfig loadFromFile(String path) throws Exception { 134 | 135 | try (InputStream in = path.equals(DEFAULT_BAPP_AI_CONFIGURATION_PATH) ? getClass().getResourceAsStream(path) : Files.newInputStream(Paths.get(path))) { 136 | LLMConfig config = yaml.load(in); 137 | 138 | if (Constants.EXTERNAL_AI) { 139 | AIProviderType chatProviderType = AIProviderType.valueOf(config.defaults.chat_provider); 140 | AIProviderType completionProviderType = AIProviderType.valueOf(config.defaults.completion_provider); 141 | AIProviderType quickActionProviderType = AIProviderType.valueOf(config.defaults.quick_action_provider); 142 | defaultChatProvider = AIProviderFactory.createProvider(chatProviderType, config, config.providers.get(chatProviderType)); 143 | completionProvider = AIProviderFactory.createProvider(completionProviderType, config, config.providers.get(completionProviderType)); 144 | quickActionProvider = AIProviderFactory.createProvider(quickActionProviderType, config, config.providers.get(quickActionProviderType)); 145 | 146 | new Thread(() -> { 147 | defaultChatProvider.testChatConfiguration(); 148 | completionProvider.testCompletionConfiguration(); 149 | quickActionProvider.testInstructConfiguration(); 150 | }).start(); 151 | 152 | } else { 153 | defaultChatProvider = AIProviderFactory.createProvider(AIProviderType.BURP, config, config.providers.get(AIProviderType.BURP)); 154 | quickActionProvider = AIProviderFactory.createProvider(AIProviderType.BURP, config, config.providers.get(AIProviderType.BURP)); 155 | } 156 | 157 | return config; 158 | } 159 | } 160 | 161 | public void chooseConfigurationFile() { 162 | JFileChooser fileChooser = new JFileChooser(); 163 | fileChooser.setAcceptAllFileFilterUsed(false); 164 | FileNameExtensionFilter filter = new FileNameExtensionFilter("YAML files (*.yaml, *.yml)", "yaml", "yml"); 165 | fileChooser.setFileFilter(filter); 166 | 167 | int result = fileChooser.showOpenDialog(null); 168 | 169 | if (result == JFileChooser.APPROVE_OPTION) { 170 | File selectedFile = fileChooser.getSelectedFile(); 171 | String path = selectedFile.getAbsolutePath(); 172 | 173 | if (!isYamlFile(path)) { 174 | Utils.alert(Constants.ERROR_INVALID_FILE_EXTENSION); 175 | return; 176 | } 177 | 178 | try { 179 | loadFromFile(path); 180 | preferences.setString(Constants.PREFERENCE_AI_CONFIGURATION, path); 181 | load(); 182 | Utils.success(Constants.CONFIGURATION_FILE_LOADED); 183 | api.logging().logToOutput(Constants.CONFIGURATION_FILE_LOADED + ": " + path); 184 | } catch (Exception e) { 185 | api.logging().logToError(Constants.ERROR_INVALID_FILE + e.getMessage()); 186 | Utils.alert(Constants.ERROR_INVALID_FILE + e.getMessage()); 187 | } 188 | } 189 | } 190 | 191 | public AIProvider getChatProvider(AIProviderType providerType) { 192 | return AIProviderFactory.createProvider(providerType, config, config.providers.get(providerType)); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HopLa - Burp copilot 2 | 3 | 💥 All the power of PayloadsAllTheThings, without the overhead. 4 | 5 | This extension enhances Burp Suite with intelligent autocompletion and built-in payloads to simplify intrusion testing. 6 | It supports integration with AI providers like Ollama, OpenAI, and Gemini to offer advanced features such as chat and 7 | content generation/transformation. You can also add your own payloads to tailor it to your needs! 8 | 9 | Developed by Alexis Danizan [![Twitter Follow](https://img.shields.io/twitter/follow/alexisdanizan?style=social)](https://twitter.com/alexisdanizan/) 10 | Released as open source by [Synacktiv 🥷](https://www.synacktiv.com/) 11 | 12 | **Features**: 13 | * Integrate AI-powered autocompletion (Copilot style) 14 | * Enable AI-based chat for interaction and guidance 15 | * Use AI instructions to quickly transform HTTP requests 16 | * Copy formatted requests and responses for easy reporting 17 | * Search and replace in Repeater 18 | * Access a one-click payload insertion menu 19 | * Insert Burp Collaborator domains dynamically 20 | * Assign keyboard shortcuts to specific payloads 21 | * Add custom keywords on the fly during testing 22 | 23 | ![Demo GIF](img/demo.gif) 24 | 25 |

26 | 27 | 28 |

29 | 30 | ## Getting started 31 | 32 | ### Installation 33 | 34 | * Download the jar file from the [release directory](https://github.com/synacktiv/HopLa/releases) 35 | * Add it to Burp Suite using the Extender tab 36 | 37 | ## Usage 38 | 39 | By default, HopLa comes with a built-in set of payloads. You can extend them by loading your own custom YAML file via the top menu. 40 | (See the [default payloads file](https://github.com/synacktiv/HopLa/blob/main/src/main/resources/default-payloads.yaml) for reference.) 41 | 42 | AI providers can be configured by importing your YAML configuration (see [Configure AI providers](#configure-ai-providers)) 43 | 44 | Several keyboard shortcuts are predefined by default and can be customized through configuration files. 45 | 46 | * **`Ctrl+Q`** - Open the **Payload Library** menu 47 | * **`Ctrl+J`** - Launch the **AI Chat** 48 | * **`Ctrl+Alt+O`** - Open the **Quick Actions** menu 49 | * **`Ctrl+L`** - Open the **Search & Replace** menu 50 | * **`Ctrl+M`** - Insert a **Burp Collaborator** payload 51 | * **`Ctrl+Alt+J`** - Open the **Custom Keywords Manager** 52 | 53 | If you're using **i3**, add the following line to your `$HOME/.config/i3/config` to enable floating mode for the frame: 54 | ``` 55 | for_window [class=".*burp-StartBurp.*" title="^ $"] floating enable 56 | ``` 57 | 58 | ### Configure AI providers 59 | 60 | HopLa supports multiple AI providers (OpenAI, Gemini, Ollama), but AI-powered autocompletion is only available with Ollama. 61 | 62 | | Features | Ollama | Gemini | OpenAI | 63 | |-------------------|:------:|:------:|:------:| 64 | | Chat | ✅ Yes | ✅ Yes | ✅ Yes | 65 | | Autocompletion | ✅ Yes | ❌ No | ❌ No | 66 | | Quick Action | ✅ Yes | ✅ Yes | ✅ Yes | 67 | 68 | 69 | The YAML configuration file for AI is structured as follows (a sample file can be exported from the HopLa menu): 70 | 71 | ```yaml 72 | shortcut_ai_chat: Ctrl+J 73 | shortcut_quick_action: Ctrl+Alt+O 74 | #autocompletion_min_chars: 1 # Minimum input length for AI-powered autocompletion (default: 1) 75 | 76 | providers: 77 | OPENAI: 78 | enabled: true 79 | chat_model: gpt-4.1 80 | chat_endpoint: https://api.openai.com/v1/chat/completions 81 | #chat_model_system_prompt: REPLACE_ME 82 | quick_action_model: gpt-4.1 83 | #quick_action_system_prompt: REPLACE_ME 84 | quick_action_endpoint: https://api.openai.com/v1/chat/completions 85 | headers: 86 | Authorization: "Bearer REPLACE_ME" 87 | proxy: 88 | enabled: true 89 | host: 127.0.0.1 90 | port: 5555 91 | username: user123 92 | password: pass123 93 | type: SOCKS # SOCKS or HTTP 94 | GEMINI: 95 | enabled: true 96 | chat_model: gemini-2.0-flash 97 | chat_endpoint: https://generativelanguage.googleapis.com/v1beta/models/@model:streamGenerateContent?alt=sse&key=@key #HopLa replace @key with api_key value 98 | #chat_model_system_prompt: REPLACE_ME 99 | quick_action_endpoint: https://generativelanguage.googleapis.com/v1beta/models/@model:streamGenerateContent?alt=sse&key=@key #HopLa replace @key with api_key value 100 | #quick_action_system_prompt: REPLACE_ME 101 | api_key: REPLACE_ME 102 | proxy: 103 | enabled: true 104 | host: 127.0.0.1 105 | port: 5555 106 | username: user123 107 | password: pass123 108 | type: SOCKS # SOCKS or HTTP 109 | 110 | OLLAMA: 111 | enabled: true 112 | completion_model: qwen2.5-coder:3b 113 | completion_endpoint: http://localhost:11434/api/generate 114 | #completion_model_system_prompt: REPLACE_ME 115 | completion_prompt: "<|fim_prefix|>@before<|fim_suffix|>@after<|fim_middle|>" # @input, @section, @before, @after 116 | completion_params: 117 | seed: 42 118 | temperature: 0.0 119 | top_p: 1.0 120 | top_k: 0 121 | num_predict: 15 122 | completion_stops: 123 | - "\n" 124 | - "<|fim_middle|>" 125 | chat_model: qwen2.5-coder:3b 126 | #chat_system_prompt: REPLACE_ME 127 | chat_endpoint: http://localhost:11434/api/chat 128 | #chat_stops: 129 | # - "\n" 130 | #chat_params: 131 | # temperature: 0.0 132 | quick_action_model: qwen2.5-coder:7b 133 | quick_action_endpoint: http://localhost:11434/api/generate 134 | #quick_action_system_prompt: REPLACE_ME 135 | #quick_action_stops: 136 | # - "\n" 137 | #quick_action_params: 138 | # temperature: 0.0 139 | BURP: 140 | enabled: true 141 | #chat_system_prompt: REPLACE_ME 142 | chat_params: 143 | temperature: 0.0 144 | #quick_action_system_prompt: REPLACE_ME 145 | quick_action_params: 146 | temperature: 0.0 147 | 148 | defaults: 149 | chat_provider: OLLAMA # OLLAMA, OPENAI, GEMINI, BURP 150 | completion_provider: OLLAMA # OLLAMA, OPENAI, GEMINI, BURP 151 | quick_action_provider: OLLAMA # OLLAMA, OPENAI, GEMINI, BURP 152 | timeout_sec: 60 153 | 154 | prompts: 155 | - name: technologies 156 | description: "Fingerprint web technologies" 157 | content: | 158 | Analyze the following HTTP response and identify the web technologies used. 159 | List your reasoning for each technology detected. 160 | 161 | quick_actions: 162 | - name: multipart 163 | description: "Transform request to multipart" 164 | content: | 165 | Transform the following HTTP POST request into a multipart/form-data request: 166 | - name: json 167 | description: "Transform request to json" 168 | content: | 169 | Transform the following HTTP POST request into a JSON request: 170 | - name: headers_name 171 | description: "Extract HTTP header names" 172 | content: | 173 | From the HTTP request below, extract only the unique header names. List each name on a separate line. Do not include header values. 174 | ``` 175 | 176 | For the BApp Store version of the extension, only the Burp provider is supported. 177 | 178 | ### How to customize payloads 179 | 180 | The YAML payloads file follow the structure (there is no nesting limit): 181 | 182 | ```yaml 183 | shortcut_search_and_replace: Ctrl+L 184 | shortcut_payload_menu: Ctrl+Q 185 | shortcut_collaborator: Ctrl+M 186 | shortcut_add_custom_keyword: Ctrl+Alt+J 187 | 188 | categories: 189 | - name: "XSS" 190 | payloads: 191 | - name: "Fingerprint" 192 | value: "\">

" 193 | shortcut: Ctrl+k # Use this shortcut to insert a payload 194 | - name: "Path Traversal" 195 | categories: 196 | - name: "Simple" 197 | payloads: 198 | - name: "" 199 | value: "../" 200 | - name: "Simple 2" 201 | payloads: 202 | - name: "" 203 | value: "../" 204 | keywords: 205 | - name: "Headers" 206 | values: 207 | - "Accept" 208 | - "Accept-Charset" 209 | ``` 210 | 211 | To add only autocompletion keywords that do not appear in the menu, you can add them in the **keywords** category: 212 | 213 | ```yaml 214 | keywords: 215 | - name: "Headers" 216 | values: 217 | - "Accept" 218 | - "Accept-Charset" 219 | ``` 220 | 221 | ## Build 222 | 223 | To enable other providers, set `def externalAIEnabled = true` in `build.gradle`. 224 | 225 | Build using Docker or Podman: 226 | 227 | ```bash 228 | $ podman build -t hopla . 229 | 230 | $ podman run --rm -v "$PWD":/data hopla gradle build 231 | Starting a Gradle Daemon (subsequent builds will be faster) 232 | > Task :compileJava 233 | 234 | > Task :encryptResource 235 | Encrypting /data/src/main/resources/default-payloads.yaml to /data/build/encryptedResources/default-payloads.enc.yaml 236 | 237 | > Task :processResources 238 | > Task :classes 239 | > Task :jar 240 | > Task :assemble 241 | > Task :compileTestJava NO-SOURCE 242 | > Task :processTestResources NO-SOURCE 243 | > Task :testClasses UP-TO-DATE 244 | > Task :test NO-SOURCE 245 | > Task :check UP-TO-DATE 246 | > Task :build 247 | 248 | BUILD SUCCESSFUL in 11s 249 | 4 actionable tasks: 4 executed 250 | 251 | $ ls releases 252 | HopLa.jar 253 | ``` 254 | 255 | Execute `gradle build` and you'll have the plugin ready in `releases/HopLa.jar`. 256 | 257 | To avoid triggering antivirus alerts, the YAML payload file is encrypted at build time. 258 | 259 | ## Thanks To 260 | 261 | * https://github.com/Static-Flow/BurpSuiteAutoCompletion 262 | * https://github.com/d3vilbug/HackBar 263 | * https://github.com/swisskyrepo/PayloadsAllTheThings 264 | 265 | Thanks a lot for your awesome work ! 266 | 267 | ## License 268 | 269 | Released under BSD 3-Clause License see LICENSE for more information 270 | 271 | Please feel free to report bugs, suggest features, or send pull requests. 272 | -------------------------------------------------------------------------------- /src/main/java/com/hopla/Completer.java: -------------------------------------------------------------------------------- 1 | package com.hopla; 2 | 3 | import burp.api.montoya.MontoyaApi; 4 | 5 | import javax.swing.*; 6 | import javax.swing.event.CaretEvent; 7 | import javax.swing.event.CaretListener; 8 | import javax.swing.text.BadLocationException; 9 | import java.awt.event.*; 10 | 11 | import static com.hopla.Constants.DEBUG; 12 | 13 | public class Completer { 14 | private final JTextArea source; 15 | private final MontoyaApi api; 16 | 17 | private final AutoCompleteMenu autoCompleteMenu; 18 | private KeyListener keyListener; 19 | private CaretListener caretListener; 20 | private int caretPositionStart = 0; 21 | private boolean manual_move = false; 22 | private boolean backspace = false; 23 | private boolean escape = false; 24 | private int in_selection = 0; 25 | private FocusListener focusListener; 26 | 27 | public Completer(MontoyaApi api, JTextArea source, AutoCompleteMenu autoCompleteMenu) { 28 | this.source = source; 29 | this.api = api; 30 | this.autoCompleteMenu = autoCompleteMenu; 31 | this.addListenersDetectManualCaretMove(); 32 | 33 | } 34 | 35 | public static CaretContext getCaretContext(JTextArea source, int caretPosition) { 36 | String[] lines = source.getText().split("\n"); 37 | 38 | int charCount = 0; 39 | int caretLineIndex = 0; 40 | 41 | for (int i = 0; i < lines.length; i++) { 42 | charCount += lines[i].length() + 1; // +1 for \n 43 | if (caretPosition < charCount) { 44 | caretLineIndex = i; 45 | break; 46 | } 47 | } 48 | 49 | int bodyStartIndex = -1; 50 | for (int i = 1; i < lines.length; i++) { 51 | if (lines[i].trim().isEmpty()) { 52 | bodyStartIndex = i + 1; 53 | break; 54 | } 55 | } 56 | 57 | HttpSection section; 58 | if (caretLineIndex == 0) { 59 | section = HttpSection.REQUEST_LINE; 60 | } else if (bodyStartIndex == -1) { 61 | section = HttpSection.HEADERS; 62 | } else if (caretLineIndex < bodyStartIndex) { 63 | section = HttpSection.HEADERS; 64 | } else { 65 | section = HttpSection.BODY; 66 | } 67 | 68 | String textBeforeCaret = source.getText().substring(0, caretPosition); 69 | String textAfterCaret = source.getText().substring(caretPosition); 70 | if (textAfterCaret.isEmpty()) { 71 | textAfterCaret = "\n"; 72 | } 73 | int lastNewline = textBeforeCaret.lastIndexOf('\n'); 74 | String lineUpToCaret = textBeforeCaret.substring(lastNewline + 1); 75 | 76 | 77 | return new CaretContext(section, lineUpToCaret, textBeforeCaret, textAfterCaret); 78 | } 79 | 80 | public JTextArea getSource() { 81 | return this.source; 82 | } 83 | 84 | public boolean isPrintableChar(char c) { 85 | if (Character.isLetterOrDigit(c)) { 86 | return true; 87 | } 88 | 89 | String specialChars = "!@#$%^&*()-_=+[]{};:'\",.<>/?\\|`~"; 90 | 91 | return specialChars.contains(Character.toString(c)); 92 | } 93 | 94 | private void addListenersDetectManualCaretMove() { 95 | keyListener = new KeyAdapter() { 96 | public void keyTyped(KeyEvent e) { 97 | if (DEBUG) { 98 | api.logging().logToOutput("Input: " + e.getKeyChar()); 99 | } 100 | if (isPrintableChar(e.getKeyChar())) { 101 | manual_move = false; 102 | } 103 | } 104 | 105 | public void keyPressed(KeyEvent e) { 106 | if (e.getKeyCode() == KeyEvent.VK_BACK_SPACE) { 107 | backspace = true; 108 | return; 109 | } 110 | 111 | if (autoCompleteMenu.isVisible()) { 112 | int keyCode = e.getKeyCode(); 113 | if (DEBUG) { 114 | api.logging().logToOutput("Key caught"); 115 | } 116 | autoCompleteMenu.handleKey(keyCode); 117 | e.consume(); 118 | if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { 119 | if (DEBUG) { 120 | api.logging().logToOutput("Escape key caught"); 121 | } 122 | escape = true; 123 | } 124 | } 125 | } 126 | }; 127 | caretListener = new CaretListener() { 128 | @Override 129 | public void caretUpdate(CaretEvent e) { 130 | 131 | int pos = e.getDot(); 132 | 133 | // selection create 3 events, skip the next 2 134 | if (e.getMark() != e.getDot()) { 135 | if (DEBUG) { 136 | api.logging().logToOutput("selection " + e.getMark() + " " + pos); 137 | } 138 | 139 | // min depend on selection direction 140 | caretPositionStart = Math.min(e.getMark(), e.getDot()); 141 | in_selection = 2; 142 | } 143 | 144 | if (in_selection > 0 && backspace) { 145 | in_selection = -1; 146 | } else if (in_selection > 0) { 147 | in_selection -= 1; 148 | return; 149 | } 150 | 151 | if (DEBUG) { 152 | api.logging().logToOutput("caret manual move: " + manual_move); 153 | api.logging().logToOutput("caret start: " + caretPositionStart + " end: " + pos); 154 | } 155 | 156 | if ((manual_move || escape) && !backspace) { 157 | if (DEBUG) { 158 | api.logging().logToOutput("manual move or escape"); 159 | } 160 | 161 | caretPositionStart = pos; 162 | autoCompleteMenu.hide(); 163 | } else { 164 | if (backspace) { 165 | if (DEBUG) { 166 | api.logging().logToOutput("backspace"); 167 | } 168 | 169 | if (pos <= caretPositionStart) { 170 | caretPositionStart = pos; 171 | autoCompleteMenu.hide(); 172 | } 173 | 174 | } 175 | 176 | try { 177 | String content = source.getText(0, pos); 178 | String text = content.substring(caretPositionStart); 179 | CaretContext caretContext = getCaretContext(source, pos); 180 | if (DEBUG) { 181 | api.logging().logToOutput("complete: " + text); 182 | api.logging().logToOutput("Caret context: " + caretContext); 183 | } 184 | autoCompleteMenu.suggest(source, text, caretPositionStart, pos, caretContext); 185 | 186 | } catch (BadLocationException ex) { 187 | if (DEBUG) { 188 | api.logging().logToError("Bad location user completion input" + ex.getMessage()); 189 | } 190 | } catch (StringIndexOutOfBoundsException ex) { 191 | if (DEBUG) { 192 | api.logging().logToError("Out of bound error start: " + caretPositionStart + " end: " + source.getCaretPosition()); 193 | } 194 | } catch (Exception ex) { 195 | if (DEBUG) { 196 | api.logging().logToError("Completion input error: " + ex.getMessage()); 197 | } 198 | } 199 | } 200 | backspace = false; 201 | escape = false; 202 | manual_move = true; 203 | 204 | if (DEBUG) { 205 | api.logging().logToOutput("-----------------"); 206 | } 207 | 208 | } 209 | }; 210 | 211 | 212 | focusListener = new FocusListener() { 213 | @Override 214 | public void focusGained(FocusEvent e) { 215 | } 216 | 217 | @Override 218 | public void focusLost(FocusEvent e) { 219 | autoCompleteMenu.hide(); 220 | } 221 | }; 222 | 223 | source.addCaretListener(caretListener); 224 | source.addKeyListener(keyListener); 225 | source.addFocusListener(focusListener); 226 | } 227 | 228 | public void detach() { 229 | source.removeKeyListener(keyListener); 230 | source.removeCaretListener(caretListener); 231 | source.removeFocusListener(focusListener); 232 | } 233 | 234 | public enum HttpSection { 235 | REQUEST_LINE, 236 | HEADERS, 237 | BODY, 238 | UNKNOWN 239 | } 240 | 241 | public static class CaretContext { 242 | public final HttpSection section; 243 | public final String lineUpToCaret; 244 | public final String textBeforeCaret; 245 | public final String textAfterCaret; 246 | 247 | public CaretContext(HttpSection section, String lineUpToCaret, String textBeforeCaret, String textAfterCaret) { 248 | this.section = section; 249 | this.lineUpToCaret = lineUpToCaret; 250 | this.textBeforeCaret = textBeforeCaret; 251 | this.textAfterCaret = textAfterCaret; 252 | } 253 | 254 | @Override 255 | public String toString() { 256 | return "Section: " + section + ", Line: \"" + lineUpToCaret + "\""; 257 | } 258 | } 259 | 260 | } 261 | -------------------------------------------------------------------------------- /src/main/java/com/hopla/AutoCompleteMenu.java: -------------------------------------------------------------------------------- 1 | package com.hopla; 2 | 3 | import burp.api.montoya.MontoyaApi; 4 | import com.hopla.ai.AIConfiguration; 5 | 6 | import javax.swing.*; 7 | import javax.swing.text.BadLocationException; 8 | import javax.swing.text.Document; 9 | import javax.swing.text.JTextComponent; 10 | import java.awt.*; 11 | import java.awt.event.KeyEvent; 12 | import java.awt.event.MouseAdapter; 13 | import java.awt.event.MouseEvent; 14 | import java.util.List; 15 | import java.util.concurrent.CancellationException; 16 | import java.util.concurrent.ExecutionException; 17 | import java.util.function.Supplier; 18 | import java.util.regex.Pattern; 19 | 20 | import static com.hopla.Constants.DEBUG; 21 | import static com.hopla.Utils.alert; 22 | import static com.hopla.Utils.generateJWindow; 23 | 24 | public class AutoCompleteMenu { 25 | public static final String CUSTOM_KEYWORD_SEPARATOR = " [CUSTOM]-> "; 26 | public static final String AI_KEYWORD_SEPARATOR = " [AI] "; 27 | private static final int MAX_VISIBLE_ROWS = 10; 28 | private static final int MIN_VISIBLE_ROWS = 2; 29 | private static final int FRAME_TOP_MARGIN = 20; 30 | private static final int FRAME_WIDTH = 400; 31 | private static final int FRAME_HEIGHT = 50; 32 | private static final int SCROLL_STEP = 50; 33 | private final JWindow frame; 34 | private final JList suggestionList; 35 | private final MontoyaApi api; 36 | private final PayloadManager payloadManager; 37 | private final HopLa hopla; 38 | private final JScrollBar hBar; 39 | private final AIConfiguration aiConfiguration; 40 | DebouncedSwingWorker, Void> debouncer = new DebouncedSwingWorker<>(); 41 | private JTextComponent source; 42 | private int caretStart = 0; 43 | private int caretPos = 0; 44 | 45 | public AutoCompleteMenu(HopLa hopla, MontoyaApi api, PayloadManager payloadManager, AIConfiguration aiConfiguration) { 46 | this.api = api; 47 | this.hopla = hopla; 48 | this.payloadManager = payloadManager; 49 | this.aiConfiguration = aiConfiguration; 50 | 51 | frame = generateJWindow(); 52 | 53 | suggestionList = new JList<>(); 54 | suggestionList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 55 | suggestionList.setLayoutOrientation(JList.VERTICAL); 56 | suggestionList.setFocusable(false); 57 | suggestionList.setVisibleRowCount(MIN_VISIBLE_ROWS); 58 | suggestionList.addMouseListener(new MouseAdapter() { 59 | public void mouseClicked(MouseEvent e) { 60 | // Double click 61 | if (e.getClickCount() == 2) { 62 | insertSelectedSuggestion(); 63 | } 64 | } 65 | }); 66 | JScrollPane scrollPane = new JScrollPane(suggestionList, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); 67 | scrollPane.setPreferredSize(new Dimension(FRAME_WIDTH, FRAME_HEIGHT)); 68 | hBar = scrollPane.getHorizontalScrollBar(); 69 | frame.getContentPane().add(scrollPane, BorderLayout.CENTER); 70 | } 71 | 72 | public void suggest(JTextComponent source, String input, int caretStart, int caretPos, Completer.CaretContext caretContext) { 73 | this.source = source; 74 | this.caretStart = caretStart; 75 | this.caretPos = caretPos; 76 | 77 | List suggestions = this.payloadManager.getSuggestions(input); 78 | 79 | if (Constants.EXTERNAL_AI && hopla.aiAutocompletionEnabled && aiConfiguration.isAIConfigured && input.length() > HopLa.aiConfiguration.config.autocompletion_min_chars) { 80 | debouncer.trigger(() -> 81 | new AICompletion(suggestionList, suggestions, input, caretContext) 82 | ); 83 | } 84 | 85 | if (suggestions.isEmpty()) { 86 | hide(); 87 | return; 88 | } 89 | 90 | suggestionList.setListData(suggestions.toArray(new String[0])); 91 | suggestionList.setSelectedIndex(0); 92 | 93 | show(suggestions.size()); 94 | hBar.setValue(0); 95 | 96 | if (DEBUG) { 97 | api.logging().logToOutput("suggestion: " + suggestions); 98 | } 99 | 100 | } 101 | 102 | private void show(int lines) { 103 | int rowHeight = suggestionList.getCellBounds(0, 0).height; 104 | int heightList = ((Math.min(lines, MAX_VISIBLE_ROWS) + 1) * rowHeight) + 25; 105 | if (heightList < 50) { 106 | heightList = 50; 107 | } 108 | 109 | Point np = new Point(); 110 | try { 111 | np.x = source.modelToView2D(source.getCaretPosition()).getBounds().x + source.getLocationOnScreen().x; 112 | np.y = source.modelToView2D(source.getCaretPosition()).getBounds().y + source.getLocationOnScreen().y + FRAME_TOP_MARGIN; 113 | frame.setLocation(np); 114 | } catch (BadLocationException e) { 115 | HopLa.montoyaApi.logging().logToError("Suggest suggestion error: " + e.getMessage()); 116 | return; 117 | } 118 | 119 | Rectangle screenBounds = GraphicsEnvironment 120 | .getLocalGraphicsEnvironment() 121 | .getMaximumWindowBounds(); 122 | 123 | int height = Math.min(heightList, screenBounds.height - np.y) - FRAME_TOP_MARGIN; 124 | frame.setPreferredSize(new Dimension(FRAME_WIDTH, height)); 125 | 126 | frame.pack(); 127 | frame.setVisible(true); 128 | } 129 | 130 | public void handleKey(int keyCode) { 131 | int i = suggestionList.getSelectedIndex(); 132 | switch (keyCode) { 133 | case KeyEvent.VK_UP: 134 | if (i > 0) { 135 | suggestionList.setSelectedIndex(i - 1); 136 | suggestionList.ensureIndexIsVisible(i - 1); 137 | } 138 | break; 139 | case KeyEvent.VK_DOWN: 140 | if (i < suggestionList.getModel().getSize() - 1) { 141 | suggestionList.setSelectedIndex(i + 1); 142 | suggestionList.ensureIndexIsVisible(i + 1); 143 | } 144 | break; 145 | case KeyEvent.VK_RIGHT: 146 | hBar.setValue(Math.min(hBar.getValue() + SCROLL_STEP, hBar.getMaximum())); 147 | break; 148 | case KeyEvent.VK_LEFT: 149 | hBar.setValue(Math.min(hBar.getValue() - SCROLL_STEP, 0)); 150 | break; 151 | case KeyEvent.VK_ENTER: 152 | case KeyEvent.VK_TAB: 153 | insertSelectedSuggestion(); 154 | break; 155 | case KeyEvent.VK_ESCAPE: 156 | hide(); 157 | break; 158 | } 159 | 160 | } 161 | 162 | private void insertSelectedSuggestion() { 163 | String val = suggestionList.getSelectedValue(); 164 | if (val.contains(CUSTOM_KEYWORD_SEPARATOR)) { 165 | val = val.split(Pattern.quote(CUSTOM_KEYWORD_SEPARATOR))[1]; 166 | } 167 | if (val.contains(AI_KEYWORD_SEPARATOR)) { 168 | val = val.split(Pattern.quote(AI_KEYWORD_SEPARATOR))[1]; 169 | } 170 | 171 | 172 | if (val == null || source == null) return; 173 | try { 174 | Document doc = source.getDocument(); 175 | doc.remove(caretStart, caretPos - caretStart); 176 | doc.insertString(caretStart, val, null); 177 | source.setCaretPosition(caretStart + val.length()); 178 | } catch (Exception ex) { 179 | api.logging().logToError("Insert suggestion error: " + ex.getMessage()); 180 | } 181 | debouncer.cancel(); 182 | hide(); 183 | } 184 | 185 | public void dispose() { 186 | if (frame != null) { 187 | frame.dispose(); 188 | } 189 | } 190 | 191 | public void hide() { 192 | frame.setVisible(false); 193 | } 194 | 195 | public boolean isVisible() { 196 | return frame.isVisible(); 197 | } 198 | 199 | public static class DebouncedSwingWorker { 200 | private Supplier> workerSupplier; 201 | private SwingWorker currentWorker; 202 | 203 | public DebouncedSwingWorker() { 204 | } 205 | 206 | public void cancel() { 207 | if (currentWorker != null && !currentWorker.isDone()) { 208 | currentWorker.cancel(true); 209 | } 210 | } 211 | 212 | public void trigger(Supplier> workerSupplier) { 213 | cancel(); 214 | 215 | currentWorker = workerSupplier.get(); 216 | currentWorker.execute(); 217 | } 218 | 219 | } 220 | 221 | class AICompletion extends SwingWorker, Void> { 222 | private final JList suggestionList; 223 | private final List suggestions; 224 | private final Completer.CaretContext caretContext; 225 | private final String input; 226 | 227 | public AICompletion(JList suggestionList, List suggestions, String input, Completer.CaretContext caretContext) { 228 | this.suggestionList = suggestionList; 229 | this.suggestions = suggestions; 230 | this.caretContext = caretContext; 231 | this.input = input; 232 | } 233 | 234 | @Override 235 | protected List doInBackground() throws Exception { 236 | try { 237 | return HopLa.aiConfiguration.completionProvider.autoCompletion(this.caretContext); 238 | } catch (Exception e) { 239 | api.logging().logToError("AI Completion cancelled, input: " + input); 240 | throw e; 241 | } 242 | } 243 | 244 | @Override 245 | protected void done() { 246 | try { 247 | suggestions.addAll(0, get().stream().map(s -> AI_KEYWORD_SEPARATOR + input + s).toList()); 248 | suggestionList.setListData(suggestions.toArray(new String[0])); 249 | if (!suggestions.isEmpty()) { 250 | if (DEBUG) { 251 | api.logging().logToOutput("AI suggestion: " + suggestions); 252 | } 253 | show(suggestions.size()); 254 | } 255 | 256 | } catch (InterruptedException | ExecutionException e) { 257 | alert("AI Completion error: " + e.getMessage()); 258 | api.logging().logToError("AI Completion: " + e.getMessage()); 259 | } catch (CancellationException exc) { 260 | if (DEBUG) { 261 | api.logging().logToError("AI Completion cancelled, input: " + input); 262 | } 263 | } 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/main/java/com/hopla/ai/OllamaProvider.java: -------------------------------------------------------------------------------- 1 | package com.hopla.ai; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonObject; 5 | import com.hopla.Completer; 6 | import com.hopla.HopLa; 7 | import okhttp3.Call; 8 | import okhttp3.Request; 9 | import okhttp3.RequestBody; 10 | import okhttp3.Response; 11 | 12 | import java.io.BufferedReader; 13 | import java.io.IOException; 14 | import java.io.InputStreamReader; 15 | import java.nio.charset.StandardCharsets; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.Objects; 20 | 21 | import static com.hopla.Constants.DEBUG_AI; 22 | import static com.hopla.Utils.mapToJson; 23 | 24 | public class OllamaProvider extends AIProvider { 25 | 26 | public OllamaProvider(LLMConfig config, LLMConfig.Provider providerConfig) { 27 | super(AIProviderType.OLLAMA, AIProviderType.OLLAMA.toString(), config, providerConfig); 28 | } 29 | 30 | @Override 31 | public void instruct(String prompt, StreamingCallback callback) throws IOException { 32 | 33 | if (providerConfig.quick_action_model == null || providerConfig.quick_action_model.isEmpty()) { 34 | throw new IOException("Ollama model undefined"); 35 | } 36 | 37 | JsonObject jsonPayload = new JsonObject(); 38 | jsonPayload.addProperty("model", providerConfig.quick_action_model); 39 | jsonPayload.addProperty("prompt", prompt); 40 | jsonPayload.addProperty("stream", true); 41 | jsonPayload.addProperty("keep_alive", "60m"); 42 | 43 | 44 | if (!providerConfig.quick_action_system_prompt.isEmpty()) { 45 | jsonPayload.addProperty("system", providerConfig.quick_action_system_prompt); 46 | } 47 | if (!providerConfig.quick_action_params.isEmpty()) { 48 | jsonPayload.add("options", mapToJson(providerConfig.quick_action_params)); 49 | } 50 | 51 | if (!providerConfig.quick_action_stops.isEmpty()) { 52 | JsonArray stopArray = new JsonArray(); 53 | providerConfig.quick_action_stops.forEach(stopArray::add); 54 | if (jsonPayload.has("options")) { 55 | jsonPayload.get("options").getAsJsonObject().add("stop", stopArray); 56 | } else { 57 | JsonObject obj = new JsonObject(); 58 | obj.add("stop", stopArray); 59 | jsonPayload.add("options", obj); 60 | } 61 | } 62 | String jsonString = gson.toJson(jsonPayload); 63 | 64 | if (DEBUG_AI) { 65 | HopLa.montoyaApi.logging().logToOutput("quick action request: " + jsonString); 66 | } 67 | 68 | RequestBody body = RequestBody.create( 69 | jsonString, 70 | JSON 71 | ); 72 | 73 | Request.Builder builder = new Request.Builder().url(providerConfig.quick_action_endpoint); 74 | 75 | for (Map.Entry entry : providerConfig.headers.entrySet()) { 76 | builder.addHeader(entry.getKey(), String.valueOf(entry.getValue())); 77 | } 78 | 79 | Request request = builder.post(body).build(); 80 | 81 | currentQuickActionCall = client.newCall(request); 82 | sendStreamingRequest(currentQuickActionCall, callback, false); 83 | } 84 | 85 | @Override 86 | public List autoCompletion(Completer.CaretContext caretContext) throws IOException { 87 | 88 | List completionParts = new ArrayList<>(); 89 | if (providerConfig.completion_model == null || providerConfig.completion_model.isEmpty()) { 90 | throw new IOException("Ollama model undefined"); 91 | } 92 | 93 | JsonObject jsonPayload = new JsonObject(); 94 | jsonPayload.addProperty("model", providerConfig.completion_model); 95 | jsonPayload.addProperty("prompt", promptReplace(caretContext, providerConfig.completion_prompt)); 96 | 97 | if (DEBUG_AI) { 98 | HopLa.montoyaApi.logging().logToOutput("Suggestion prompt: " + promptReplace(caretContext, providerConfig.completion_prompt)); 99 | } 100 | 101 | 102 | jsonPayload.addProperty("stream", false); 103 | jsonPayload.addProperty("raw", true); 104 | jsonPayload.addProperty("keep_alive", "60m"); 105 | 106 | 107 | if (!providerConfig.completion_system_prompt.isEmpty()) { 108 | jsonPayload.addProperty("system", promptReplace(caretContext, providerConfig.completion_system_prompt)); 109 | } 110 | if (!providerConfig.completion_params.isEmpty()) { 111 | jsonPayload.add("options", mapToJson(providerConfig.completion_params)); 112 | } 113 | 114 | if (!providerConfig.completion_stops.isEmpty()) { 115 | JsonArray stopArray = new JsonArray(); 116 | providerConfig.completion_stops.forEach(stopArray::add); 117 | if (jsonPayload.has("options")) { 118 | jsonPayload.get("options").getAsJsonObject().add("stop", stopArray); 119 | } else { 120 | JsonObject obj = new JsonObject(); 121 | obj.add("stop", stopArray); 122 | jsonPayload.add("options", obj); 123 | } 124 | } 125 | 126 | String jsonString = gson.toJson(jsonPayload); 127 | 128 | if (DEBUG_AI) { 129 | HopLa.montoyaApi.logging().logToOutput("Suggestion request: " + jsonString); 130 | } 131 | 132 | RequestBody body = RequestBody.create( 133 | jsonString, 134 | JSON 135 | ); 136 | 137 | 138 | Request.Builder builder = new Request.Builder().url(providerConfig.completion_endpoint); 139 | 140 | for (Map.Entry entry : providerConfig.headers.entrySet()) { 141 | builder.addHeader(entry.getKey(), String.valueOf(entry.getValue())); 142 | } 143 | 144 | Request request = builder.post(body).build(); 145 | 146 | if (currentQuickActionCall != null) { 147 | currentCompletionCall.cancel(); 148 | } 149 | currentCompletionCall = client.newCall(request); 150 | 151 | try (Response response = currentCompletionCall.execute()) { 152 | if (!response.isSuccessful()) { 153 | throw new IOException(response.code() + "\n" + Objects.requireNonNull(response.body()).string()); 154 | } 155 | 156 | try (BufferedReader reader = new BufferedReader(new InputStreamReader( 157 | Objects.requireNonNull(response.body()).byteStream(), StandardCharsets.UTF_8)) 158 | ) { 159 | String line; 160 | while ((line = reader.readLine()) != null) { 161 | JsonObject lineJson = gson.fromJson(line, JsonObject.class); 162 | String responsePart = lineJson.get("response").getAsString(); 163 | if (responsePart.contains(" ")) { 164 | completionParts.add(responsePart.split(" ")[0]); 165 | } 166 | completionParts.add(responsePart); 167 | 168 | if (lineJson.get("done").getAsBoolean()) { 169 | break; 170 | } 171 | } 172 | } 173 | } catch (Exception e) { 174 | throw new IOException("Error : " + e.getMessage()); 175 | } 176 | 177 | if (DEBUG_AI) { 178 | HopLa.montoyaApi.logging().logToOutput("AI suggestion: " + completionParts); 179 | } 180 | return completionParts; 181 | } 182 | 183 | @Override 184 | public void chat(AIChats.Chat chat, StreamingCallback callback) { 185 | JsonArray messages = new JsonArray(); 186 | 187 | if (!providerConfig.chat_system_prompt.isEmpty()) { 188 | JsonObject userMessage = new JsonObject(); 189 | userMessage.addProperty("role", AIChats.MessageRole.SYSTEM.toString()); 190 | userMessage.addProperty("content", providerConfig.chat_system_prompt); 191 | messages.add(userMessage); 192 | } 193 | 194 | for (AIChats.Message message : chat.getMessages().subList(0, chat.getMessages().size() - 1)) { 195 | JsonObject userMessage = new JsonObject(); 196 | userMessage.addProperty("role", message.getRole().toString().toLowerCase()); 197 | userMessage.addProperty("content", message.getContent()); 198 | messages.add(userMessage); 199 | } 200 | 201 | JsonObject jsonPayload = new JsonObject(); 202 | jsonPayload.addProperty("model", providerConfig.chat_model); 203 | jsonPayload.add("messages", messages); 204 | jsonPayload.addProperty("stream", true); 205 | jsonPayload.addProperty("keep_alive", "60m"); 206 | 207 | if (!providerConfig.chat_params.isEmpty()) { 208 | jsonPayload.add("options", mapToJson(providerConfig.chat_params)); 209 | } 210 | 211 | if (!providerConfig.chat_stops.isEmpty()) { 212 | JsonArray stopArray = new JsonArray(); 213 | providerConfig.chat_stops.forEach(stopArray::add); 214 | if (jsonPayload.has("options")) { 215 | jsonPayload.get("options").getAsJsonObject().add("stop", stopArray); 216 | } else { 217 | JsonObject obj = new JsonObject(); 218 | obj.add("stop", stopArray); 219 | jsonPayload.add("options", obj); 220 | } 221 | } 222 | 223 | String jsonString = gson.toJson(jsonPayload); 224 | RequestBody body = RequestBody.create(jsonString, JSON); 225 | 226 | Request.Builder builder = new Request.Builder().url(providerConfig.chat_endpoint); 227 | 228 | for (Map.Entry entry : providerConfig.headers.entrySet()) { 229 | builder.addHeader(entry.getKey(), String.valueOf(entry.getValue())); 230 | } 231 | 232 | Request request = builder.post(body).build(); 233 | 234 | if (DEBUG_AI) { 235 | HopLa.montoyaApi.logging().logToOutput("AI chat request: " + jsonString); 236 | } 237 | 238 | currentChatcall = client.newCall(request); 239 | 240 | sendStreamingRequest(currentChatcall, callback, true); 241 | 242 | } 243 | 244 | private void sendStreamingRequest(Call call, StreamingCallback callback, Boolean isChat) { 245 | new Thread(() -> { 246 | try (Response response = call.execute()) { 247 | if (!response.isSuccessful()) { 248 | callback.onError("AI API error : " + response.code() + "\n" + Objects.requireNonNull(response.body()).string()); 249 | return; 250 | } 251 | 252 | BufferedReader reader = new BufferedReader(new InputStreamReader(response.body().byteStream())); 253 | String line; 254 | while ((line = reader.readLine()) != null) { 255 | if (Thread.currentThread().isInterrupted()) break; 256 | 257 | JsonObject responseJson = gson.fromJson(line, JsonObject.class); 258 | 259 | if (isChat) { 260 | JsonObject messageObject = responseJson.getAsJsonObject("message"); 261 | if (DEBUG_AI) { 262 | HopLa.montoyaApi.logging().logToOutput("AI streaming response: " + messageObject.get("content").getAsString()); 263 | } 264 | callback.onData(messageObject.get("content").getAsString()); 265 | } else { 266 | if (DEBUG_AI) { 267 | HopLa.montoyaApi.logging().logToOutput("AI streaming response: " + responseJson.get("response").getAsString()); 268 | } 269 | callback.onData(responseJson.get("response").getAsString()); 270 | } 271 | 272 | } 273 | callback.onDone(); 274 | } catch (IOException ex) { 275 | callback.onError("Cancelled or error : " + ex.getMessage()); 276 | } catch (Exception ex) { 277 | callback.onError("AI streaming error : " + ex.getMessage()); 278 | } 279 | }).start(); 280 | } 281 | } -------------------------------------------------------------------------------- /src/main/java/com/hopla/PayloadManager.java: -------------------------------------------------------------------------------- 1 | package com.hopla; 2 | 3 | import burp.api.montoya.MontoyaApi; 4 | import burp.api.montoya.persistence.Preferences; 5 | import org.yaml.snakeyaml.LoaderOptions; 6 | import org.yaml.snakeyaml.Yaml; 7 | import org.yaml.snakeyaml.constructor.Constructor; 8 | import org.yaml.snakeyaml.inspector.TagInspector; 9 | 10 | import javax.crypto.Cipher; 11 | import javax.crypto.spec.SecretKeySpec; 12 | import javax.swing.*; 13 | import javax.swing.filechooser.FileNameExtensionFilter; 14 | import java.io.*; 15 | import java.nio.charset.StandardCharsets; 16 | import java.nio.file.Files; 17 | import java.nio.file.Paths; 18 | import java.util.*; 19 | import java.util.stream.Collectors; 20 | 21 | import static com.hopla.Constants.DEFAULT_RESOURCE_ENCRYPT_KEY; 22 | import static com.hopla.Utils.isYamlFile; 23 | 24 | public class PayloadManager { 25 | 26 | private final MontoyaApi api; 27 | private final Preferences preferences; 28 | private final LocalPayloadsManager localPayloadsManager; 29 | private PayloadDefinition payloads; 30 | 31 | public PayloadManager(MontoyaApi api, LocalPayloadsManager localPayloadsManager) { 32 | this.api = api; 33 | this.preferences = api.persistence().preferences(); 34 | this.localPayloadsManager = localPayloadsManager; 35 | loadPayloads(); 36 | } 37 | 38 | private static List filterSuggestions(String input, Set options) { 39 | return options.stream() 40 | .filter(word -> word.startsWith(input)) 41 | .map(word -> new AbstractMap.SimpleEntry<>(word, longestCommonPrefixLength(input, word))) 42 | .filter(entry -> entry.getValue() > 0) 43 | .sorted((a, b) -> Integer.compare(b.getValue(), a.getValue())) 44 | .map(Map.Entry::getKey) 45 | .toList(); 46 | } 47 | 48 | private static int longestCommonPrefixLength(String a, String b) { 49 | int len = Math.min(a.length(), b.length()); 50 | int i = 0; 51 | while (i < len && a.charAt(i) == b.charAt(i)) { 52 | i++; 53 | } 54 | return i; 55 | } 56 | 57 | public void export() { 58 | InputStream inputStream = getClass().getResourceAsStream(Constants.DEFAULT_PAYLOAD_RESOURCE_PATH); 59 | if (inputStream == null) { 60 | String exc = "Default Payloads configuration sample not found: " + Constants.DEFAULT_PAYLOAD_RESOURCE_PATH; 61 | api.logging().logToError(exc); 62 | Utils.alert(exc); 63 | return; 64 | } 65 | String sample = ""; 66 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { 67 | sample = reader.lines().collect(Collectors.joining("\n")); 68 | } catch (Exception e) { 69 | String exc = "Failed to read Payloads configuration sample: " + Constants.DEFAULT_PAYLOAD_RESOURCE_PATH; 70 | api.logging().logToError(exc); 71 | Utils.alert(exc); 72 | } 73 | 74 | JFileChooser fileChooser = new JFileChooser(); 75 | fileChooser.setAcceptAllFileFilterUsed(false); 76 | FileNameExtensionFilter filter = new FileNameExtensionFilter("YAML files (*.yaml, *.yml)", "yaml", "yml"); 77 | fileChooser.setFileFilter(filter); 78 | fileChooser.setDialogTitle("Choose export location"); 79 | 80 | int userSelection = fileChooser.showSaveDialog(null); 81 | 82 | if (userSelection == JFileChooser.APPROVE_OPTION) { 83 | File fileToSave = fileChooser.getSelectedFile(); 84 | if (!fileToSave.getName().toLowerCase().endsWith(".yaml") || !fileToSave.getName().toLowerCase().endsWith(".yml")) { 85 | fileToSave = new File(fileToSave.getAbsolutePath() + ".yml"); 86 | } 87 | 88 | try { 89 | Files.writeString(fileToSave.toPath(), sample); 90 | JOptionPane.showMessageDialog(null, "File saved: " + fileToSave.getAbsolutePath()); 91 | } catch (IOException e) { 92 | JOptionPane.showMessageDialog(null, "Write error: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); 93 | } 94 | } 95 | } 96 | 97 | public void loadPayloads() { 98 | String savedPath = preferences.getString(Constants.PREFERENCE_CUSTOM_PATH); 99 | 100 | if (savedPath != null && !savedPath.isEmpty() && !Constants.DEFAULT_PAYLOAD_RESOURCE_PATH.equals(savedPath)) { 101 | try { 102 | payloads = loadFromFile(savedPath, false); 103 | api.logging().logToOutput("Loaded payloads from saved path: " + savedPath); 104 | return; 105 | } catch (Exception e) { 106 | api.logging().logToError("Failed to load payloads from saved path: " + savedPath + ", loading default"); 107 | Utils.alert(Constants.ERROR_INVALID_FILE + e.getMessage()); 108 | } 109 | } 110 | 111 | // Load from default resource 112 | try (InputStream in = getClass().getResourceAsStream(Constants.DEFAULT_PAYLOAD_RESOURCE_PATH)) { 113 | if (in == null) { 114 | api.logging().logToError("Default payload resource not found. " + Constants.DEFAULT_PAYLOAD_RESOURCE_PATH); 115 | payloads = new PayloadDefinition(); 116 | } else { 117 | payloads = loadFromInputStream(in, true); 118 | preferences.setString(Constants.PREFERENCE_CUSTOM_PATH, Constants.DEFAULT_PAYLOAD_RESOURCE_PATH); 119 | api.logging().logToOutput("Loaded payloads from default resource. " + Constants.DEFAULT_PAYLOAD_RESOURCE_PATH); 120 | } 121 | } catch (Exception e) { 122 | api.logging().logToError(Constants.ERROR_INVALID_FILE + e.getMessage()); 123 | Utils.alert(Constants.ERROR_INVALID_FILE + e.getMessage()); 124 | api.logging().logToError("Failed to load default payloads resource. " + Constants.DEFAULT_PAYLOAD_RESOURCE_PATH); 125 | payloads = new PayloadDefinition(); 126 | } 127 | } 128 | 129 | 130 | private PayloadDefinition loadFromFile(String path, boolean decrypt) throws Exception { 131 | try (InputStream in = Files.newInputStream(Paths.get(path))) { 132 | return loadFromInputStream(in, decrypt); 133 | } 134 | } 135 | 136 | private PayloadDefinition loadFromInputStream(InputStream in, boolean decrypt) throws Exception { 137 | var loaderoptions = new LoaderOptions(); 138 | TagInspector taginspector = 139 | tag -> tag.getClassName().equals(PayloadDefinition.class.getName()); 140 | loaderoptions.setTagInspector(taginspector); 141 | 142 | Yaml yaml = new Yaml(new Constructor(PayloadDefinition.class, loaderoptions)); 143 | PayloadDefinition data = null; 144 | if (decrypt) { 145 | // decrypt embedded resource 146 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 147 | byte[] buffer = new byte[4096]; 148 | int r; 149 | while ((r = in.read(buffer)) != -1) { 150 | baos.write(buffer, 0, r); 151 | } 152 | byte[] encryptedData = baos.toByteArray(); 153 | 154 | byte[] keyBytes = DEFAULT_RESOURCE_ENCRYPT_KEY.getBytes(); 155 | SecretKeySpec key = new SecretKeySpec(keyBytes, "AES"); 156 | 157 | Cipher cipher = Cipher.getInstance("AES"); 158 | cipher.init(Cipher.DECRYPT_MODE, key); 159 | 160 | byte[] content = cipher.doFinal(encryptedData); 161 | data = yaml.load(new String(content)); 162 | 163 | } else { 164 | data = yaml.load(in); 165 | } 166 | 167 | validateShortcuts(data); 168 | return data; 169 | } 170 | 171 | private void validateShortcuts(PayloadDefinition definition) { 172 | Set shortcuts = new HashSet<>(); 173 | 174 | for (PayloadDefinition.Category category : definition.categories) { 175 | this.recursiveValidateShortcuts(category, shortcuts); 176 | } 177 | 178 | if (!shortcuts.add(Utils.normalizeShortcut(definition.shortcut_payload_menu))) { 179 | throw new IllegalArgumentException( 180 | "Duplicate shortcut found: " + definition.shortcut_payload_menu 181 | ); 182 | } 183 | if (!shortcuts.add(Utils.normalizeShortcut(definition.shortcut_search_and_replace))) { 184 | throw new IllegalArgumentException( 185 | "Duplicate shortcut found: " + definition.shortcut_search_and_replace 186 | ); 187 | } 188 | } 189 | 190 | private void recursiveValidateShortcuts(PayloadDefinition.Category category, Set collector) { 191 | if (category.payloads != null) { 192 | for (PayloadDefinition.Payload payload : category.payloads) { 193 | if (payload.shortcut != null && !payload.shortcut.isBlank()) { 194 | if (!collector.add(Utils.normalizeShortcut(payload.shortcut))) { 195 | throw new IllegalArgumentException( 196 | "Duplicate shortcut found: " + payload.shortcut + " (used in payload: " + payload.name + ")" 197 | ); 198 | } 199 | } 200 | } 201 | } 202 | 203 | if (category.categories != null) { 204 | for (PayloadDefinition.Category sub : category.categories) { 205 | recursiveValidateShortcuts(sub, collector); 206 | } 207 | } 208 | } 209 | 210 | public List getSuggestions(String input) { 211 | List suggestions = new ArrayList<>(); 212 | Set localPayloadsSet = new HashSet<>(localPayloadsManager.getPayloads()); 213 | 214 | List results = filterSuggestions(input, localPayloadsSet); 215 | suggestions.addAll(results); 216 | if (suggestions.size() >= 25) { 217 | return suggestions.subList(0, 25); 218 | } 219 | 220 | results = filterSuggestions(input, this.payloads.flattenPayloadValues()); 221 | suggestions.addAll(results); 222 | if (suggestions.size() >= 25) { 223 | return suggestions.subList(0, 25); 224 | } 225 | 226 | results = filterSuggestions(input, this.payloads.flattenKeywordsValues()); 227 | suggestions.addAll(results); 228 | 229 | return suggestions.subList(0, Math.min(25, suggestions.size())); 230 | } 231 | 232 | public void choosePayloadFile() { 233 | JFileChooser fileChooser = new JFileChooser(); 234 | fileChooser.setAcceptAllFileFilterUsed(false); 235 | FileNameExtensionFilter filter = new FileNameExtensionFilter("YAML files (*.yaml, *.yml)", "yaml", "yml"); 236 | fileChooser.setFileFilter(filter); 237 | 238 | int result = fileChooser.showOpenDialog(null); 239 | 240 | if (result == JFileChooser.APPROVE_OPTION) { 241 | File selectedFile = fileChooser.getSelectedFile(); 242 | String path = selectedFile.getAbsolutePath(); 243 | 244 | if (!isYamlFile(path)) { 245 | Utils.alert(Constants.ERROR_INVALID_FILE_EXTENSION); 246 | return; 247 | } 248 | 249 | try { 250 | PayloadDefinition loaded = loadFromFile(path, false); 251 | if (loaded.isEmpty()) { 252 | Utils.alert(Constants.ERROR_EMPTY_FILE); 253 | return; 254 | } 255 | 256 | this.payloads = loaded; 257 | preferences.setString(Constants.PREFERENCE_CUSTOM_PATH, path); 258 | Utils.success(Constants.FILE_LOADED); 259 | api.logging().logToOutput(Constants.FILE_LOADED + ": " + path); 260 | } catch (Exception e) { 261 | api.logging().logToError(Constants.ERROR_INVALID_FILE + e.getMessage()); 262 | Utils.alert(Constants.ERROR_INVALID_FILE + e.getMessage()); 263 | } 264 | } 265 | } 266 | 267 | public String getCurrentPath() { 268 | String path = preferences.getString(Constants.PREFERENCE_CUSTOM_PATH); 269 | return (path != null && !path.isEmpty()) ? path : Constants.DEFAULT_PAYLOAD_RESOURCE_PATH; 270 | } 271 | 272 | 273 | public PayloadDefinition getPayloads() { 274 | return payloads; 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/main/java/com/hopla/ai/AIQuickAction.java: -------------------------------------------------------------------------------- 1 | package com.hopla.ai; 2 | 3 | import burp.api.montoya.ui.contextmenu.MessageEditorHttpRequestResponse; 4 | import com.hopla.HopLa; 5 | import com.hopla.Utils; 6 | import org.commonmark.node.Node; 7 | import org.commonmark.parser.Parser; 8 | import org.commonmark.renderer.html.HtmlRenderer; 9 | 10 | import javax.swing.*; 11 | import javax.swing.text.BadLocationException; 12 | import javax.swing.text.DefaultEditorKit; 13 | import javax.swing.text.DefaultStyledDocument; 14 | import javax.swing.text.Document; 15 | import javax.swing.text.html.HTMLEditorKit; 16 | import javax.swing.text.html.StyleSheet; 17 | import java.awt.*; 18 | import java.awt.event.*; 19 | import java.net.URL; 20 | 21 | import static com.hopla.Constants.DEBUG_AI; 22 | import static com.hopla.Utils.*; 23 | 24 | public class AIQuickAction { 25 | private final static String REQUEST_PLACEHOLDER = "@request@"; 26 | private final static String BUTTON_TEXT_SEND = "Apply"; 27 | private final static String BUTTON_CANCEL_SEND = "Cancel"; 28 | private final HTMLEditorKit kit = new HTMLEditorKit(); 29 | private final StyleSheet styleSheet = new StyleSheet(); 30 | private final Parser parser = Parser.builder().build(); 31 | private final HtmlRenderer renderer = HtmlRenderer.builder().escapeHtml(true).sanitizeUrls(true).build(); 32 | JFrame frame; 33 | AIConfiguration aiConfiguration; 34 | private JTextArea source; 35 | private JComboBox comboBox; 36 | private String outputData; 37 | private JTextPane output; 38 | private JScrollPane outputScrollPane; 39 | private JToggleButton markdownButton; 40 | 41 | public AIQuickAction(AIConfiguration aiConfiguration) { 42 | this.aiConfiguration = aiConfiguration; 43 | loadCss(); 44 | } 45 | 46 | private void buildFrame(MessageEditorHttpRequestResponse messageEditor, InputEvent event, String input) { 47 | if (frame != null) { 48 | frame.dispose(); 49 | } 50 | frame = generateJFrame(); 51 | frame.setLayout(new BorderLayout()); 52 | frame.setPreferredSize(new Dimension(700, 600)); 53 | 54 | JLabel statusLabel = new JLabel(""); 55 | JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); 56 | JButton insertButton = new JButton("Insert"); 57 | JButton cancelButton = new JButton("Cancel"); 58 | markdownButton = new JToggleButton("Markdown", true); 59 | buttonPanel.add(statusLabel); 60 | buttonPanel.add(markdownButton); 61 | buttonPanel.add(insertButton); 62 | buttonPanel.add(cancelButton); 63 | 64 | 65 | cancelButton.addActionListener(e -> { 66 | aiConfiguration.quickActionProvider.cancelCurrentQuickActionRequest(); 67 | }); 68 | 69 | frame.addWindowListener(new WindowAdapter() { 70 | @Override 71 | public void windowClosing(WindowEvent e) { 72 | aiConfiguration.quickActionProvider.cancelCurrentQuickActionRequest(); 73 | } 74 | }); 75 | 76 | JTextArea instruction = new JTextArea(); 77 | instruction.setLineWrap(true); 78 | instruction.setWrapStyleWord(true); 79 | instruction.setText(input); 80 | 81 | 82 | output = new JTextPane(); 83 | output.setEditable(false); 84 | output.setOpaque(false); 85 | output.setContentType("text/html"); 86 | output.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, true); 87 | output.setEditorKit(kit); 88 | Document doc = kit.createDefaultDocument(); 89 | output.setDocument(doc); 90 | output.setAlignmentX(Component.LEFT_ALIGNMENT); 91 | 92 | markdownButton.addItemListener(e -> { 93 | if (markdownButton.isSelected()) { 94 | output.setContentType("text/html"); 95 | output.setDocument(kit.createDefaultDocument()); 96 | } else { 97 | output.setContentType("text/plain"); 98 | output.setDocument(new DefaultStyledDocument()); 99 | } 100 | }); 101 | 102 | 103 | JPopupMenu contextMenu = new JPopupMenu(); 104 | contextMenu.add(new JMenuItem(new DefaultEditorKit.CopyAction())); 105 | contextMenu.add(new JMenuItem(new AbstractAction("Insert selection in editor") { 106 | public void actionPerformed(ActionEvent e) { 107 | if (output.getSelectedText() != null) { 108 | Utils.insertPayload(messageEditor, output.getSelectedText(), event); 109 | } 110 | 111 | } 112 | })); 113 | 114 | output.addMouseListener(new MouseAdapter() { 115 | public void mousePressed(MouseEvent e) { 116 | if (e.isPopupTrigger()) show(e); 117 | } 118 | 119 | public void mouseReleased(MouseEvent e) { 120 | if (e.isPopupTrigger()) show(e); 121 | } 122 | 123 | private void show(MouseEvent e) { 124 | if (output.getSelectedText() != null && !output.getSelectedText().isEmpty()) { 125 | contextMenu.show(e.getComponent(), e.getX(), e.getY()); 126 | } 127 | } 128 | }); 129 | 130 | insertButton.addActionListener(e -> { 131 | Utils.insertPayload(messageEditor, output.getText(), event); 132 | }); 133 | 134 | 135 | JComboBox comboBox = new JComboBox<>(this.aiConfiguration.config.quick_actions.toArray(new LLMConfig.QuickAction[0])); 136 | 137 | comboBox.addActionListener(e -> { 138 | LLMConfig.QuickAction item = (LLMConfig.QuickAction) comboBox.getSelectedItem(); 139 | if (item != null) { 140 | instruction.insert(item.content + " ", 0); 141 | } 142 | }); 143 | 144 | 145 | JPanel middleButtonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); 146 | JButton applyButton = new JButton("Apply"); 147 | JButton requestButton = new JButton(REQUEST_PLACEHOLDER); 148 | requestButton.addActionListener(e -> { 149 | int start = instruction.getCaretPosition(); 150 | int end = start; 151 | 152 | if (instruction.getSelectedText() != null && !instruction.getSelectedText().isEmpty()) { 153 | start = instruction.getSelectionStart(); 154 | end = instruction.getSelectionEnd(); 155 | } 156 | Document doc_instruct = instruction.getDocument(); 157 | try { 158 | doc_instruct.remove(start, end - start); 159 | doc_instruct.insertString(start, REQUEST_PLACEHOLDER, null); 160 | } catch (BadLocationException exc) { 161 | HopLa.montoyaApi.logging().logToError("AI chat insertion error: " + exc.getMessage()); 162 | } 163 | instruction.setCaretPosition(start + REQUEST_PLACEHOLDER.length()); 164 | }); 165 | JButton clearButton = new JButton("Clear"); 166 | 167 | clearButton.addActionListener(e -> instruction.setText("")); 168 | cancelButton.setEnabled(false); 169 | 170 | applyButton.addActionListener(e -> { 171 | if (applyButton.getText().equals(BUTTON_CANCEL_SEND) && aiConfiguration.quickActionProvider != null) { 172 | aiConfiguration.quickActionProvider.cancelCurrentQuickActionRequest(); 173 | applyButton.setText(BUTTON_TEXT_SEND); 174 | statusLabel.setText("Cancelled"); 175 | return; 176 | } 177 | 178 | String userInput = instruction.getText().trim(); 179 | userInput = userInput.replace(REQUEST_PLACEHOLDER, getRequest(messageEditor)); 180 | 181 | if (!userInput.isEmpty()) { 182 | statusLabel.setText("Thinking..."); 183 | outputData = ""; 184 | output.setText(""); 185 | cancelButton.setEnabled(true); 186 | 187 | try { 188 | aiConfiguration.quickActionProvider.instruct(userInput, new AIProvider.StreamingCallback() { 189 | @Override 190 | public void onData(String chunk) { 191 | if (!chunk.isEmpty()) { 192 | SwingUtilities.invokeLater(() -> { 193 | updateOutput(chunk); 194 | }); 195 | 196 | } 197 | } 198 | 199 | @Override 200 | public void onDone() { 201 | statusLabel.setText(""); 202 | applyButton.setText(BUTTON_TEXT_SEND); 203 | cancelButton.setEnabled(false); 204 | } 205 | 206 | @Override 207 | public void onError(String error) { 208 | statusLabel.setText(error); 209 | applyButton.setText(BUTTON_TEXT_SEND); 210 | cancelButton.setEnabled(false); 211 | } 212 | }); 213 | } catch (Exception exc) { 214 | alert("AI quick action error: " + exc.getMessage()); 215 | HopLa.montoyaApi.logging().logToError("AI quick action error: " + exc.getMessage()); 216 | cancelButton.setEnabled(false); 217 | } 218 | 219 | 220 | } 221 | }); 222 | 223 | 224 | frame.addComponentListener(new ComponentAdapter() { 225 | @Override 226 | public void componentResized(ComponentEvent e) { 227 | int width = frame.getWidth() - 50; 228 | output.setMaximumSize(new Dimension(width, Short.MAX_VALUE)); 229 | frame.repaint(); 230 | frame.invalidate(); 231 | } 232 | }); 233 | 234 | middleButtonPanel.add(requestButton); 235 | middleButtonPanel.add(applyButton); 236 | middleButtonPanel.add(clearButton); 237 | 238 | JPanel centerPanel = new JPanel(); 239 | centerPanel.setLayout(new BoxLayout(centerPanel, BoxLayout.Y_AXIS)); 240 | 241 | 242 | JScrollPane scrollPane1 = new JScrollPane(instruction); 243 | outputScrollPane = new JScrollPane(output); 244 | outputScrollPane.getVerticalScrollBar().setUnitIncrement(4); 245 | 246 | scrollPane1.setPreferredSize(new Dimension(600, 150)); 247 | middleButtonPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 40)); 248 | outputScrollPane.setPreferredSize(new Dimension(600, 150)); 249 | 250 | centerPanel.add(scrollPane1); 251 | centerPanel.add(middleButtonPanel); 252 | centerPanel.add(outputScrollPane); 253 | 254 | frame.add(comboBox, BorderLayout.NORTH); 255 | frame.add(centerPanel, BorderLayout.CENTER); 256 | frame.add(buttonPanel, BorderLayout.SOUTH); 257 | frame.pack(); 258 | } 259 | 260 | private void updateOutput(String chunk) { 261 | outputData += chunk; 262 | 263 | if (markdownButton.isSelected()) { 264 | String data = renderMarkdownToHtml(outputData); 265 | output.setText(data); 266 | } else { 267 | output.setText(outputData); 268 | } 269 | 270 | JScrollBar verticalBar = outputScrollPane.getVerticalScrollBar(); 271 | boolean atBottom = verticalBar.getValue() + verticalBar.getVisibleAmount() >= verticalBar.getMaximum() + 70; 272 | 273 | if (!atBottom) { 274 | SwingUtilities.invokeLater(() -> { 275 | verticalBar.setValue(verticalBar.getMaximum()); 276 | }); 277 | output.setCaretPosition(output.getDocument().getLength()); 278 | } 279 | 280 | } 281 | 282 | private String renderMarkdownToHtml(String markdown) { 283 | Node document = parser.parse(markdown); 284 | String body = renderer.render(document); 285 | if (DEBUG_AI) { 286 | HopLa.montoyaApi.logging().logToOutput("AI quick action html: " + body); 287 | } 288 | body = body.replaceAll("<([a-zA-Z][a-zA-Z0-9-]*)(?:\\s+[^<>]*?)?(/?)>", "<$1$2>"); 289 | return body; 290 | } 291 | 292 | private void loadCss() { 293 | URL cssUrl = getClass().getResource("/style.css"); 294 | styleSheet.importStyleSheet(cssUrl); 295 | kit.setStyleSheet(styleSheet); 296 | } 297 | 298 | public void show(MessageEditorHttpRequestResponse messageEditor, InputEvent event, String input) { 299 | this.source = (JTextArea) event.getSource(); 300 | buildFrame(messageEditor, event, input); 301 | frame.setVisible(true); 302 | } 303 | 304 | public void hide() { 305 | frame.setVisible(false); 306 | } 307 | 308 | public void dispose() { 309 | if (frame != null) { 310 | frame.dispose(); 311 | } 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /src/main/java/com/hopla/HopLa.java: -------------------------------------------------------------------------------- 1 | package com.hopla; 2 | 3 | 4 | import burp.api.montoya.BurpExtension; 5 | import burp.api.montoya.EnhancedCapability; 6 | import burp.api.montoya.MontoyaApi; 7 | import burp.api.montoya.core.Registration; 8 | import burp.api.montoya.extension.ExtensionUnloadingHandler; 9 | import burp.api.montoya.ui.contextmenu.MessageEditorHttpRequestResponse; 10 | import burp.api.montoya.ui.hotkey.HotKeyContext; 11 | import burp.api.montoya.ui.hotkey.HotKeyHandler; 12 | import com.hopla.ai.AIChats; 13 | import com.hopla.ai.AIConfiguration; 14 | import com.hopla.ai.AIQuickAction; 15 | 16 | import javax.swing.*; 17 | import java.awt.*; 18 | import java.awt.event.AWTEventListener; 19 | import java.util.ArrayList; 20 | import java.util.Set; 21 | 22 | import static com.hopla.Constants.*; 23 | import static com.hopla.Utils.alert; 24 | import static com.hopla.Utils.getSelectedText; 25 | 26 | public class HopLa implements BurpExtension, ExtensionUnloadingHandler, AWTEventListener { 27 | public static MontoyaApi montoyaApi; 28 | public static LocalPayloadsManager localPayloadsManager; 29 | public static SearchReplaceWindow searchReplaceWindow; 30 | public static AIChatPanel aiChatPanel; 31 | public static AIConfiguration aiConfiguration; 32 | public static AIChats aiChats; 33 | public static AIQuickAction aiQuickAction; 34 | private static String extensionName; 35 | private final ArrayList listeners = new ArrayList<>(); 36 | private final ArrayList registrations = new ArrayList(); 37 | public Boolean autocompletionEnabled; 38 | public Boolean shortcutsEnabled; 39 | public Boolean aiAutocompletionEnabled; 40 | private PayloadManager payloadManager; 41 | private AutoCompleteMenu autoCompleteMenu; 42 | private PayloadMenu payloadMenu; 43 | 44 | @Override 45 | public void initialize(MontoyaApi montoyaApi) { 46 | HopLa.montoyaApi = montoyaApi; 47 | HopLa.extensionName = Constants.EXTENSION_NAME; 48 | 49 | montoyaApi.extension().setName(Constants.EXTENSION_NAME); 50 | montoyaApi.extension().registerUnloadingHandler(this); 51 | 52 | aiConfiguration = new AIConfiguration(montoyaApi); 53 | aiChats = new AIChats(); 54 | 55 | aiQuickAction = new AIQuickAction(aiConfiguration); 56 | 57 | aiAutocompletionEnabled = montoyaApi.persistence() 58 | .preferences() 59 | .getBoolean(PREFERENCE_AI); 60 | 61 | if (aiAutocompletionEnabled == null) { 62 | aiAutocompletionEnabled = Boolean.FALSE; 63 | } 64 | 65 | shortcutsEnabled = montoyaApi.persistence() 66 | .preferences() 67 | .getBoolean(PREFERENCE_SHORTCUTS); 68 | 69 | if (shortcutsEnabled == null) { 70 | shortcutsEnabled = Boolean.TRUE; 71 | } 72 | 73 | autocompletionEnabled = montoyaApi.persistence() 74 | .preferences() 75 | .getBoolean(PREFERENCE_AUTOCOMPLETION); 76 | 77 | if (autocompletionEnabled == null) { 78 | autocompletionEnabled = Boolean.TRUE; 79 | } 80 | 81 | montoyaApi.logging().logToOutput("AI configured: " + aiConfiguration.isAIConfigured); 82 | 83 | 84 | if (Constants.EXTERNAL_AI) { 85 | montoyaApi.logging().logToOutput("AI Autocompletion enabled: " + aiAutocompletionEnabled); 86 | } 87 | 88 | montoyaApi.logging().logToOutput("Shortcuts enabled: " + shortcutsEnabled); 89 | montoyaApi.logging().logToOutput("Autocompletion enabled: " + autocompletionEnabled); 90 | 91 | localPayloadsManager = new LocalPayloadsManager(montoyaApi); 92 | payloadManager = new PayloadManager(montoyaApi, localPayloadsManager); 93 | autoCompleteMenu = new AutoCompleteMenu(this, montoyaApi, payloadManager, aiConfiguration); 94 | searchReplaceWindow = new SearchReplaceWindow(montoyaApi); 95 | payloadMenu = new PayloadMenu(payloadManager, montoyaApi); 96 | aiChatPanel = new AIChatPanel(aiConfiguration, aiChats); 97 | montoyaApi.userInterface().registerContextMenuItemsProvider(new ContextMenu(montoyaApi, payloadManager)); 98 | new MenuBar(montoyaApi, this, payloadManager, aiConfiguration); 99 | 100 | 101 | if (shortcutsEnabled) { 102 | enableShortcuts(); 103 | } 104 | if (autocompletionEnabled) { 105 | enableAutocompletion(); 106 | } 107 | 108 | if (Constants.DEBUG) { 109 | montoyaApi.logging().logToOutput("Debug enabled"); 110 | } 111 | montoyaApi.logging().logToOutput(Constants.INIT_MESSAGE); 112 | 113 | } 114 | 115 | @Override 116 | public Set enhancedCapabilities() { 117 | return Set.of(EnhancedCapability.AI_FEATURES); 118 | } 119 | 120 | public void enableAutocompletion() { 121 | montoyaApi.persistence() 122 | .preferences().setBoolean(PREFERENCE_AUTOCOMPLETION, true); 123 | autocompletionEnabled = true; 124 | Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK); 125 | } 126 | 127 | public void disableAutocompletion() { 128 | montoyaApi.persistence() 129 | .preferences().setBoolean(PREFERENCE_AUTOCOMPLETION, false); 130 | autocompletionEnabled = false; 131 | removeListeners(); 132 | } 133 | 134 | public void enableShortcuts() { 135 | montoyaApi.persistence() 136 | .preferences().setBoolean(PREFERENCE_SHORTCUTS, true); 137 | shortcutsEnabled = true; 138 | registerShortcuts(); 139 | } 140 | 141 | public void disableShortcuts() { 142 | montoyaApi.persistence() 143 | .preferences().setBoolean(PREFERENCE_SHORTCUTS, false); 144 | shortcutsEnabled = false; 145 | unregisterShortcuts(); 146 | } 147 | 148 | private void unregisterShortcuts() { 149 | for (Registration registration : registrations) { 150 | registration.deregister(); 151 | } 152 | registrations.clear(); 153 | } 154 | 155 | 156 | @Override 157 | public void eventDispatched(AWTEvent event) { 158 | if (event.getSource() instanceof JTextArea source) { 159 | if (source.getClientProperty("hasListener") != null && ((Boolean) source.getClientProperty("hasListener"))) { 160 | return; 161 | } 162 | 163 | // enable to debug awt frame 164 | if (AWT_DEBUG) { 165 | Container comp = source; 166 | while (comp != null) { 167 | montoyaApi.logging().logToOutput("Ancestor: " + comp.getClass().getName() + " name: " + comp.getName()); 168 | comp = comp.getParent(); 169 | } 170 | } 171 | 172 | Container is_editor = SwingUtilities.getAncestorNamed("messageEditor", source); 173 | 174 | if (is_editor == null) { 175 | return; 176 | } 177 | if (AWT_DEBUG) { 178 | montoyaApi.logging().logToOutput("Message editor detected: " + source.getName()); 179 | } 180 | if (!source.isEditable()) { 181 | return; 182 | } 183 | if (Constants.DEBUG) { 184 | montoyaApi.logging().logToOutput("Message editor is editable: " + source.getName()); 185 | } 186 | if (autocompletionEnabled) { 187 | Completer t = new Completer(montoyaApi, source, autoCompleteMenu); 188 | source.putClientProperty("hasListener", true); 189 | this.listeners.add(t); 190 | if (Constants.DEBUG) { 191 | montoyaApi.logging().logToOutput("Add completer: " + source.getName()); 192 | } 193 | } 194 | } 195 | } 196 | 197 | 198 | private void removeListeners() { 199 | Toolkit.getDefaultToolkit().removeAWTEventListener(this); 200 | 201 | // Remove all listeners on unload 202 | for (Completer listener : this.listeners) { 203 | listener.detach(); 204 | listener.getSource().putClientProperty("hasListener", false); 205 | } 206 | } 207 | 208 | @Override 209 | public void extensionUnloaded() { 210 | removeListeners(); 211 | autoCompleteMenu.dispose(); 212 | payloadMenu.dispose(); 213 | localPayloadsManager.dispose(); 214 | searchReplaceWindow.dispose(); 215 | aiChatPanel.dispose(); 216 | aiQuickAction.dispose(); 217 | unregisterShortcuts(); 218 | montoyaApi.logging().logToOutput(extensionName + " unloaded"); 219 | } 220 | 221 | private void registerShortcuts() { 222 | 223 | if (montoyaApi.burpSuite().version().buildNumber() < 20250300000037651L) { 224 | alert("Register Hotkey not supported with this Burp Version"); 225 | return; 226 | } 227 | 228 | for (PayloadDefinition.Category category : payloadManager.getPayloads().categories) { 229 | this.recursiveRegisterShortcuts(category); 230 | } 231 | 232 | this.registerShortcut(payloadManager.getPayloads().shortcut_payload_menu, "Payload Menu", event -> { 233 | MessageEditorHttpRequestResponse messageEditor = event.messageEditorRequestResponse().get(); 234 | payloadMenu.show(messageEditor, event.inputEvent()); 235 | }); 236 | 237 | this.registerShortcut(payloadManager.getPayloads().shortcut_search_and_replace, "Search Replace", event -> { 238 | MessageEditorHttpRequestResponse messageEditor = event.messageEditorRequestResponse().get(); 239 | searchReplaceWindow.attach(messageEditor, event.inputEvent(), getSelectedText(messageEditor)); 240 | }); 241 | 242 | this.registerShortcut(payloadManager.getPayloads().shortcut_add_custom_keyword, "Add custom keyword", event -> { 243 | MessageEditorHttpRequestResponse messageEditor = event.messageEditorRequestResponse().get(); 244 | HopLa.localPayloadsManager.add(getSelectedText(messageEditor)); 245 | }); 246 | 247 | this.registerShortcut(payloadManager.getPayloads().shortcut_collaborator, "Collaborator", event -> { 248 | MessageEditorHttpRequestResponse messageEditor = event.messageEditorRequestResponse().get(); 249 | Utils.InsertCollaboratorPayload(montoyaApi, messageEditor, event.inputEvent()); 250 | }); 251 | 252 | if (aiConfiguration.isAIConfigured) { 253 | this.registerShortcut(aiConfiguration.config.shortcut_ai_chat, "AI chat", event -> { 254 | MessageEditorHttpRequestResponse messageEditor = event.messageEditorRequestResponse().get(); 255 | aiChatPanel.show(messageEditor, event.inputEvent(), getSelectedText(messageEditor)); 256 | }); 257 | this.registerShortcut(aiConfiguration.config.shortcut_quick_action, "AI Quick action", event -> { 258 | MessageEditorHttpRequestResponse messageEditor = event.messageEditorRequestResponse().get(); 259 | aiQuickAction.show(messageEditor, event.inputEvent(), getSelectedText(messageEditor)); 260 | }); 261 | 262 | 263 | } 264 | 265 | 266 | } 267 | 268 | private void recursiveRegisterShortcuts(PayloadDefinition.Category category) { 269 | if (category.payloads != null) { 270 | for (PayloadDefinition.Payload payload : category.payloads) { 271 | if (payload.shortcut == null || payload.shortcut.isBlank()) { 272 | continue; 273 | } 274 | this.registerShortcut(payload.shortcut, category.name + " " + payload.name, event -> { 275 | if (event.messageEditorRequestResponse().isEmpty()) { 276 | return; 277 | } 278 | MessageEditorHttpRequestResponse messageEditor = event.messageEditorRequestResponse().get(); 279 | Utils.insertPayload(messageEditor, payload.value, event.inputEvent()); 280 | }); 281 | } 282 | } 283 | 284 | if (category.categories != null) { 285 | for (PayloadDefinition.Category sub : category.categories) { 286 | this.recursiveRegisterShortcuts(sub); 287 | } 288 | } 289 | } 290 | 291 | 292 | private void registerShortcut(String shortcut, String message, HotKeyHandler handler) { 293 | String normalizedShortcut = Utils.normalizeShortcut(shortcut); 294 | Registration registration = montoyaApi.userInterface().registerHotKeyHandler(HotKeyContext.HTTP_MESSAGE_EDITOR, normalizedShortcut, handler); 295 | 296 | if (registration.isRegistered()) { 297 | montoyaApi.logging().logToOutput("Successfully registered hotkey handler: " + normalizedShortcut + " - " + message); 298 | registrations.add(registration); 299 | } else { 300 | montoyaApi.logging().logToError("Failed to register hotkey handler: " + normalizedShortcut + " - " + message); 301 | alert("Failed to register hotkey handler: " + normalizedShortcut); 302 | } 303 | } 304 | } -------------------------------------------------------------------------------- /src/main/java/com/hopla/SearchReplaceWindow.java: -------------------------------------------------------------------------------- 1 | package com.hopla; 2 | 3 | import burp.api.montoya.MontoyaApi; 4 | import burp.api.montoya.core.HighlightColor; 5 | import burp.api.montoya.http.message.requests.HttpRequest; 6 | import burp.api.montoya.ui.Theme; 7 | import burp.api.montoya.ui.contextmenu.MessageEditorHttpRequestResponse; 8 | 9 | import javax.swing.*; 10 | import javax.swing.event.DocumentEvent; 11 | import javax.swing.event.DocumentListener; 12 | import javax.swing.text.BadLocationException; 13 | import javax.swing.text.Highlighter; 14 | import javax.swing.text.JTextComponent; 15 | import java.awt.*; 16 | import java.awt.event.InputEvent; 17 | import java.awt.event.WindowAdapter; 18 | import java.awt.event.WindowEvent; 19 | import java.awt.geom.Rectangle2D; 20 | import java.util.regex.Matcher; 21 | import java.util.regex.Pattern; 22 | 23 | import static com.hopla.Constants.DEBUG; 24 | import static com.hopla.Utils.generateJFrame; 25 | 26 | public class SearchReplaceWindow { 27 | private static final int FRAME_MARGIN = 10; 28 | private final JCheckBox regexCheck = new JCheckBox("Regex"); 29 | private final JCheckBox caseCheck = new JCheckBox("Case sensitive"); 30 | private final JButton replaceAllButton = new JButton("Replace All"); 31 | private final JLabel statusLabel = new JLabel(" "); 32 | private final MontoyaApi api; 33 | private final java.util.List matchPositions = new java.util.ArrayList<>(); 34 | private final DocumentListener documentListener; 35 | private Highlighter highlighter; 36 | private MessageEditorHttpRequestResponse messageEditor; 37 | private JTextField searchField; 38 | private JTextField replaceField; 39 | private int currentMatchIndex = -1; 40 | private JTextComponent source; 41 | private JFrame frame; 42 | 43 | public SearchReplaceWindow(MontoyaApi api) { 44 | this.api = api; 45 | this.documentListener = new DocumentListener() { 46 | @Override 47 | public void insertUpdate(DocumentEvent e) { 48 | highlightSearch(); 49 | } 50 | 51 | @Override 52 | public void removeUpdate(DocumentEvent e) { 53 | highlightSearch(); 54 | } 55 | 56 | @Override 57 | public void changedUpdate(DocumentEvent e) { 58 | } 59 | 60 | }; 61 | } 62 | 63 | public void attach(MessageEditorHttpRequestResponse messageEditor, InputEvent event, String input) { 64 | this.messageEditor = messageEditor; 65 | 66 | if (frame != null) { 67 | frame.dispose(); 68 | } 69 | frame = generateJFrame(); 70 | frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); 71 | 72 | frame.addWindowListener(new WindowAdapter() { 73 | @Override 74 | public void windowClosing(WindowEvent e) { 75 | SearchReplaceWindow.this.dispose(); 76 | } 77 | }); 78 | 79 | this.source = (JTextComponent) event.getSource(); 80 | highlighter = source.getHighlighter(); 81 | 82 | JPanel panel = new JPanel(); 83 | frame.getContentPane().add(panel); 84 | panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); 85 | 86 | JPanel row1 = new JPanel(new FlowLayout(FlowLayout.LEFT)); 87 | JLabel label1 = new JLabel("Search:"); 88 | label1.setPreferredSize(new Dimension(80, 20)); 89 | 90 | row1.add(label1); 91 | searchField = new JTextField(input, 20); 92 | row1.add(searchField); 93 | 94 | searchField.getDocument().addDocumentListener(documentListener); 95 | source.getDocument().addDocumentListener(documentListener); 96 | if (!input.isEmpty()) { 97 | highlightSearch(); 98 | } 99 | 100 | row1.add(regexCheck); 101 | row1.add(caseCheck); 102 | JButton searchButton = new JButton("Search"); 103 | row1.add(searchButton); 104 | JButton nextButton = new JButton("Next"); 105 | JButton prevButton = new JButton("Previous"); 106 | 107 | row1.add(prevButton); 108 | row1.add(nextButton); 109 | 110 | nextButton.addActionListener(e -> findNext()); 111 | prevButton.addActionListener(e -> findPrevious()); 112 | 113 | 114 | JPanel row2 = new JPanel(new FlowLayout(FlowLayout.LEFT)); 115 | JLabel label2 = new JLabel("Replace:"); 116 | label2.setPreferredSize(new Dimension(80, 20)); 117 | row2.add(label2); 118 | replaceField = new JTextField(20); 119 | row2.add(replaceField); 120 | JButton replaceButton = new JButton("Replace"); 121 | row2.add(replaceButton); 122 | row2.add(replaceAllButton); 123 | 124 | 125 | JPanel row3 = new JPanel(new FlowLayout(FlowLayout.LEFT)); 126 | row3.add(statusLabel); 127 | 128 | panel.add(row1); 129 | panel.add(row2); 130 | panel.add(row3); 131 | 132 | searchButton.addActionListener(e -> search()); 133 | caseCheck.addActionListener(e -> highlightSearch()); 134 | regexCheck.addActionListener(e -> highlightSearch()); 135 | replaceButton.addActionListener(e -> replaceOne()); 136 | replaceAllButton.addActionListener(e -> replaceAll()); 137 | 138 | frame.addWindowListener(new WindowAdapter() { 139 | @Override 140 | public void windowClosing(WindowEvent e) { 141 | removeHighlights(); 142 | } 143 | }); 144 | frame.pack(); 145 | 146 | Point mousePos = MouseInfo.getPointerInfo().getLocation(); 147 | mousePos.x += FRAME_MARGIN; 148 | mousePos.y += frame.getHeight() / 2; 149 | 150 | frame.setLocation(mousePos); 151 | frame.setVisible(true); 152 | } 153 | 154 | public void dispose() { 155 | if (frame != null) { 156 | frame.dispose(); 157 | } 158 | if (source != null) { 159 | if (highlighter != null) { 160 | removeHighlights(); 161 | } 162 | source.getDocument().removeDocumentListener(documentListener); 163 | } 164 | } 165 | 166 | private void removeHighlights() { 167 | Highlighter highlighter = source.getHighlighter(); 168 | Highlighter.Highlight[] highlights = highlighter.getHighlights(); 169 | 170 | for (Highlighter.Highlight h : highlights) { 171 | highlighter.removeHighlight(h); 172 | } 173 | } 174 | 175 | private String getContent() { 176 | return source.getText(); 177 | } 178 | 179 | private void search() { 180 | highlightSearch(); 181 | if (matchPositions.isEmpty()) return; 182 | scrollToCurrentMatch(); 183 | } 184 | 185 | private void highlightSearch() { 186 | removeHighlights(); 187 | matchPositions.clear(); 188 | currentMatchIndex = -1; 189 | 190 | String searchText = searchField.getText(); 191 | if (searchText.isEmpty()) return; 192 | 193 | Pattern pattern = buildPattern(searchText); 194 | Matcher matcher = pattern.matcher(getContent()); 195 | 196 | int count = 0; 197 | while (matcher.find()) { 198 | if (DEBUG) { 199 | HopLa.montoyaApi.logging().logToOutput("HighlightSearch: " + matcher.start() + " " + matcher.end()); 200 | } 201 | 202 | matchPositions.add(new int[]{matcher.start(), matcher.end()}); 203 | count++; 204 | } 205 | 206 | if (count > 0) { 207 | currentMatchIndex = 0; 208 | highlightMatches(); 209 | statusLabel.setText(count + " founds"); 210 | } else { 211 | statusLabel.setText("Not found"); 212 | } 213 | } 214 | 215 | private void highlightMatches() { 216 | boolean isDarkMode = this.api.userInterface().currentTheme() == Theme.DARK; 217 | Highlighter.HighlightPainter currentPainter = new HighlightPainter(this.api.userInterface().swingUtils().colorForHighLight( 218 | isDarkMode ? HighlightColor.CYAN : HighlightColor.ORANGE 219 | )); 220 | Highlighter.HighlightPainter otherPainter = new HighlightPainter(this.api.userInterface().swingUtils().colorForHighLight( 221 | isDarkMode ? HighlightColor.BLUE : HighlightColor.YELLOW 222 | )); 223 | 224 | removeHighlights(); 225 | for (int i = 0; i < matchPositions.size(); i++) { 226 | int[] pos = matchPositions.get(i); 227 | Highlighter.HighlightPainter painter = (i == currentMatchIndex) ? currentPainter : otherPainter; 228 | try { 229 | highlighter.addHighlight(pos[0], pos[1], painter); 230 | } catch (BadLocationException e) { 231 | HopLa.montoyaApi.logging().logToError("Highlighter add error: " + e.getMessage()); 232 | } 233 | } 234 | } 235 | 236 | private void findNext() { 237 | if (matchPositions.isEmpty()) return; 238 | currentMatchIndex = (currentMatchIndex + 1) % matchPositions.size(); 239 | highlightMatches(); 240 | scrollToCurrentMatch(); 241 | } 242 | 243 | private void findPrevious() { 244 | if (matchPositions.isEmpty()) return; 245 | currentMatchIndex = (currentMatchIndex - 1 + matchPositions.size()) % matchPositions.size(); 246 | highlightMatches(); 247 | scrollToCurrentMatch(); 248 | } 249 | 250 | private void scrollToCurrentMatch() { 251 | if (currentMatchIndex < 0 || currentMatchIndex >= matchPositions.size()) return; 252 | int[] pos = matchPositions.get(currentMatchIndex); 253 | source.setCaretPosition(pos[0]); 254 | source.requestFocusInWindow(); 255 | } 256 | 257 | private void replaceOne() { 258 | if (matchPositions.isEmpty() || currentMatchIndex < 0) { 259 | statusLabel.setText("Not found"); 260 | return; 261 | } 262 | 263 | int[] pos = matchPositions.get(currentMatchIndex); 264 | int start = pos[0]; 265 | int end = pos[1]; 266 | String replaceText = replaceField.getText(); 267 | 268 | String updated = getContent().substring(0, start) + replaceText + getContent().substring(end); 269 | removeHighlights(); 270 | updateSource(updated); 271 | SwingUtilities.invokeLater(this::highlightSearch); 272 | SwingUtilities.invokeLater(() -> { 273 | source.setCaretPosition(pos[0] + replaceText.length()); 274 | }); 275 | } 276 | 277 | private void updateSource(String content) { 278 | HttpRequest patched_request = HttpRequest.httpRequest(content); 279 | messageEditor.setRequest(patched_request); 280 | } 281 | 282 | private void replaceAll() { 283 | String searchText = searchField.getText(); 284 | String replaceText = replaceField.getText(); 285 | if (searchText.isEmpty()) return; 286 | 287 | Pattern pattern = buildPattern(searchText); 288 | Matcher matcher = pattern.matcher(getContent()); 289 | 290 | int count = 0; 291 | StringBuilder result = new StringBuilder(); 292 | while (matcher.find()) { 293 | matcher.appendReplacement(result, Matcher.quoteReplacement(replaceText)); 294 | count++; 295 | } 296 | matcher.appendTail(result); 297 | int pos = source.getCaretPosition(); 298 | if (count > 0) { 299 | removeHighlights(); 300 | updateSource(result.toString()); 301 | statusLabel.setText(count + " replaced"); 302 | SwingUtilities.invokeLater(this::highlightSearch); 303 | source.setCaretPosition(pos); 304 | } else { 305 | statusLabel.setText("Not found"); 306 | } 307 | } 308 | 309 | private Pattern buildPattern(String searchText) { 310 | int flags = caseCheck.isSelected() ? 0 : Pattern.CASE_INSENSITIVE; 311 | if (regexCheck.isSelected()) { 312 | return Pattern.compile(searchText, flags); 313 | } else { 314 | return Pattern.compile(Pattern.quote(searchText), flags); 315 | } 316 | } 317 | 318 | // Fix start line bug 319 | static class HighlightPainter implements Highlighter.HighlightPainter { 320 | private final Color color; 321 | 322 | public HighlightPainter(Color color) { 323 | this.color = color; 324 | } 325 | 326 | @Override 327 | public void paint(Graphics g, int p0, int p1, Shape bounds, JTextComponent c) { 328 | try { 329 | 330 | Rectangle2D r0 = c.modelToView2D(p0); 331 | Rectangle2D r1 = c.modelToView2D(p1); 332 | 333 | if (r0 == null || r1 == null) return; 334 | 335 | double x = r0.getX(); 336 | double y = r0.getY(); 337 | double width = r1.getX() - r0.getX(); 338 | double height = r0.getHeight(); 339 | 340 | if (width <= 0) width = 1; // fallback minimal 341 | 342 | Graphics2D g2 = (Graphics2D) g; 343 | g2.setColor(color); 344 | g2.fill(new Rectangle2D.Double(x, y, width, height)); 345 | } catch (BadLocationException ex) { 346 | HopLa.montoyaApi.logging().logToError("Highlighter painter paint: " + ex.getMessage()); 347 | 348 | } 349 | } 350 | } 351 | } -------------------------------------------------------------------------------- /src/main/java/com/hopla/AIChatPanel.java: -------------------------------------------------------------------------------- 1 | package com.hopla; 2 | 3 | import burp.api.montoya.ui.contextmenu.MessageEditorHttpRequestResponse; 4 | import com.hopla.ai.*; 5 | import org.commonmark.node.Node; 6 | import org.commonmark.parser.Parser; 7 | import org.commonmark.renderer.html.HtmlRenderer; 8 | 9 | import javax.swing.*; 10 | import javax.swing.text.BadLocationException; 11 | import javax.swing.text.DefaultEditorKit; 12 | import javax.swing.text.Document; 13 | import javax.swing.text.html.HTMLEditorKit; 14 | import javax.swing.text.html.StyleSheet; 15 | import java.awt.*; 16 | import java.awt.event.*; 17 | import java.net.URL; 18 | import java.time.LocalDateTime; 19 | import java.time.format.DateTimeFormatter; 20 | import java.util.ArrayList; 21 | import java.util.Map; 22 | import java.util.stream.Collectors; 23 | 24 | import static com.hopla.Constants.DEBUG_AI; 25 | import static com.hopla.Utils.*; 26 | 27 | public class AIChatPanel { 28 | private final static String REQUEST_PLACEHOLDER = "@request@"; 29 | private final static String RESPONSE_PLACEHOLDER = "@response@"; 30 | private final static String BUTTON_TEXT_SEND = "Ask"; 31 | private final static String BUTTON_CANCEL_SEND = "Cancel"; 32 | 33 | private final JLabel statusLabel = new JLabel(" "); 34 | private final AIConfiguration aiConfiguration; 35 | private final AIChats chats; 36 | private final HTMLEditorKit kit = new HTMLEditorKit(); 37 | private final StyleSheet styleSheet = new StyleSheet(); 38 | private final Parser parser = Parser.builder().build(); 39 | private final HtmlRenderer renderer = HtmlRenderer.builder().escapeHtml(true).sanitizeUrls(true).build(); 40 | private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); 41 | private JTextArea inputField; 42 | private JFrame frame; 43 | private JTextArea source; 44 | private JList chatsList; 45 | private JTextPane editorPane; 46 | private AIProviderType currentProvider; 47 | private AIProvider aiProvider; 48 | private JScrollPane scrollPane; 49 | 50 | public AIChatPanel(AIConfiguration aiConfiguration, AIChats chats) { 51 | this.aiConfiguration = aiConfiguration; 52 | this.chats = chats; 53 | if (aiConfiguration.isAIConfigured) { 54 | this.currentProvider = aiConfiguration.defaultChatProvider.type; 55 | } 56 | 57 | loadCss(); 58 | } 59 | 60 | public void show(MessageEditorHttpRequestResponse messageEditor, InputEvent event, String input) { 61 | if (frame != null) { 62 | frame.dispose(); 63 | } 64 | if (!aiConfiguration.isAIConfigured) { 65 | alert("AI is not configured"); 66 | return; 67 | } 68 | 69 | if (currentProvider == null) { 70 | this.currentProvider = aiConfiguration.defaultChatProvider.type; 71 | } 72 | 73 | this.source = (JTextArea) event.getSource(); 74 | frame = generateJFrame(); 75 | 76 | frame.addWindowListener(new WindowAdapter() { 77 | @Override 78 | public void windowClosing(WindowEvent e) { 79 | aiConfiguration.defaultChatProvider.cancelCurrentQuickActionRequest(); 80 | } 81 | }); 82 | 83 | JPanel panel = new JPanel(); 84 | panel.setLayout(new BorderLayout()); 85 | 86 | editorPane = new JTextPane(); 87 | editorPane.setEditable(false); 88 | editorPane.setOpaque(false); 89 | editorPane.setContentType("text/html"); 90 | editorPane.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, true); 91 | editorPane.setEditorKit(kit); 92 | Document doc = kit.createDefaultDocument(); 93 | editorPane.setDocument(doc); 94 | editorPane.setAlignmentX(Component.LEFT_ALIGNMENT); 95 | 96 | JPopupMenu contextMenu = new JPopupMenu(); 97 | contextMenu.add(new JMenuItem(new DefaultEditorKit.CopyAction())); 98 | contextMenu.add(new JMenuItem(new AbstractAction("Insert selection in editor") { 99 | public void actionPerformed(ActionEvent e) { 100 | if (editorPane.getSelectedText() != null) { 101 | source.insert(editorPane.getSelectedText(), source.getCaretPosition()); 102 | } 103 | 104 | } 105 | })); 106 | 107 | editorPane.addMouseListener(new MouseAdapter() { 108 | public void mousePressed(MouseEvent e) { 109 | if (e.isPopupTrigger()) show(e); 110 | } 111 | 112 | public void mouseReleased(MouseEvent e) { 113 | if (e.isPopupTrigger()) show(e); 114 | } 115 | 116 | private void show(MouseEvent e) { 117 | if (editorPane.getSelectedText() != null && !editorPane.getSelectedText().isEmpty()) { 118 | contextMenu.show(e.getComponent(), e.getX(), e.getY()); 119 | } 120 | } 121 | }); 122 | 123 | frame.addComponentListener(new ComponentAdapter() { 124 | @Override 125 | public void componentResized(ComponentEvent e) { 126 | int width = panel.getWidth() - 50; 127 | editorPane.setMaximumSize(new Dimension(width, Short.MAX_VALUE)); 128 | panel.repaint(); 129 | panel.invalidate(); 130 | } 131 | }); 132 | 133 | scrollPane = new JScrollPane(editorPane, 134 | JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, 135 | JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); 136 | scrollPane.getVerticalScrollBar().setUnitIncrement(4); 137 | 138 | panel.add(scrollPane, BorderLayout.CENTER); 139 | 140 | 141 | JPanel inputPanel = new JPanel(new BorderLayout()); 142 | inputPanel.add(statusLabel, BorderLayout.NORTH); 143 | 144 | inputField = new JTextArea(5, 20); 145 | inputField.setLineWrap(true); 146 | inputField.setWrapStyleWord(true); 147 | 148 | JScrollPane inputScrollPane = new JScrollPane(inputField); 149 | 150 | JButton sendButton = new JButton(BUTTON_TEXT_SEND); 151 | 152 | inputPanel.add(inputScrollPane, BorderLayout.CENTER); 153 | inputPanel.add(sendButton, BorderLayout.EAST); 154 | 155 | inputField.addKeyListener(new KeyAdapter() { 156 | @Override 157 | public void keyPressed(KeyEvent e) { 158 | if (e.getKeyCode() == KeyEvent.VK_ENTER && e.isControlDown()) { 159 | inputField.insert("\n", inputField.getCaretPosition()); 160 | e.consume(); 161 | return; 162 | } 163 | if (e.getKeyCode() == KeyEvent.VK_ENTER) { 164 | if (sendButton.getText().equals(BUTTON_TEXT_SEND)) { 165 | sendButton.doClick(); 166 | } 167 | e.consume(); 168 | return; 169 | } 170 | if (e.getKeyCode() == KeyEvent.VK_UP) { 171 | AIChats.Chat chat = getCurrentChat(); 172 | inputField.setText(chat.getLastUserMessage().getContent()); 173 | e.consume(); 174 | } 175 | } 176 | }); 177 | 178 | 179 | panel.add(inputPanel, BorderLayout.SOUTH); 180 | 181 | JPanel historyPanel = new JPanel(); 182 | historyPanel.setLayout(new BoxLayout(historyPanel, BoxLayout.Y_AXIS)); 183 | historyPanel.setPreferredSize(new Dimension(200, 500)); 184 | historyPanel.setMinimumSize(new Dimension(100, 0)); 185 | historyPanel.setMaximumSize(new Dimension(200, Integer.MAX_VALUE)); 186 | 187 | JButton buttonNewChat = new JButton("New chat"); 188 | buttonNewChat.setAlignmentX(Component.CENTER_ALIGNMENT); 189 | buttonNewChat.setMaximumSize(new Dimension(Integer.MAX_VALUE, buttonNewChat.getPreferredSize().height)); 190 | buttonNewChat.addActionListener(new ActionListener() { 191 | @Override 192 | public void actionPerformed(ActionEvent e) { 193 | chats.getChats().add(new AIChats.Chat(LocalDateTime.now().format(dateFormatter), new ArrayList<>())); 194 | loadChatList(); 195 | sendButton.setText(BUTTON_TEXT_SEND); 196 | statusLabel.setText(""); 197 | } 198 | }); 199 | historyPanel.add(buttonNewChat); 200 | 201 | DefaultListModel listModel = new DefaultListModel<>(); 202 | 203 | 204 | chatsList = new JList<>(listModel); 205 | chatsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 206 | chatsList.setLayoutOrientation(JList.VERTICAL); 207 | chatsList.setFixedCellWidth(200); 208 | historyPanel.add(chatsList); 209 | loadChatList(); 210 | 211 | 212 | final int[] clickedItem = new int[1]; 213 | JPopupMenu chatContextMenu = new JPopupMenu(); 214 | chatContextMenu.add(new JMenuItem(new AbstractAction("Delete chat") { 215 | public void actionPerformed(ActionEvent e) { 216 | int confirm = JOptionPane.showConfirmDialog( 217 | frame, 218 | "Delete ?", 219 | null, 220 | JOptionPane.YES_NO_OPTION 221 | ); 222 | if (confirm == JOptionPane.YES_OPTION) { 223 | chats.getChats().remove(chatsList.getModel().getSize() - 1 - clickedItem[0]); 224 | loadChatList(); 225 | } 226 | 227 | } 228 | })); 229 | 230 | chatsList.addMouseListener(new MouseAdapter() { 231 | @Override 232 | public void mouseClicked(MouseEvent e) { 233 | if (e.getClickCount() == 1) { 234 | int index = chatsList.locationToIndex(e.getPoint()); 235 | if (index != -1) { 236 | int chatIndex = chatsList.getModel().getSize() - 1 - index; 237 | loadChat(chats.getChats().get(chatIndex)); 238 | 239 | } 240 | } 241 | } 242 | 243 | @Override 244 | public void mousePressed(MouseEvent e) { 245 | if (e.isPopupTrigger()) show(e); 246 | } 247 | 248 | @Override 249 | public void mouseReleased(MouseEvent e) { 250 | if (e.isPopupTrigger()) show(e); 251 | } 252 | 253 | private void show(MouseEvent e) { 254 | clickedItem[0] = chatsList.getSelectedIndex(); 255 | chatContextMenu.show(e.getComponent(), e.getX(), e.getY()); 256 | } 257 | }); 258 | 259 | 260 | JScrollPane historyScroll = new JScrollPane(historyPanel); 261 | historyScroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); 262 | 263 | 264 | panel.setPreferredSize(new Dimension(800, 500)); 265 | frame.add(historyPanel, BorderLayout.WEST); 266 | frame.add(panel, BorderLayout.CENTER); 267 | inputField.setText(input); 268 | 269 | ActionListener sendAction = new ActionListener() { 270 | @Override 271 | public void actionPerformed(ActionEvent e) { 272 | 273 | if (sendButton.getText().equals(BUTTON_CANCEL_SEND) && aiProvider != null) { 274 | aiProvider.cancelCurrentChatRequest(); 275 | sendButton.setText(BUTTON_TEXT_SEND); 276 | statusLabel.setText("Cancelled"); 277 | return; 278 | } 279 | 280 | String userInput = inputField.getText().trim(); 281 | if (!userInput.isEmpty()) { 282 | 283 | userInput = userInput.replace(REQUEST_PLACEHOLDER, getRequest(messageEditor)); 284 | userInput = userInput.replace(RESPONSE_PLACEHOLDER, getResponse(messageEditor)); 285 | 286 | inputField.setText(""); 287 | statusLabel.setText("Thinking..."); 288 | 289 | AIChats.Chat chat = getCurrentChat(); 290 | AIChats.Message message = new AIChats.Message( 291 | AIChats.MessageRole.USER, 292 | userInput 293 | ); 294 | chat.addMessage(message); 295 | loadChat(chat); 296 | chats.save(); 297 | sendButton.setText(BUTTON_CANCEL_SEND); 298 | 299 | try { 300 | aiProvider = aiConfiguration.getChatProvider(currentProvider); 301 | AIChats.Message answer = new AIChats.Message( 302 | AIChats.MessageRole.ASSISTANT, 303 | "" 304 | ); 305 | chat.addMessage(answer); 306 | 307 | aiProvider.chat(chat, new AIProvider.StreamingCallback() { 308 | @Override 309 | public void onData(String chunk) { 310 | if (!chunk.isEmpty()) { 311 | chat.getLastMessage().appendContent(chunk); 312 | chats.save(); 313 | SwingUtilities.invokeLater(() -> { 314 | loadChat(chat); 315 | }); 316 | 317 | } 318 | } 319 | 320 | @Override 321 | public void onDone() { 322 | chats.save(); 323 | loadChat(chat); 324 | statusLabel.setText(""); 325 | sendButton.setText(BUTTON_TEXT_SEND); 326 | } 327 | 328 | @Override 329 | public void onError(String error) { 330 | statusLabel.setText(error); 331 | sendButton.setText(BUTTON_TEXT_SEND); 332 | } 333 | }); 334 | } catch (Exception exc) { 335 | alert("AI chat error: " + exc.getMessage()); 336 | HopLa.montoyaApi.logging().logToError("AI chat error: " + exc.getMessage()); 337 | } 338 | 339 | 340 | } 341 | } 342 | }; 343 | 344 | sendButton.addActionListener(sendAction); 345 | 346 | 347 | JPanel bottomBar = new JPanel(new BorderLayout()); 348 | bottomBar.setBorder(BorderFactory.createEmptyBorder(8, 10, 8, 10)); 349 | 350 | JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); 351 | 352 | 353 | JButton buttonRequest = new JButton(REQUEST_PLACEHOLDER); 354 | buttonRequest.addActionListener(new ActionListener() { 355 | @Override 356 | public void actionPerformed(ActionEvent e) { 357 | insertTextTextArea(REQUEST_PLACEHOLDER); 358 | } 359 | }); 360 | buttonPanel.add(buttonRequest); 361 | 362 | JButton buttonResponse = new JButton(RESPONSE_PLACEHOLDER); 363 | buttonResponse.addActionListener(new ActionListener() { 364 | @Override 365 | public void actionPerformed(ActionEvent e) { 366 | insertTextTextArea(RESPONSE_PLACEHOLDER); 367 | 368 | } 369 | }); 370 | buttonPanel.add(buttonResponse); 371 | 372 | JComboBox selectBox = new JComboBox<>(aiConfiguration.config.prompts.toArray(new LLMConfig.Prompt[0])); 373 | selectBox.addActionListener(e -> { 374 | int idx = selectBox.getSelectedIndex(); 375 | if (idx != -1) { 376 | insertTextTextArea(aiConfiguration.config.prompts.get(idx).content); 377 | 378 | } 379 | }); 380 | 381 | buttonPanel.add(selectBox); 382 | 383 | if (Constants.EXTERNAL_AI) { 384 | Map enabledProviders = aiConfiguration.config.providers.entrySet().stream() 385 | .filter(entry -> entry.getValue().enabled) 386 | .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 387 | 388 | JComboBox aiProviderSelectBox = new JComboBox<>(enabledProviders.keySet().toArray(new AIProviderType[0])); 389 | 390 | aiProviderSelectBox.addActionListener(e -> { 391 | AIProviderType selectedProvider = (AIProviderType) aiProviderSelectBox.getSelectedItem(); 392 | if (selectedProvider != null) { 393 | currentProvider = selectedProvider; 394 | if (DEBUG_AI) { 395 | HopLa.montoyaApi.logging().logToOutput("Provider selected:" + selectedProvider); 396 | } 397 | } 398 | }); 399 | 400 | aiProviderSelectBox.setSelectedItem(aiConfiguration.defaultChatProvider.type); 401 | buttonPanel.add(aiProviderSelectBox); 402 | } 403 | 404 | 405 | bottomBar.add(buttonPanel, BorderLayout.WEST); 406 | inputPanel.add(bottomBar, BorderLayout.SOUTH); 407 | 408 | frame.pack(); 409 | frame.setVisible(true); 410 | SwingUtilities.invokeLater(() -> inputField.requestFocusInWindow()); 411 | } 412 | 413 | private AIChats.Chat getCurrentChat() { 414 | return chats.getChats().get(chatsList.getModel().getSize() - 1 - chatsList.getSelectedIndex()); 415 | } 416 | 417 | private void loadChatList() { 418 | if (chats.getChats().isEmpty()) { 419 | chats.getChats().add(new AIChats.Chat(LocalDateTime.now().format(dateFormatter), new ArrayList<>())); 420 | chats.save(); 421 | } 422 | DefaultListModel listModel = new DefaultListModel<>(); 423 | for (AIChats.Chat item : chats.getChats().reversed()) { 424 | listModel.addElement(item.timestamp); 425 | } 426 | 427 | 428 | chatsList.setModel(listModel); 429 | 430 | if (!listModel.isEmpty()) { 431 | chatsList.setSelectedIndex(0); 432 | loadChat(chats.getChats().getLast()); 433 | } 434 | } 435 | 436 | private void loadChat(AIChats.Chat chat) { 437 | String m = ""; 438 | for (AIChats.Message message : chat.getMessages()) { 439 | String html = renderMarkdownToHtml(message.getContent()); 440 | m += "
" + 441 | "" + message.getRole().toString() + "" + 442 | html + 443 | "
"; 444 | } 445 | if (DEBUG_AI) { 446 | HopLa.montoyaApi.logging().logToOutput("AI chat html: " + m); 447 | 448 | } 449 | 450 | JScrollBar verticalBar = scrollPane.getVerticalScrollBar(); 451 | boolean atBottom = verticalBar.getValue() + verticalBar.getVisibleAmount() >= verticalBar.getMaximum() + 70; 452 | 453 | editorPane.setText(m); 454 | if (!atBottom) { 455 | editorPane.setCaretPosition(editorPane.getDocument().getLength()); 456 | } 457 | 458 | } 459 | 460 | private void loadCss() { 461 | URL cssUrl = getClass().getResource("/style.css"); 462 | styleSheet.importStyleSheet(cssUrl); 463 | kit.setStyleSheet(styleSheet); 464 | } 465 | 466 | private String renderMarkdownToHtml(String markdown) { 467 | Node document = parser.parse(markdown); 468 | String body = renderer.render(document); 469 | if (DEBUG_AI) { 470 | HopLa.montoyaApi.logging().logToError("AI chat html: " + body); 471 | } 472 | return body; 473 | } 474 | 475 | private void insertTextTextArea(String text) { 476 | int start = inputField.getCaretPosition(); 477 | int end = start; 478 | 479 | if (inputField.getSelectedText() != null && !inputField.getSelectedText().isEmpty()) { 480 | start = inputField.getSelectionStart(); 481 | end = inputField.getSelectionEnd(); 482 | } 483 | Document doc = inputField.getDocument(); 484 | try { 485 | doc.remove(start, end - start); 486 | doc.insertString(start, text, null); 487 | } catch (BadLocationException exc) { 488 | HopLa.montoyaApi.logging().logToError("AI chat insertion error: " + exc.getMessage()); 489 | } 490 | inputField.setCaretPosition(start + text.length()); 491 | } 492 | 493 | public void dispose() { 494 | if (frame != null) { 495 | frame.dispose(); 496 | } 497 | } 498 | 499 | } 500 | --------------------------------------------------------------------------------