Skip to content

Commit c5afa88

Browse files
feat: burpference recon scanner for remote domains (#11)
* feat: burpference recon scanner for remote domains * fix: broken pr workflow from last pr * fix: fixup self hosts errors * chore: separate prompts like the proxy * feat: preload default prompt for context * chore: ensure configs are updated when switched * chore: remove test default load first config * feat: add scan results to inference logger and burp issues * docs: add supporting scanner doc content
1 parent a6ad7c6 commit c5afa88

6 files changed

Lines changed: 725 additions & 26 deletions

File tree

.github/workflows/rigging_pr_description.yml

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ jobs:
2121
id: diff
2222
# shellcheck disable=SC2102
2323
run: |
24-
git fetch origin "${GITHUB_BASE_REF}"
25-
MERGE_BASE="$(git merge-base HEAD "origin/${GITHUB_BASE_REF}")"
26-
DIFF="$(git diff "${MERGE_BASE}" HEAD | base64 --wrap=0)"
27-
echo "diff=${DIFF}" >> "${GITHUB_OUTPUT}"
24+
git fetch origin "${{ github.base_ref }}"
25+
MERGE_BASE=$(git merge-base HEAD "origin/${{ github.base_ref }}")
26+
# Use separate diff arguments instead of range notation
27+
DIFF=$(git diff "$MERGE_BASE" HEAD | base64 --wrap=0)
28+
echo "diff=${DIFF}" >> "$GITHUB_OUTPUT"
2829
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b #v5.0.3
2930
with:
3031
python-version: "3.11"
@@ -45,14 +46,6 @@ jobs:
4546
GIT_DIFF: ${{ steps.diff.outputs.diff }}
4647
run: |
4748
python .github/scripts/rigging_pr_decorator.py
48-
# Extract PR body
49-
- name: Extract PR body
50-
id: pr
51-
run: |
52-
PR_BODY="$(gh pr view "${GITHUB_EVENT_PULL_REQUEST_NUMBER}" --json body --jq .body)"
53-
echo "body=${PR_BODY}" >> "${GITHUB_OUTPUT}"
54-
env:
55-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5649
# Update the PR description
5750
- name: Update PR Description
5851
uses: nefrob/pr-description@4dcc9f3ad5ec06b2a197c5f8f93db5e69d2fdca7 #v1.2.0

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Experimenting with yarrr' Burp Proxy tab going brrrrrrrrrrrrr.
66

77
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/dreadnode/burpference)](https://github.com/dreadnode/burpference/releases)
88
[![GitHub stars](https://img.shields.io/github/stars/dreadnode/burpference?style=social)](https://github.com/dreadnode/burpference/stargazers)
9-
[![GitHub license](https://img.shields.io/github/license/dreadnode/burpference)](https://github.com/dreadnode/burpference/blob/main/LICENSE)
9+
[![GitHub license](https://img.shields.io/github/license/dreadnode/burpference)](https://img.shields.io/github/license/dreadnode/burpference)
1010
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/dreadnode/burpference/pulls)
1111

1212
@@ -40,6 +40,14 @@ Some key features:
4040
- Only in-scope items are sent, optimizing resource usage and avoiding unnecessary API calls.
4141
- By default, [certain MIME types are excluded](https://github.com/dreadnode/burpference/blob/7e81641e263bbdfe4a38e30746eb3c27f3454190/burpference/burpference.py#L616).
4242
- Color-coded tabs display `critical/high/medium/low/informational` findings from your model for easy visualization.
43+
- **Scanner Analysis**: A dedicated scanner tab provides focused security analysis capabilities:
44+
- Direct analysis of URLs and OpenAPI specifications
45+
- Load the configuration files using the API adapter, the same as usual in burpference for efficient management of API keys/model selection etc
46+
- Automated extraction of security headers and server information
47+
- Real-time security header assessment (X-Frame-Options, CSP, HSTS, etc.)
48+
- Custom system prompts for specialized analysis scenarios
49+
- Support for both single-endpoint and full domain scanning
50+
- Integration with Burp's native issue reporting system
4351
- **Comprehensive Logging**: A logging system allows you to review intercepted responses, API requests sent, and replies received—all clearly displayed for analysis.
4452
- A clean table interface displaying all logs, intercepted responses, API calls, and status codes for comprehensive engagement tracking.
4553
- Stores inference logs in both the "_Inference Logger_" tab as a live preview and a timestamped file in the /logs directory.

burpference/burpference.py

Lines changed: 193 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
# -*- coding: utf-8 -*-
22
# type: ignore[import]
3+
from datetime import datetime
34
from burp import IBurpExtender, ITab, IHttpListener, IScanIssue
4-
from java.awt import BorderLayout, GridBagLayout, GridBagConstraints, Font
5+
from java.awt import BorderLayout, GridBagLayout, GridBagConstraints, Font, Dimension
56
from javax.swing import (
67
JPanel, JTextArea, JScrollPane,
78
BorderFactory, JSplitPane, JButton, JComboBox,
89
JTable, table, ListSelectionModel, JOptionPane, JTextField, JTabbedPane)
10+
from javax.swing import BoxLayout, JLabel
911
from javax.swing.table import DefaultTableCellRenderer, TableRowSorter
1012
from javax.swing.border import TitledBorder
1113
from java.util import Comparator
1214
import json
1315
import urllib2
1416
import os
15-
from datetime import datetime
1617
from consts import *
1718
from api_adapters import get_api_adapter
1819
from issues import BurpferenceIssue
20+
from threading import Thread
21+
from scanner import BurpferenceScanner
1922

2023

2124
def load_ascii_art(file_path):
@@ -51,6 +54,8 @@ def __init__(self):
5154
self.temp_log_messages = []
5255
self.request_counter = 0
5356
self.log_message("Extension initialized and running.")
57+
self._hosts = set()
58+
self.scanner = None # Will initialize after callbacks
5459

5560
def registerExtenderCallbacks(self, callbacks):
5661
self._callbacks = callbacks
@@ -264,12 +269,35 @@ def compare(self, s1, s2):
264269
self._panel.add(diffSplitPane, BorderLayout.NORTH)
265270

266271
self.inference_tab = self.create_inference_logger_tab()
272+
self.scanner_tab = self.create_scanner_tab()
273+
267274
self.tabbedPane = JTabbedPane()
268275
self.tabbedPane.setBackground(DARK_BACKGROUND)
269276
self.tabbedPane.setForeground(DREADNODE_GREY)
270277
self.tabbedPane.addTab("burpference", self._panel)
271278
self.tabbedPane.addTab("Inference Logger", self.inference_tab)
272279

280+
# Initialize scanner AFTER loading config
281+
self.scanner = None
282+
283+
# Now initialize scanner with current config
284+
colors = {
285+
'DARK_BACKGROUND': DARK_BACKGROUND,
286+
'LIGHTER_BACKGROUND': LIGHTER_BACKGROUND,
287+
'DREADNODE_GREY': DREADNODE_GREY,
288+
'DREADNODE_ORANGE': DREADNODE_ORANGE
289+
}
290+
self.scanner = BurpferenceScanner(
291+
callbacks=self._callbacks,
292+
helpers=self._helpers,
293+
config=None,
294+
api_adapter=None,
295+
colors=colors
296+
)
297+
298+
self.scanner_tab = self.scanner.create_scanner_tab()
299+
self.tabbedPane.addTab("Scanner", self.scanner_tab)
300+
273301
for i in range(self.tabbedPane.getTabCount()):
274302
self.tabbedPane.setBackgroundAt(i, DREADNODE_GREY)
275303
self.tabbedPane.setForegroundAt(i, DREADNODE_ORANGE)
@@ -333,33 +361,50 @@ def loadConfiguration(self, event):
333361
try:
334362
with open(config_path, 'r') as config_file:
335363
self.config = json.load(config_file)
336-
self.log_message("Loaded configuration: %s" %
337-
json.dumps(self.config, indent=2))
364+
self.config["config_file"] = selected_config
365+
338366
try:
339367
self.api_adapter = get_api_adapter(self.config)
368+
if self.scanner:
369+
self.scanner.config = self.config
370+
self.scanner.api_adapter = self.api_adapter
371+
self.scanner.update_config_display()
340372
self.log_message("API adapter initialized successfully")
341373
except ValueError as e:
342374
self.log_message("Error initializing API adapter: %s" % str(e))
343375
self.api_adapter = None
376+
if self.scanner:
377+
self.scanner.api_adapter = None
344378
except Exception as e:
345379
self.log_message(
346380
"Unexpected error initializing API adapter: %s" % str(e))
347381
self.api_adapter = None
382+
if self.scanner:
383+
self.scanner.api_adapter = None
348384
except ValueError as e:
349385
self.log_message(
350386
"Error parsing JSON in configuration file: %s" % str(e))
351387
self.config = None
352388
self.api_adapter = None
389+
if self.scanner:
390+
self.scanner.config = None
391+
self.scanner.api_adapter = None
353392
except Exception as e:
354393
self.log_message(
355394
"Unexpected error loading configuration: %s" % str(e))
356395
self.config = None
357396
self.api_adapter = None
397+
if self.scanner:
398+
self.scanner.config = None
399+
self.scanner.api_adapter = None
358400
else:
359401
self.log_message(
360402
"Configuration file %s not found." % selected_config)
361403
self.config = None
362404
self.api_adapter = None
405+
if self.scanner:
406+
self.scanner.config = None
407+
self.scanner.api_adapter = None
363408

364409
def create_inference_logger_tab(self):
365410
panel = JPanel(BorderLayout())
@@ -434,6 +479,133 @@ def create_inference_logger_tab(self):
434479

435480
return panel
436481

482+
def create_scanner_tab(self):
483+
"""Creates the burpference scanner tab with domain filtering and direct model interaction"""
484+
panel = JPanel()
485+
panel.setLayout(BoxLayout(panel, BoxLayout.Y_AXIS))
486+
panel.setBackground(DARK_BACKGROUND)
487+
488+
# Create top control panel
489+
top_panel = JPanel()
490+
top_panel.setLayout(BoxLayout(top_panel, BoxLayout.X_AXIS))
491+
top_panel.setBackground(DARK_BACKGROUND)
492+
493+
# Domain selector
494+
domain_panel = JPanel()
495+
domain_panel.setBackground(DARK_BACKGROUND)
496+
domain_label = JLabel("Target Domain:")
497+
domain_label.setForeground(DREADNODE_GREY)
498+
self._domain_selector = JComboBox(list(self._hosts))
499+
self._domain_selector.setBackground(LIGHTER_BACKGROUND)
500+
self._domain_selector.setForeground(DREADNODE_GREY)
501+
domain_panel.add(domain_label)
502+
domain_panel.add(self._domain_selector)
503+
top_panel.add(domain_panel)
504+
505+
# Optional prompt input
506+
middle_panel = JPanel()
507+
middle_panel.setBackground(DARK_BACKGROUND)
508+
middle_panel.setLayout(BoxLayout(middle_panel, BoxLayout.Y_AXIS))
509+
prompt_label = JLabel("Custom Analysis Prompt:")
510+
prompt_label.setForeground(DREADNODE_GREY)
511+
self._custom_prompt = JTextArea(5, 50)
512+
self._custom_prompt.setLineWrap(True)
513+
self._custom_prompt.setWrapStyleWord(True)
514+
self._custom_prompt.setBackground(LIGHTER_BACKGROUND)
515+
self._custom_prompt.setForeground(DREADNODE_ORANGE)
516+
prompt_scroll = JScrollPane(self._custom_prompt)
517+
518+
# Analyze button
519+
analyze_button = JButton("Analyze Domain", actionPerformed=self.analyze_domain)
520+
analyze_button.setBackground(DREADNODE_ORANGE)
521+
analyze_button.setForeground(DREADNODE_GREY)
522+
523+
middle_panel.add(prompt_label)
524+
middle_panel.add(prompt_scroll)
525+
middle_panel.add(analyze_button)
526+
527+
# Results area
528+
self._scanner_output = JTextArea(20, 50)
529+
self._scanner_output.setEditable(False)
530+
self._scanner_output.setLineWrap(True)
531+
self._scanner_output.setWrapStyleWord(True)
532+
self._scanner_output.setBackground(LIGHTER_BACKGROUND)
533+
self._scanner_output.setForeground(DREADNODE_ORANGE)
534+
scanner_scroll = JScrollPane(self._scanner_output)
535+
536+
# Add all components
537+
panel.add(top_panel)
538+
panel.add(middle_panel)
539+
panel.add(scanner_scroll)
540+
541+
return panel
542+
543+
def analyze_domain(self, event):
544+
"""Handles the domain analysis button click"""
545+
domain = self._domain_selector.getSelectedItem()
546+
custom_prompt = self._custom_prompt.getText()
547+
548+
def run_analysis():
549+
self._scanner_output.setText("Analyzing domain: %s...\n" % domain)
550+
try:
551+
# Get all requests for selected domain
552+
http_pairs = self.get_domain_traffic(domain)
553+
if not http_pairs:
554+
self._scanner_output.append("\nNo traffic found for domain.")
555+
return
556+
557+
# Use custom prompt if provided, otherwise use default
558+
prompt = custom_prompt if custom_prompt else "Analyze this domain's traffic for security issues:"
559+
560+
# Prepare and send to current model
561+
analysis_request = self.api_adapter.prepare_request(
562+
user_content=json.dumps(http_pairs, indent=2),
563+
system_content=prompt
564+
)
565+
566+
# Make request and process response
567+
req = urllib2.Request(self.config.get("host", ""))
568+
for header, value in self.config.get("headers", {}).items():
569+
req.add_header(header, value)
570+
571+
response = urllib2.urlopen(req, json.dumps(analysis_request))
572+
response_data = response.read()
573+
analysis = self.api_adapter.process_response(response_data)
574+
575+
# Update UI
576+
self._scanner_output.setText("Analysis for %s:\n\n%s" % (domain, analysis))
577+
578+
except Exception as e:
579+
self._scanner_output.setText("Error analyzing domain: %s" % str(e))
580+
581+
# Run analysis in background thread
582+
Thread(target=run_analysis).start()
583+
584+
def get_domain_traffic(self, domain):
585+
"""Gets all traffic for a specific domain"""
586+
traffic = []
587+
for message in self._callbacks.getProxyHistory():
588+
if domain in message.getHttpService().getHost():
589+
analyzed_request = self._helpers.analyzeRequest(message)
590+
analyzed_response = self._helpers.analyzeResponse(message.getResponse())
591+
592+
# Extract request/response data
593+
request_info = {
594+
"method": analyzed_request.getMethod(),
595+
"url": str(message.getUrl()),
596+
"headers": dict(header.split(': ', 1) for header in analyzed_request.getHeaders()[1:] if ': ' in header),
597+
"body": message.getRequest()[analyzed_request.getBodyOffset():].tostring()
598+
}
599+
600+
response_info = {
601+
"status": analyzed_response.getStatusCode(),
602+
"headers": dict(header.split(': ', 1) for header in analyzed_response.getHeaders()[1:] if ': ' in header),
603+
"body": message.getResponse()[analyzed_response.getBodyOffset():].tostring()
604+
}
605+
606+
traffic.append({"request": request_info, "response": response_info})
607+
return traffic
608+
437609
def inferenceLogSelectionChanged(self, event):
438610
selectedRow = self.inferenceLogTable.getSelectedRow()
439611
if selectedRow != -1:
@@ -451,9 +623,7 @@ def inferenceLogSelectionChanged(self, event):
451623
except (ValueError, TypeError):
452624
formatted_request = str(request)
453625

454-
# For response, try to extract the message content if it's a model response
455626
try:
456-
# Handle case where response is already a dict
457627
if isinstance(response, dict):
458628
response_obj = response
459629
else:
@@ -552,7 +722,7 @@ def getTableCellRendererComponent(self, table, value, isSelected, hasFocus, row,
552722

553723
def log_message(self, message):
554724
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
555-
log_entry = "[{0}] {1}\n".format(timestamp, message) # Python2 format strings
725+
log_entry = "[{0}] {1}\n".format(timestamp, message)
556726

557727
if self.logArea is None:
558728
self.temp_log_messages.append(log_entry)
@@ -562,12 +732,10 @@ def log_message(self, message):
562732
self.logArea.getDocument().getLength())
563733

564734
try:
565-
# Try to create/write to log file with explicit permissions
566735
log_dir = os.path.dirname(self.log_file_path)
567736
if not os.path.exists(log_dir):
568-
os.makedirs(log_dir, 0755) # Python2 octal notation
737+
os.makedirs(log_dir, 0755)
569738

570-
# Open with explicit write permissions
571739
with open(self.log_file_path, 'a+') as log_file:
572740
log_file.write(log_entry)
573741
except (IOError, OSError) as e:
@@ -629,7 +797,7 @@ def create_scan_issue(self, messageInfo, processed_response):
629797
detail = str(processed_response)
630798

631799
if detail.startswith('"') and detail.endswith('"'):
632-
detail = detail[1:-1] # Remove surrounding quotes
800+
detail = detail[1:-1]
633801

634802
# Create properly formatted issue name
635803
issue_name = "burpference: %s Security Finding" % severity
@@ -651,6 +819,13 @@ def create_scan_issue(self, messageInfo, processed_response):
651819
self.log_message("Error creating scan issue: %s" % str(e))
652820

653821
def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo):
822+
if messageIsRequest:
823+
# Add new domains to both main extension and scanner
824+
host = messageInfo.getHttpService().getHost()
825+
if host not in self._hosts:
826+
self._hosts.add(host)
827+
if self.scanner:
828+
self.scanner.add_host(host)
654829
if not self.is_running:
655830
return
656831
if not self.api_adapter:
@@ -661,6 +836,13 @@ def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo):
661836
if messageIsRequest:
662837
# Store the request for later use
663838
self.current_request = messageInfo
839+
host = messageInfo.getHttpService().getHost()
840+
if host not in self._hosts:
841+
self._hosts.add(host)
842+
if hasattr(self, '_domain_selector'):
843+
self._domain_selector.addItem(host)
844+
if self.scanner:
845+
self.scanner.add_host(host)
664846
else:
665847
request = self.current_request
666848
response = messageInfo
@@ -799,7 +981,6 @@ def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo):
799981

800982
self.requestArea.append("\n\n=== Request #" + str(self.request_counter) + " ===\n")
801983
try:
802-
# Format the request nicely
803984
formatted_request = json.dumps(http_pair, indent=2)
804985
formatted_request = formatted_request.replace('\\n', '\n')
805986
formatted_request = formatted_request.replace('\\"', '"')
@@ -810,7 +991,6 @@ def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo):
810991

811992
self.responseArea.append("\n\n=== Response #" + str(self.request_counter) + " ===\n")
812993
try:
813-
# Format the response nicely
814994
if isinstance(processed_response, dict) and 'message' in processed_response and 'content' in processed_response['message']:
815995
formatted_response = processed_response['message']['content']
816996
else:

0 commit comments

Comments
 (0)