diff --git a/Autorize.py b/Autorize.py index 1f452f1..212b156 100644 --- a/Autorize.py +++ b/Autorize.py @@ -26,10 +26,12 @@ def registerExtenderCallbacks(self, callbacks): initiator.implement_all() - initiator.init_ui() - + initiator.init_ui() + + initiator.init_mcp() + initiator.print_welcome_message() - + return # @@ -48,5 +50,8 @@ def processProxyMessage(self, messageIsRequest, message): # implement IExtensionStateListener # def extensionUnloaded(self): + mcp = getattr(self, 'mcp', None) + if mcp is not None: + mcp.stop() self.executor.shutdown() print "Autorize extension unloaded." diff --git a/BappManifest.bmf b/BappManifest.bmf index 8df98e3..3e3c422 100644 --- a/BappManifest.bmf +++ b/BappManifest.bmf @@ -2,8 +2,8 @@ Uuid: f9bbac8c4acf4aefa4d7dc92a991af2f ExtensionType: 2 Name: Autorize RepoName: autorize -ScreenVersion: 1.9.1 -SerialVersion: 33 +ScreenVersion: 1.9.4 +SerialVersion: 36 MinPlatformVersion: 2 ProOnly: False Author: Barak Tawily, AppSec Labs diff --git a/README.md b/README.md index 9093bff..5549cc5 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ The extension is written in Jython by **Barak Tawily**, an application security - Burp Repeater - Flexible interception filtering (scope, whitelist, blacklist, regex) - Visual enforcement status indicators for rapid triage +- **Built-in MCP server** for AI-agent integration (see below) --- @@ -191,6 +192,42 @@ This allows you to customize the right panel to show only the comparisons you ca The visibility selection dynamically adjusts the panel layout, giving more vertical space to the remaining viewers. +--- +## MCP Server (AI agent integration) + +Autorize can expose its findings and configuration to an AI agent (e.g. Claude) through a built-in **MCP (Model Context Protocol) server**, in the same spirit as PortSwigger's own Burp MCP server. This lets an agent list and triage authorization findings, inspect a specific result's request/response variants, stand up low-privileged users, drive scanning, and pivot a finding into Repeater — without clicking through the UI. + +### Enabling it + +1. Open the **Configuration → MCP Server** tab (a sub-tab alongside Interception Filters / Table Filter / Save/Restore). +2. Set a **Port** (default `9877`) and tick **Enable MCP Server**. +3. A per-session **bearer token** is generated and shown in the Token field. Use **Copy Token** to grab it, or **Regenerate Token** to roll it. + +The server binds to **127.0.0.1 only** and requires the bearer token on **every** request, because the results table can contain live session cookies and `Authorization` headers. The token lives in memory only — it is never written to disk and is deliberately **excluded** from Save/Restore state files (only the enabled flag and port are persisted). + +> The server is implemented on `java.net.ServerSocket` (part of `java.base`), so it works on Burp's bundled/trimmed JRE with no extra modules required. + +### Connecting + +The endpoint speaks MCP over JSON-RPC 2.0 (Streamable HTTP, non-streaming) at `POST http://127.0.0.1:/mcp`. Point any MCP client at it and send the token as `Authorization: Bearer `. Quick check with `curl`: + +```bash +TOKEN= +curl -s http://127.0.0.1:9877/mcp \ + -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' +``` + +### Available tools + +- **Status / control:** `autorize_status`, `autorize_start`, `autorize_stop`, `autorize_clear_results` +- **Findings:** `autorize_list_results`, `autorize_list_results_regex`, `autorize_get_result`, `autorize_send_to_repeater` +- **Users:** `autorize_list_users`, `autorize_add_user` (one or many at once), `autorize_update_user`, `autorize_remove_user`, `autorize_get_last_headers` (the source behind the *Fetch Cookies/Authorization header* buttons — `autorize_update_user` can pull from it directly via `fetch_from_last_request`) +- **Detectors & filters:** `autorize_get_filter_options`, `autorize_get_interception_filters` / `autorize_set_interception_filters`, `autorize_get_unauth_detector` / `autorize_set_unauth_detector` (per-user enforcement detectors and match/replace rules are configured through `autorize_add_user` / `autorize_update_user`, with all filter/rule types supported) +- **Config & state:** `autorize_get_config`, `autorize_set_config`, `autorize_export` (HTML/CSV), `autorize_save_state`, `autorize_restore_state` + +Call `autorize_get_filter_options` first to retrieve the exact type strings accepted by the enforcement-detector, match/replace, and interception-filter tools. + --- ## Limitations & Notes diff --git a/gui/configuration_tab.py b/gui/configuration_tab.py index 5cbd0ae..37cdf17 100644 --- a/gui/configuration_tab.py +++ b/gui/configuration_tab.py @@ -16,6 +16,18 @@ from table import UpdateTableEDT +def set_running(extender, running): + """Turn Autorize scanning on/off. Shared by the GUI toggle and MCP tools.""" + if running: + extender.startButton.setText("Autorize is on") + extender.startButton.setSelected(True) + extender.intercept = 1 + else: + extender.startButton.setText("Autorize is off") + extender.startButton.setSelected(False) + extender.intercept = 0 + + class ClearTableRunnable(Runnable): """Runs on executor so EDT never blocks on _lock.""" def __init__(self, extender): @@ -206,14 +218,7 @@ def draw(self): self._extender._cfg_splitpane.setLeftComponent(self.config_pnl) def startOrStop(self, event): - if self._extender.startButton.getText() == "Autorize is off": - self._extender.startButton.setText("Autorize is on") - self._extender.startButton.setSelected(True) - self._extender.intercept = 1 - else: - self._extender.startButton.setText("Autorize is off") - self._extender.startButton.setSelected(False) - self._extender.intercept = 0 + set_running(self._extender, self._extender.startButton.getText() == "Autorize is off") def clearTable(self, event): # Run on executor so the EDT never blocks on _lock (avoids UI freeze) diff --git a/gui/export.py b/gui/export.py index e02ef5f..8ffbb38 100644 --- a/gui/export.py +++ b/gui/export.py @@ -246,42 +246,7 @@ def restoreStateAction(self, event): self.save_restore.restoreState() def shouldIncludeRow(self, logEntry, enforcementStatusFilter): - should_include = False - - if enforcementStatusFilter == "All Statuses": - should_include = True - elif enforcementStatusFilter == "As table filter": - if hasattr(self._extender, 'showBypassed') and hasattr(self._extender, 'showIsEnforced') and hasattr(self._extender, 'showEnforced'): - # Check unauthenticated status - unauth_status = logEntry._enfocementStatusUnauthorized - if ((self._extender.showBypassed.isSelected() and self.BYPASSSED_STR == unauth_status) or - (self._extender.showIsEnforced.isSelected() and self.IS_ENFORCED_STR == unauth_status) or - (self._extender.showEnforced.isSelected() and self.ENFORCED_STR == unauth_status) or - ("Disabled" == unauth_status)): - should_include = True - - for user_id in logEntry.get_all_users(): - user_data = logEntry.get_user_enforcement(user_id) - if user_data: - user_status = user_data['enforcementStatus'] - if ((self._extender.showBypassed.isSelected() and self.BYPASSSED_STR == user_status) or - (self._extender.showIsEnforced.isSelected() and self.IS_ENFORCED_STR == user_status) or - (self._extender.showEnforced.isSelected() and self.ENFORCED_STR == user_status)): - should_include = True - break - else: - should_include = True - else: - if enforcementStatusFilter == logEntry._enfocementStatusUnauthorized: - should_include = True - else: - for user_id in logEntry.get_all_users(): - user_data = logEntry.get_user_enforcement(user_id) - if user_data and enforcementStatusFilter == user_data['enforcementStatus']: - should_include = True - break - - return should_include + return _should_include_row(self._extender, logEntry, enforcementStatusFilter) def exportToHTML(self): parentFrame = JFrame() @@ -289,21 +254,92 @@ def exportToHTML(self): fileChooser.setSelectedFile(File("AutorizeReport.html")) fileChooser.setDialogTitle("Save Autorize Report") userSelection = fileChooser.showSaveDialog(parentFrame) - if userSelection == JFileChooser.APPROVE_OPTION: - fileToSave = fileChooser.getSelectedFile() + if userSelection != JFileChooser.APPROVE_OPTION: + return + fileToSave = fileChooser.getSelectedFile() - enforcementStatusFilter = self.exportES.getSelectedItem() + htmlContent = build_html_report(self._extender, + self.exportES.getSelectedItem(), + self.removeDuplicates.isSelected()) + f = open(fileToSave.getAbsolutePath(), 'w') + f.writelines(htmlContent) + f.close() - header_html = "IDMethodURLOriginal lengthUnauth lengthUnauth Status" - - if hasattr(self._extender, 'userTab') and self._extender.userTab: - for user_id in sorted(self._extender.userTab.user_tabs.keys()): - user_name = self._extender.userTab.user_tabs[user_id]['user_name'] - header_html += "{} Len{} Status".format(user_name, user_name) - - header_html += "" - - htmlContent = """Autorize Report by Barak Tawily + def exportToCSV(self): + parentFrame = JFrame() + fileChooser = JFileChooser() + fileChooser.setSelectedFile(File("AutorizeReport.csv")) + fileChooser.setDialogTitle("Save Autorize Report") + userSelection = fileChooser.showSaveDialog(parentFrame) + if userSelection != JFileChooser.APPROVE_OPTION: + return + fileToSave = fileChooser.getSelectedFile() + + csvContent = build_csv_report(self._extender, + self.exportES.getSelectedItem(), + self.removeDuplicates.isSelected()) + f = open(fileToSave.getAbsolutePath(), 'w') + f.writelines(csvContent) + f.close() + + +def _should_include_row(extender, logEntry, enforcementStatusFilter): + BYPASSSED_STR = extender.BYPASSSED_STR + ENFORCED_STR = extender.ENFORCED_STR + IS_ENFORCED_STR = extender.IS_ENFORCED_STR + should_include = False + + if enforcementStatusFilter == "All Statuses": + should_include = True + elif enforcementStatusFilter == "As table filter": + if hasattr(extender, 'showBypassed') and hasattr(extender, 'showIsEnforced') and hasattr(extender, 'showEnforced'): + unauth_status = logEntry._enfocementStatusUnauthorized + if ((extender.showBypassed.isSelected() and BYPASSSED_STR == unauth_status) or + (extender.showIsEnforced.isSelected() and IS_ENFORCED_STR == unauth_status) or + (extender.showEnforced.isSelected() and ENFORCED_STR == unauth_status) or + ("Disabled" == unauth_status)): + should_include = True + + for user_id in logEntry.get_all_users(): + user_data = logEntry.get_user_enforcement(user_id) + if user_data: + user_status = user_data['enforcementStatus'] + if ((extender.showBypassed.isSelected() and BYPASSSED_STR == user_status) or + (extender.showIsEnforced.isSelected() and IS_ENFORCED_STR == user_status) or + (extender.showEnforced.isSelected() and ENFORCED_STR == user_status)): + should_include = True + break + else: + should_include = True + else: + if enforcementStatusFilter == logEntry._enfocementStatusUnauthorized: + should_include = True + else: + for user_id in logEntry.get_all_users(): + user_data = logEntry.get_user_enforcement(user_id) + if user_data and enforcementStatusFilter == user_data['enforcementStatus']: + should_include = True + break + + return should_include + + +def build_html_report(extender, enforcementStatusFilter, removeDuplicates): + BYPASSSED_STR = extender.BYPASSSED_STR + ENFORCED_STR = extender.ENFORCED_STR + IS_ENFORCED_STR = extender.IS_ENFORCED_STR + log = extender._log + + header_html = "IDMethodURLOriginal lengthUnauth lengthUnauth Status" + + if hasattr(extender, 'userTab') and extender.userTab: + for user_id in sorted(extender.userTab.user_tabs.keys()): + user_name = extender.userTab.user_tabs[user_id]['user_name'] + header_html += "{} Len{} Status".format(user_name, user_name) + + header_html += "" + + htmlContent = """Autorize Report by Barak Tawily