diff --git a/.github/workflows/rigging_pr_description.yml b/.github/workflows/rigging_pr_description.yml index c936345..5bb8b8f 100644 --- a/.github/workflows/rigging_pr_description.yml +++ b/.github/workflows/rigging_pr_description.yml @@ -21,10 +21,11 @@ jobs: id: diff # shellcheck disable=SC2102 run: | - git fetch origin "${GITHUB_BASE_REF}" - MERGE_BASE="$(git merge-base HEAD "origin/${GITHUB_BASE_REF}")" - DIFF="$(git diff "${MERGE_BASE}" HEAD | base64 --wrap=0)" - echo "diff=${DIFF}" >> "${GITHUB_OUTPUT}" + git fetch origin "${{ github.base_ref }}" + MERGE_BASE=$(git merge-base HEAD "origin/${{ github.base_ref }}") + # Use separate diff arguments instead of range notation + DIFF=$(git diff "$MERGE_BASE" HEAD | base64 --wrap=0) + echo "diff=${DIFF}" >> "$GITHUB_OUTPUT" - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b #v5.0.3 with: python-version: "3.11" @@ -45,14 +46,6 @@ jobs: GIT_DIFF: ${{ steps.diff.outputs.diff }} run: | python .github/scripts/rigging_pr_decorator.py - # Extract PR body - - name: Extract PR body - id: pr - run: | - PR_BODY="$(gh pr view "${GITHUB_EVENT_PULL_REQUEST_NUMBER}" --json body --jq .body)" - echo "body=${PR_BODY}" >> "${GITHUB_OUTPUT}" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Update the PR description - name: Update PR Description uses: nefrob/pr-description@4dcc9f3ad5ec06b2a197c5f8f93db5e69d2fdca7 #v1.2.0 diff --git a/README.md b/README.md index e8ffba8..a768070 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Experimenting with yarrr' Burp Proxy tab going brrrrrrrrrrrrr. [![GitHub release (latest by date)](https://img.shields.io/github/v/release/dreadnode/burpference)](https://github.com/dreadnode/burpference/releases) [![GitHub stars](https://img.shields.io/github/stars/dreadnode/burpference?style=social)](https://github.com/dreadnode/burpference/stargazers) -[![GitHub license](https://img.shields.io/github/license/dreadnode/burpference)](https://github.com/dreadnode/burpference/blob/main/LICENSE) +[![GitHub license](https://img.shields.io/github/license/dreadnode/burpference)](https://img.shields.io/github/license/dreadnode/burpference) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/dreadnode/burpference/pulls) • @@ -40,6 +40,14 @@ Some key features: - Only in-scope items are sent, optimizing resource usage and avoiding unnecessary API calls. - By default, [certain MIME types are excluded](https://github.com/dreadnode/burpference/blob/7e81641e263bbdfe4a38e30746eb3c27f3454190/burpference/burpference.py#L616). - Color-coded tabs display `critical/high/medium/low/informational` findings from your model for easy visualization. +- **Scanner Analysis**: A dedicated scanner tab provides focused security analysis capabilities: + - Direct analysis of URLs and OpenAPI specifications + - Load the configuration files using the API adapter, the same as usual in burpference for efficient management of API keys/model selection etc + - Automated extraction of security headers and server information + - Real-time security header assessment (X-Frame-Options, CSP, HSTS, etc.) + - Custom system prompts for specialized analysis scenarios + - Support for both single-endpoint and full domain scanning + - Integration with Burp's native issue reporting system - **Comprehensive Logging**: A logging system allows you to review intercepted responses, API requests sent, and replies received—all clearly displayed for analysis. - A clean table interface displaying all logs, intercepted responses, API calls, and status codes for comprehensive engagement tracking. - Stores inference logs in both the "_Inference Logger_" tab as a live preview and a timestamped file in the /logs directory. diff --git a/burpference/burpference.py b/burpference/burpference.py index 864d4df..1f90299 100644 --- a/burpference/burpference.py +++ b/burpference/burpference.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # type: ignore[import] +from datetime import datetime from burp import IBurpExtender, ITab, IHttpListener, IScanIssue -from java.awt import BorderLayout, GridBagLayout, GridBagConstraints, Font +from java.awt import BorderLayout, GridBagLayout, GridBagConstraints, Font, Dimension from javax.swing import ( JPanel, JTextArea, JScrollPane, BorderFactory, JSplitPane, JButton, JComboBox, JTable, table, ListSelectionModel, JOptionPane, JTextField, JTabbedPane) +from javax.swing import BoxLayout, JLabel from javax.swing.table import DefaultTableCellRenderer, TableRowSorter from javax.swing.border import TitledBorder from java.util import Comparator import json import urllib2 import os -from datetime import datetime from consts import * from api_adapters import get_api_adapter from issues import BurpferenceIssue +from threading import Thread +from scanner import BurpferenceScanner def load_ascii_art(file_path): @@ -51,6 +54,8 @@ def __init__(self): self.temp_log_messages = [] self.request_counter = 0 self.log_message("Extension initialized and running.") + self._hosts = set() + self.scanner = None # Will initialize after callbacks def registerExtenderCallbacks(self, callbacks): self._callbacks = callbacks @@ -264,12 +269,35 @@ def compare(self, s1, s2): self._panel.add(diffSplitPane, BorderLayout.NORTH) self.inference_tab = self.create_inference_logger_tab() + self.scanner_tab = self.create_scanner_tab() + self.tabbedPane = JTabbedPane() self.tabbedPane.setBackground(DARK_BACKGROUND) self.tabbedPane.setForeground(DREADNODE_GREY) self.tabbedPane.addTab("burpference", self._panel) self.tabbedPane.addTab("Inference Logger", self.inference_tab) + # Initialize scanner AFTER loading config + self.scanner = None + + # Now initialize scanner with current config + colors = { + 'DARK_BACKGROUND': DARK_BACKGROUND, + 'LIGHTER_BACKGROUND': LIGHTER_BACKGROUND, + 'DREADNODE_GREY': DREADNODE_GREY, + 'DREADNODE_ORANGE': DREADNODE_ORANGE + } + self.scanner = BurpferenceScanner( + callbacks=self._callbacks, + helpers=self._helpers, + config=None, + api_adapter=None, + colors=colors + ) + + self.scanner_tab = self.scanner.create_scanner_tab() + self.tabbedPane.addTab("Scanner", self.scanner_tab) + for i in range(self.tabbedPane.getTabCount()): self.tabbedPane.setBackgroundAt(i, DREADNODE_GREY) self.tabbedPane.setForegroundAt(i, DREADNODE_ORANGE) @@ -333,33 +361,50 @@ def loadConfiguration(self, event): try: with open(config_path, 'r') as config_file: self.config = json.load(config_file) - self.log_message("Loaded configuration: %s" % - json.dumps(self.config, indent=2)) + self.config["config_file"] = selected_config + try: self.api_adapter = get_api_adapter(self.config) + if self.scanner: + self.scanner.config = self.config + self.scanner.api_adapter = self.api_adapter + self.scanner.update_config_display() self.log_message("API adapter initialized successfully") except ValueError as e: self.log_message("Error initializing API adapter: %s" % str(e)) self.api_adapter = None + if self.scanner: + self.scanner.api_adapter = None except Exception as e: self.log_message( "Unexpected error initializing API adapter: %s" % str(e)) self.api_adapter = None + if self.scanner: + self.scanner.api_adapter = None except ValueError as e: self.log_message( "Error parsing JSON in configuration file: %s" % str(e)) self.config = None self.api_adapter = None + if self.scanner: + self.scanner.config = None + self.scanner.api_adapter = None except Exception as e: self.log_message( "Unexpected error loading configuration: %s" % str(e)) self.config = None self.api_adapter = None + if self.scanner: + self.scanner.config = None + self.scanner.api_adapter = None else: self.log_message( "Configuration file %s not found." % selected_config) self.config = None self.api_adapter = None + if self.scanner: + self.scanner.config = None + self.scanner.api_adapter = None def create_inference_logger_tab(self): panel = JPanel(BorderLayout()) @@ -434,6 +479,133 @@ def create_inference_logger_tab(self): return panel + def create_scanner_tab(self): + """Creates the burpference scanner tab with domain filtering and direct model interaction""" + panel = JPanel() + panel.setLayout(BoxLayout(panel, BoxLayout.Y_AXIS)) + panel.setBackground(DARK_BACKGROUND) + + # Create top control panel + top_panel = JPanel() + top_panel.setLayout(BoxLayout(top_panel, BoxLayout.X_AXIS)) + top_panel.setBackground(DARK_BACKGROUND) + + # Domain selector + domain_panel = JPanel() + domain_panel.setBackground(DARK_BACKGROUND) + domain_label = JLabel("Target Domain:") + domain_label.setForeground(DREADNODE_GREY) + self._domain_selector = JComboBox(list(self._hosts)) + self._domain_selector.setBackground(LIGHTER_BACKGROUND) + self._domain_selector.setForeground(DREADNODE_GREY) + domain_panel.add(domain_label) + domain_panel.add(self._domain_selector) + top_panel.add(domain_panel) + + # Optional prompt input + middle_panel = JPanel() + middle_panel.setBackground(DARK_BACKGROUND) + middle_panel.setLayout(BoxLayout(middle_panel, BoxLayout.Y_AXIS)) + prompt_label = JLabel("Custom Analysis Prompt:") + prompt_label.setForeground(DREADNODE_GREY) + self._custom_prompt = JTextArea(5, 50) + self._custom_prompt.setLineWrap(True) + self._custom_prompt.setWrapStyleWord(True) + self._custom_prompt.setBackground(LIGHTER_BACKGROUND) + self._custom_prompt.setForeground(DREADNODE_ORANGE) + prompt_scroll = JScrollPane(self._custom_prompt) + + # Analyze button + analyze_button = JButton("Analyze Domain", actionPerformed=self.analyze_domain) + analyze_button.setBackground(DREADNODE_ORANGE) + analyze_button.setForeground(DREADNODE_GREY) + + middle_panel.add(prompt_label) + middle_panel.add(prompt_scroll) + middle_panel.add(analyze_button) + + # Results area + self._scanner_output = JTextArea(20, 50) + self._scanner_output.setEditable(False) + self._scanner_output.setLineWrap(True) + self._scanner_output.setWrapStyleWord(True) + self._scanner_output.setBackground(LIGHTER_BACKGROUND) + self._scanner_output.setForeground(DREADNODE_ORANGE) + scanner_scroll = JScrollPane(self._scanner_output) + + # Add all components + panel.add(top_panel) + panel.add(middle_panel) + panel.add(scanner_scroll) + + return panel + + def analyze_domain(self, event): + """Handles the domain analysis button click""" + domain = self._domain_selector.getSelectedItem() + custom_prompt = self._custom_prompt.getText() + + def run_analysis(): + self._scanner_output.setText("Analyzing domain: %s...\n" % domain) + try: + # Get all requests for selected domain + http_pairs = self.get_domain_traffic(domain) + if not http_pairs: + self._scanner_output.append("\nNo traffic found for domain.") + return + + # Use custom prompt if provided, otherwise use default + prompt = custom_prompt if custom_prompt else "Analyze this domain's traffic for security issues:" + + # Prepare and send to current model + analysis_request = self.api_adapter.prepare_request( + user_content=json.dumps(http_pairs, indent=2), + system_content=prompt + ) + + # Make request and process response + req = urllib2.Request(self.config.get("host", "")) + for header, value in self.config.get("headers", {}).items(): + req.add_header(header, value) + + response = urllib2.urlopen(req, json.dumps(analysis_request)) + response_data = response.read() + analysis = self.api_adapter.process_response(response_data) + + # Update UI + self._scanner_output.setText("Analysis for %s:\n\n%s" % (domain, analysis)) + + except Exception as e: + self._scanner_output.setText("Error analyzing domain: %s" % str(e)) + + # Run analysis in background thread + Thread(target=run_analysis).start() + + def get_domain_traffic(self, domain): + """Gets all traffic for a specific domain""" + traffic = [] + for message in self._callbacks.getProxyHistory(): + if domain in message.getHttpService().getHost(): + analyzed_request = self._helpers.analyzeRequest(message) + analyzed_response = self._helpers.analyzeResponse(message.getResponse()) + + # Extract request/response data + request_info = { + "method": analyzed_request.getMethod(), + "url": str(message.getUrl()), + "headers": dict(header.split(': ', 1) for header in analyzed_request.getHeaders()[1:] if ': ' in header), + "body": message.getRequest()[analyzed_request.getBodyOffset():].tostring() + } + + response_info = { + "status": analyzed_response.getStatusCode(), + "headers": dict(header.split(': ', 1) for header in analyzed_response.getHeaders()[1:] if ': ' in header), + "body": message.getResponse()[analyzed_response.getBodyOffset():].tostring() + } + + traffic.append({"request": request_info, "response": response_info}) + return traffic + def inferenceLogSelectionChanged(self, event): selectedRow = self.inferenceLogTable.getSelectedRow() if selectedRow != -1: @@ -451,9 +623,7 @@ def inferenceLogSelectionChanged(self, event): except (ValueError, TypeError): formatted_request = str(request) - # For response, try to extract the message content if it's a model response try: - # Handle case where response is already a dict if isinstance(response, dict): response_obj = response else: @@ -552,7 +722,7 @@ def getTableCellRendererComponent(self, table, value, isSelected, hasFocus, row, def log_message(self, message): timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - log_entry = "[{0}] {1}\n".format(timestamp, message) # Python2 format strings + log_entry = "[{0}] {1}\n".format(timestamp, message) if self.logArea is None: self.temp_log_messages.append(log_entry) @@ -562,12 +732,10 @@ def log_message(self, message): self.logArea.getDocument().getLength()) try: - # Try to create/write to log file with explicit permissions log_dir = os.path.dirname(self.log_file_path) if not os.path.exists(log_dir): - os.makedirs(log_dir, 0755) # Python2 octal notation + os.makedirs(log_dir, 0755) - # Open with explicit write permissions with open(self.log_file_path, 'a+') as log_file: log_file.write(log_entry) except (IOError, OSError) as e: @@ -629,7 +797,7 @@ def create_scan_issue(self, messageInfo, processed_response): detail = str(processed_response) if detail.startswith('"') and detail.endswith('"'): - detail = detail[1:-1] # Remove surrounding quotes + detail = detail[1:-1] # Create properly formatted issue name issue_name = "burpference: %s Security Finding" % severity @@ -651,6 +819,13 @@ def create_scan_issue(self, messageInfo, processed_response): self.log_message("Error creating scan issue: %s" % str(e)) def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo): + if messageIsRequest: + # Add new domains to both main extension and scanner + host = messageInfo.getHttpService().getHost() + if host not in self._hosts: + self._hosts.add(host) + if self.scanner: + self.scanner.add_host(host) if not self.is_running: return if not self.api_adapter: @@ -661,6 +836,13 @@ def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo): if messageIsRequest: # Store the request for later use self.current_request = messageInfo + host = messageInfo.getHttpService().getHost() + if host not in self._hosts: + self._hosts.add(host) + if hasattr(self, '_domain_selector'): + self._domain_selector.addItem(host) + if self.scanner: + self.scanner.add_host(host) else: request = self.current_request response = messageInfo @@ -799,7 +981,6 @@ def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo): self.requestArea.append("\n\n=== Request #" + str(self.request_counter) + " ===\n") try: - # Format the request nicely formatted_request = json.dumps(http_pair, indent=2) formatted_request = formatted_request.replace('\\n', '\n') formatted_request = formatted_request.replace('\\"', '"') @@ -810,7 +991,6 @@ def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo): self.responseArea.append("\n\n=== Response #" + str(self.request_counter) + " ===\n") try: - # Format the response nicely if isinstance(processed_response, dict) and 'message' in processed_response and 'content' in processed_response['message']: formatted_response = processed_response['message']['content'] else: diff --git a/burpference/scanner.py b/burpference/scanner.py new file mode 100644 index 0000000..c01d879 --- /dev/null +++ b/burpference/scanner.py @@ -0,0 +1,469 @@ +import os +from javax.swing import ( + JPanel, + JLabel, + JTextArea, + JScrollPane, + JButton, + JComboBox, + BoxLayout, + JTextField, + ButtonGroup, + JRadioButton, +) +from java.awt import BorderLayout, FlowLayout, Dimension +from java.lang import Short +from threading import Thread +import json +import urllib2 +import re +from datetime import datetime +from javax.swing.border import EmptyBorder + +SCANNER_PROMPT = os.path.join( + os.path.dirname(os.path.dirname(__file__)), "prompts", "scanner_prompt.txt" +) +OPENAPI_PROMPT = os.path.join( + os.path.dirname(os.path.dirname(__file__)), "prompts", "openapi_prompt.txt" +) + + +class BurpferenceScanner: + def __init__(self, callbacks, helpers, config, api_adapter, colors=None): + self._callbacks = callbacks + self._helpers = helpers + self.config = config + self.api_adapter = api_adapter + self._hosts = set() + self._last_prompt_content = None + self._last_openapi_content = None + + self.colors = colors or {} + self.DARK_BACKGROUND = self.colors.get("DARK_BACKGROUND") + self.LIGHTER_BACKGROUND = self.colors.get("LIGHTER_BACKGROUND") + self.DREADNODE_GREY = self.colors.get("DREADNODE_GREY") + self.DREADNODE_ORANGE = self.colors.get("DREADNODE_ORANGE") + + def add_host(self, host): + """Add a host to the scanner's tracked hosts""" + if host not in self._hosts: + self._hosts.add(host) + if hasattr(self, "_domain_selector"): + self._domain_selector.removeAllItems() + for h in sorted(self._hosts): + self._domain_selector.addItem(h) + + def create_scanner_tab(self): + """Creates the security analysis scanner tab""" + panel = JPanel() + panel.setLayout(BorderLayout()) + panel.setBackground(self.DARK_BACKGROUND) + + # Create a top container for config and input sections + top_container = JPanel() + top_container.setLayout(BoxLayout(top_container, BoxLayout.Y_AXIS)) + top_container.setBackground(self.DARK_BACKGROUND) + # Set maximum height for top container to keep it compact + top_container.setMaximumSize(Dimension(Short.MAX_VALUE, 300)) + + # Config info panel + config_info_panel = JPanel(FlowLayout(FlowLayout.LEFT, 5, 2)) + config_info_panel.setBackground(self.DARK_BACKGROUND) + self.config_label = JLabel(self.get_config_status()) + self.config_label.setForeground(self.DREADNODE_ORANGE) + config_info_panel.add(self.config_label) + config_info_panel.setMaximumSize(Dimension(Short.MAX_VALUE, 25)) + + # Target input section + target_panel = JPanel(FlowLayout(FlowLayout.LEFT, 5, 2)) + target_panel.setBackground(self.DARK_BACKGROUND) + + config_info_panel = JPanel(FlowLayout(FlowLayout.LEFT)) + config_info_panel.setBackground(self.DARK_BACKGROUND) + self.config_label = JLabel(self.get_config_status()) + self.config_label.setForeground(self.DREADNODE_ORANGE) + config_info_panel.add(self.config_label) + + # Input type selection + type_panel = JPanel() + type_panel.setBackground(self.DARK_BACKGROUND) + type_group = ButtonGroup() + + self.url_radio = JRadioButton("URL", True) + self.url_radio.setBackground(self.DARK_BACKGROUND) + self.url_radio.setForeground(self.DREADNODE_ORANGE) + + self.openapi_radio = JRadioButton("OpenAPI URL", False) + self.openapi_radio.setBackground(self.DARK_BACKGROUND) + self.openapi_radio.setForeground(self.DREADNODE_ORANGE) + + type_group.add(self.url_radio) + type_group.add(self.openapi_radio) + type_panel.add(self.url_radio) + type_panel.add(self.openapi_radio) + + # Target input + target_label = JLabel("Target:") + target_label.setForeground(self.DREADNODE_GREY) + self._target_input = JTextField(40) + self._target_input.setBackground(self.LIGHTER_BACKGROUND) + self._target_input.setForeground(self.DREADNODE_ORANGE) + + target_panel.add(type_panel) + target_panel.add(target_label) + target_panel.add(self._target_input) + target_panel.setMaximumSize(Dimension(Short.MAX_VALUE, 35)) + + # System prompt panel + prompt_panel = JPanel() + prompt_panel.setLayout(BoxLayout(prompt_panel, BoxLayout.Y_AXIS)) + prompt_panel.setBackground(self.DARK_BACKGROUND) + + prompt_desc = JLabel( + "Current system prompt for analysis. Modifies the model's behavior and focus areas." + ) + prompt_desc.setForeground(self.DREADNODE_GREY) + prompt_label = JLabel("System Prompt:") + prompt_label.setForeground(self.DREADNODE_GREY) + + self._custom_prompt = JTextArea(8, 50) + self._custom_prompt.setLineWrap(True) + self._custom_prompt.setWrapStyleWord(True) + self._custom_prompt.setBackground(self.LIGHTER_BACKGROUND) + self._custom_prompt.setForeground(self.DREADNODE_ORANGE) + + # Load and set initial prompt content + initial_prompt = self.load_prompt_template(self.openapi_radio.isSelected()) + if initial_prompt: + self._custom_prompt.setText(initial_prompt) + # Store as last content so we can detect changes + if self.openapi_radio.isSelected(): + self._last_openapi_content = initial_prompt + else: + self._last_prompt_content = initial_prompt + + prompt_scroll = JScrollPane(self._custom_prompt) + prompt_scroll.setPreferredSize(Dimension(600, 200)) + prompt_scroll.setMaximumSize(Dimension(Short.MAX_VALUE, 200)) + + # Button panel + button_panel = JPanel(FlowLayout(FlowLayout.LEFT)) + button_panel.setBackground(self.DARK_BACKGROUND) + scan_button = JButton( + "Start Security Analysis", actionPerformed=self.analyze_target + ) + scan_button.setBackground(self.DREADNODE_ORANGE) + scan_button.setForeground(self.DREADNODE_GREY) + button_panel.add(scan_button) + button_panel.setMaximumSize(Dimension(Short.MAX_VALUE, 35)) + + # Results area - should take most space + self._scanner_output = JTextArea(20, 50) + self._scanner_output.setEditable(False) + self._scanner_output.setLineWrap(True) + self._scanner_output.setWrapStyleWord(True) + self._scanner_output.setBackground(self.LIGHTER_BACKGROUND) + self._scanner_output.setForeground(self.DREADNODE_ORANGE) + scanner_scroll = JScrollPane(self._scanner_output) + + # Layout + prompt_panel.add(prompt_desc) + prompt_panel.add(prompt_label) + + top_container.add(config_info_panel) + top_container.add(target_panel) + top_container.add(prompt_panel) + top_container.add(prompt_scroll) + top_container.add(button_panel) + + panel.add(top_container, BorderLayout.NORTH) + panel.add(scanner_scroll, BorderLayout.CENTER) + + return panel + + def analyze_target(self, event): + """Handles the security analysis request""" + if not self.api_adapter: + self._scanner_output.setText( + "Error: No API adapter configured. Debug info:\n" + "Config: %s\nAPI Adapter: %s" + % ( + json.dumps(self.config, indent=2) if self.config else "None", + str(self.api_adapter), + ) + ) + return + + if not self.config: + self._scanner_output.setText( + "Error: No configuration loaded. Please select a configuration file first." + ) + return + + target = self._target_input.getText().strip() + if not target: + self._scanner_output.setText("Please enter a target URL") + return + + is_openapi = self.openapi_radio.isSelected() + custom_prompt = self._custom_prompt.getText().strip() + + def run_analysis(): + self._scanner_output.setText( + "Analyzing %s %s...\n" % ("OpenAPI at" if is_openapi else "", target) + ) + try: + if is_openapi: + content = self.fetch_openapi_spec(target) + if not content: + return + prompt = ( + custom_prompt + if custom_prompt + else "Analyze this OpenAPI specification for security vulnerabilities, authentication issues, and potential misconfigurations:" + ) + else: + content = self.analyze_url(target) + if not content: + return + prompt = ( + custom_prompt if custom_prompt else self.load_prompt_template() + ) + + # Prepare request for the model + analysis_request = self.api_adapter.prepare_request( + user_content=json.dumps(content, indent=2), system_content=prompt + ) + + # Send to configured model + req = urllib2.Request(self.config.get("host", "")) + for header, value in self.config.get("headers", {}).items(): + req.add_header(header, value) + + response = urllib2.urlopen(req, json.dumps(analysis_request)) + response_data = response.read() + analysis = self.api_adapter.process_response(response_data) + + # Log to inference logger if available + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + try: + extension_listeners = self._callbacks.getExtensionStateListeners() + if extension_listeners and len(extension_listeners) > 0: + main_extension = extension_listeners[0] + if hasattr(main_extension, "inferenceLogTableModel"): + main_extension.inferenceLogTableModel.addRow( + [ + timestamp, + self.config.get("host", ""), + json.dumps(analysis_request), + analysis, + "Success", + ] + ) + except Exception as e: + self._callbacks.printOutput( + "Warning: Could not log to inference logger: %s" % str(e) + ) + + # Create scanner issue for findings + if ( + "**CRITICAL**" in analysis + or "**HIGH**" in analysis + or "**MEDIUM**" in analysis + or "**LOW**" in analysis + or "**INFORMATIONAL**" in analysis + ): + from issues import BurpferenceIssue + from java.net import URL + + severity = "Information" + for level in ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFORMATIONAL"]: + if "**%s**" % level in analysis: + severity = ( + "High" + if level in ["CRITICAL", "HIGH"] + else level.capitalize() + ) + break + + url = URL(target) + protocol = url.getProtocol() + host = url.getHost() + port = url.getPort() + if port == -1: # No port specified + port = 443 if protocol == "https" else 80 + + issue = BurpferenceIssue( + httpService=self._helpers.buildHttpService( + host, port, protocol == "https" + ), + url=url, + httpMessages=[], + name="burpference Scanner: %s Finding" % level, + detail=analysis, + severity=severity, + confidence="Certain", + ) + self._callbacks.addScanIssue(issue) + + self._scanner_output.setText( + "Security Analysis for %s:\n\n%s" % (target, analysis) + ) + + except Exception as e: + error_msg = "Error during analysis: %s" % str(e) + self._scanner_output.setText(error_msg) + self._callbacks.printOutput(error_msg) + try: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + extension_listeners = self._callbacks.getExtensionStateListeners() + if extension_listeners and len(extension_listeners) > 0: + main_extension = extension_listeners[0] + if hasattr(main_extension, "inferenceLogTableModel"): + main_extension.inferenceLogTableModel.addRow( + [ + timestamp, + self.config.get("host", ""), + "Scanner Analysis Request", + "Error: %s" % str(e), + "Failed", + ] + ) + except Exception as log_error: + self._callbacks.printOutput( + "Warning: Could not log error to inference logger: %s" + % str(log_error) + ) + + Thread(target=run_analysis).start() + + def fetch_openapi_spec(self, url): + """Fetches and validates OpenAPI specification""" + try: + req = urllib2.Request(url) + response = urllib2.urlopen(req) + content = response.read() + + try: + spec = json.loads(content) + if not any(key in spec for key in ["swagger", "openapi"]): + self._scanner_output.setText( + "Invalid OpenAPI specification: Missing version identifier" + ) + return None + return spec + except ValueError: + self._scanner_output.setText( + "Invalid OpenAPI specification: Not valid JSON" + ) + return None + + except Exception as e: + self._scanner_output.setText("Error fetching OpenAPI spec: %s" % str(e)) + return None + + def analyze_url(self, url): + """Analyzes a URL for security assessment""" + try: + if not re.match(r"^https?://", url): + url = "http://" + url + + req = urllib2.Request(url) + response = urllib2.urlopen(req) + + # Gather information about the target + headers = dict(response.info().items()) + content = response.read() + + security_info = { + "url": url, + "status_code": response.getcode(), + "headers": headers, + "server_info": headers.get("server", "Unknown"), + "security_headers": { + "x-frame-options": headers.get("x-frame-options", "Not Set"), + "content-security-policy": headers.get( + "content-security-policy", "Not Set" + ), + "strict-transport-security": headers.get( + "strict-transport-security", "Not Set" + ), + "x-xss-protection": headers.get("x-xss-protection", "Not Set"), + "x-content-type-options": headers.get( + "x-content-type-options", "Not Set" + ), + }, + "response_size": len(content), + } + + return security_info + + except Exception as e: + self._scanner_output.setText("Error analyzing URL: {str(e)}") + return None + + def load_prompt_template(self, is_openapi=False): + """Load the appropriate prompt template""" + try: + prompt_file = OPENAPI_PROMPT if is_openapi else SCANNER_PROMPT + if os.path.exists(prompt_file): + with open(prompt_file, "r") as f: + content = f.read().strip() + # Track the last loaded content separately for each type + if is_openapi: + if content != self._last_openapi_content: + self._callbacks.printOutput( + "Loaded OpenAPI prompt template" + ) + self._last_openapi_content = content + else: + if content != self._last_prompt_content: + self._callbacks.printOutput( + "Loaded scanner prompt template" + ) + self._last_prompt_content = content + return content + else: + if not hasattr(self, "_prompt_missing_logged"): + self._callbacks.printOutput("Prompt file not found: {prompt_file}") + self._prompt_missing_logged = True + return ( + "Analyze this target for security issues:" + if not is_openapi + else "Analyze this OpenAPI specification for security vulnerabilities:" + ) + except Exception as e: + self._callbacks.printOutput("Error loading prompt: {str(e)}") + return "Analyze for security vulnerabilities:" + + def get_config_status(self): + """Get formatted configuration status""" + if not self.config: + return "No configuration loaded - Select a configuration file in the burpference tab" + + config_name = os.path.basename(self.config.get("config_file", "No config")) + model_name = self.config.get("model", "Not set") + api_type = self.config.get("api_type", "Not set") + + return "Configuration: %s | Model: %s | API: %s" % ( + config_name, + model_name, + api_type, + ) + + def refresh_prompt_template(self): + """Refresh the prompt template when config changes""" + if hasattr(self, "_custom_prompt"): + initial_prompt = self.load_prompt_template(self.openapi_radio.isSelected()) + if initial_prompt: + self._custom_prompt.setText(initial_prompt) + if self.openapi_radio.isSelected(): + self._last_openapi_content = initial_prompt + else: + self._last_prompt_content = initial_prompt + + def update_config_display(self): + """Update the configuration display and refresh prompt""" + if hasattr(self, "config_label"): + self.config_label.setText(self.get_config_status()) + self.refresh_prompt_template() diff --git a/prompts/openapi_prompt.txt b/prompts/openapi_prompt.txt new file mode 100644 index 0000000..295546d --- /dev/null +++ b/prompts/openapi_prompt.txt @@ -0,0 +1,26 @@ +You are conducting a security assessment of an OpenAPI/Swagger specification. Analyze this API documentation for potential security issues, design flaws, and best practice violations. + +Key areas to examine: +- Authentication & Authorization Schemes +- API Endpoint Security +- Data Validation Requirements +- Security Definitions +- CORS and Access Controls +- Rate Limiting Configurations +- Data Exposure Risks +- Schema Security Issues + +Provide your findings with clear severity levels using the following format (case-sensitive): +- "**CRITICAL**" for critical security design flaws +- "**HIGH**" for high-risk API vulnerabilities +- "**MEDIUM**" for moderate security concerns +- "**LOW**" for minor security improvements +- "**INFORMATIONAL**" for best practice suggestions + +For each finding include: +1. Description of the security concern +2. Affected endpoints or components +3. Potential impact +4. Recommended security improvements + +The OpenAPI specification for analysis follows: diff --git a/prompts/scanner_prompt.txt b/prompts/scanner_prompt.txt new file mode 100644 index 0000000..b9436e0 --- /dev/null +++ b/prompts/scanner_prompt.txt @@ -0,0 +1,23 @@ +You are a specialized web application security scanner focused on comprehensive security analysis of web applications and APIs. Your task is to examine the provided information for potential security vulnerabilities, misconfigurations, and architectural weaknesses. + +Key areas of focus: +- Architecture & Configuration Review +- Security Headers Analysis +- Authentication & Authorization +- API Security Best Practices +- Common Web Vulnerabilities +- Infrastructure Security + +Provide your analysis with clear severity levels using the following format (case-sensitive): +- "**CRITICAL**" for critical security issues +- "**HIGH**" for high-risk vulnerabilities +- "**MEDIUM**" for medium-risk issues +- "**LOW**" for low-risk findings +- "**INFORMATIONAL**" for security observations + +Be specific in your findings and include: +1. Clear description of each issue +2. Technical impact +3. Remediation recommendations where applicable + +The target information for analysis is provided below this line: