├── .github └── images │ ├── 0x999_X.png │ ├── change_in_monitored.png │ ├── change_in_monitored2.png │ ├── hosts.png │ ├── jsluicepp-filters.gif │ ├── jsluicepp-process-from-sitemap.gif │ ├── jsluicepp.png │ ├── logo.png │ ├── no_results_monitored.png │ ├── secret_notification.png │ └── tomnomnom_X.png ├── LICENSE ├── README.md └── jsluicepp.py /.github/images/0x999_X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x999-x/jsluicepp/e829f18faaef63673f2ec465c83371a4caa8bf52/.github/images/0x999_X.png -------------------------------------------------------------------------------- /.github/images/change_in_monitored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x999-x/jsluicepp/e829f18faaef63673f2ec465c83371a4caa8bf52/.github/images/change_in_monitored.png -------------------------------------------------------------------------------- /.github/images/change_in_monitored2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x999-x/jsluicepp/e829f18faaef63673f2ec465c83371a4caa8bf52/.github/images/change_in_monitored2.png -------------------------------------------------------------------------------- /.github/images/hosts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x999-x/jsluicepp/e829f18faaef63673f2ec465c83371a4caa8bf52/.github/images/hosts.png -------------------------------------------------------------------------------- /.github/images/jsluicepp-filters.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x999-x/jsluicepp/e829f18faaef63673f2ec465c83371a4caa8bf52/.github/images/jsluicepp-filters.gif -------------------------------------------------------------------------------- /.github/images/jsluicepp-process-from-sitemap.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x999-x/jsluicepp/e829f18faaef63673f2ec465c83371a4caa8bf52/.github/images/jsluicepp-process-from-sitemap.gif -------------------------------------------------------------------------------- /.github/images/jsluicepp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x999-x/jsluicepp/e829f18faaef63673f2ec465c83371a4caa8bf52/.github/images/jsluicepp.png -------------------------------------------------------------------------------- /.github/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x999-x/jsluicepp/e829f18faaef63673f2ec465c83371a4caa8bf52/.github/images/logo.png -------------------------------------------------------------------------------- /.github/images/no_results_monitored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x999-x/jsluicepp/e829f18faaef63673f2ec465c83371a4caa8bf52/.github/images/no_results_monitored.png -------------------------------------------------------------------------------- /.github/images/secret_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x999-x/jsluicepp/e829f18faaef63673f2ec465c83371a4caa8bf52/.github/images/secret_notification.png -------------------------------------------------------------------------------- /.github/images/tomnomnom_X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0x999-x/jsluicepp/e829f18faaef63673f2ec465c83371a4caa8bf52/.github/images/tomnomnom_X.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 0x999 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![jsluice++ Logo](https://raw.githubusercontent.com/0x999-x/jsluicepp/main/.github/images/logo.png)](#) 4 | 5 |
6 | 7 | ## 📄 Table of Contents 8 | - [👋 Introduction](#-introduction) 9 | - [🛠️ Setup](#%EF%B8%8F-setup) 10 | - [📝 Usage](#-usage) 11 | - [📜 Features](#-features) 12 | - [Monitor URLs](#monitor-urls) 13 | - [Send to Repeater](#send-to-repeater) 14 | - [Secrets](#secrets) 15 | - [Secret Notifications](#secret-notifications) 16 | - [Copy URL](#copy-url) 17 | - [Positive/Negative Match](#positivenegative-match) 18 | - [In-scope only](#in-scope-only) 19 | - [In-line Tags](#in-line-tags) 20 | - [Hide Duplicates](#hide-duplicates) 21 | - [Show Parameterized](#show-parameterized) 22 | - [Import/Export](#importexport) 23 | - [Save Settings](#save-settings) 24 | - [🤝 Contributors](#-contributors) 25 | 26 | 27 | ## 👋 Introduction 28 | 29 | jsluice++ is a Burp Suite extension designed for passive and active scanning of JavaScript traffic using the CLI tool [jsluice](https://github.com/BishopFox/jsluice/tree/main/cmd/jsluice).
30 | The extension utilizes jsluice's capabilities to extract URLs, paths, and secrets from static JavaScript files and integrates it with Burp Suite, allowing you to easily scan javascript traffic from Burp Suite's Sitemap or Proxy while also offering a user-friendly interface for data inspection and a variety of additional useful features 31 | 32 |

33 | jsluice++ 34 |

35 | 36 | ## 🛠️ Setup 37 | 38 | Requirements: 39 | - jsluice CLI 40 | - Jython(2.7.3) 41 | 42 | if this isn't your first time installing a jython extension you can skip to step 3.
43 | 1. Visit [Jython's Official Website](https://www.jython.org/download) and download Jython's standalone JAR 44 | 2. In Burp Suite -> "Extensions" -> "Extensions Settings" -> under "Python environment" select the "Location of Jython standalone JAR file" 45 | 3. Download and install [jsluice's](https://github.com/BishopFox/jsluice/tree/main/cmd/jsluice) CLI go install github.com/BishopFox/jsluice/cmd/jsluice@latest (ensure that the jsluice binary is in your $PATH otherwise the extension won't work) 46 | 4. Download jsluicepp.py, then in Burp Suite go to "Extensions" -> "Installed" -> Click "Add" -> under "Extension type" select "Python" -> Select the jsluicepp.py file. 47 | 48 | ## 📝 Usage 49 | 50 | ##### Active scan 51 | 52 | The extension adds an item to Burp Suite's context menu which allows you to easily process responses from Burp Suite's Sitemap tab
53 | 54 | to do so simply right click any host in the sitemap tree or any item in the sitemap table and select Extensions->jsluice++->Process selected item(s) in Burp Suite's context menu. 55 | When processing items from the site map tree the extension will get the site map of every selected item (Multiple hosts can be processed) 56 | 57 | ![Process from Sitemap](https://raw.githubusercontent.com/0x999-x/jsluicepp/main/.github/images/jsluicepp-process-from-sitemap.gif) 58 | 59 | ##### Passive scan 60 | Default: Off 61 | 62 | When Passive scan is toggled on the extension will register an http handler and process responses from traffic flowing through Burp Suite's Proxy (it's recommended to use the in-scope only feature when enabling passive scan to reduce noise) 63 | 64 | --- 65 | 66 | The extension will process any URL with a .js file extension or a JavaScript mime type and a success status code (2xx) using jsluice's urls mode.
67 | 68 | The processed JavaScript file is temporarily saved locally in the ".jsluicepp" directory and gets removed after jsluice has finished processing it, 69 | 70 | If jsluice returns any data, the host associated with the URL will be added to the Hosts list, to view the results from jsluice simply select the host and the desired file (multiple files can be selected). 71 | 72 | Note: the same URL (GET parameters excluded) will not be processed more than once until the extension is reloaded, this doesn't apply to monitored urls. 73 | 74 | --- 75 | ## 📜 Features 76 | 77 |

Monitor URLs

78 | 79 | Every file in the Files list can be monitored by the extension, to do so simply right-click the file and select the "Monitor URL" option from the popup menu (repeat this step if you wish to Stop Monitoring the URL),
80 | When a new URL is monitored it's details are saved to .jsluicepp/monitored_urls.txt and a copy of the output from jsluice is saved locally to .jsluicepp/monitored_files/{host}_{filename_hash}(Secrets excluded),
81 | Monitored files are colored green in the Files list,
82 | The rate at which requests are sent to the monitored URLs is determined by the selected value in the Monitor Interval selector, 83 | 84 | Monitor interval options are: 85 | - Off - Don't monitor 86 | - Once - Once when the extension loads 87 | - Hourly - Once every hour 88 | - Daily - Once every day 89 | - Weekly - Once every week 90 | - Monthly - Once every month 91 |
92 | if jsluice has returned data that is different from the locally saved copy you will be notified with the following popup dialog: 93 | 94 | ![Change in Monitored URL](https://raw.githubusercontent.com/0x999-x/jsluicepp/main/.github/images/change_in_monitored.png) 95 | 96 | the locally saved copy of the file will get moved to .jsluicepp/monitored_files/{host}_{filename_hash}.old and a new copy of the new file will be saved, 97 | When selecting the monitored file in the extension new/modified rows will be colored green and previous versions of modified rows/deleted rows will be colored red, example: 98 | 99 | ![Change in Monitored URL 2](https://raw.githubusercontent.com/0x999-x/jsluicepp/main/.github/images/change_in_monitored2.png) 100 | 101 | 102 | if a Monitored URL responds with a non-success status code(2xx) or if jsluice returns no output you will be prompted with a popup dialog asking if you wish to Stop Monitoring the URL, if "Yes" is selected all local copies of the file will be removed. 103 | 104 | ![No results from Monitored URL](https://raw.githubusercontent.com/0x999-x/jsluicepp/main/.github/images/no_results_monitored.png) 105 |
106 |

Send to Repeater

107 | 108 | Every row in the URL/Paths results table can be sent to Burp Suite's Repeater by right clicking any row and selecting the "Send to Repeater" option in the popup menu.
109 | Query Params, Body Params & Headers will be included if any exist, 110 | if the URL/Path column starts with "http://" or "https://" or "//" the host will be extracted from the URL/Path column otherwise the selected host will be used, 111 | if a content-type header with a value of application/json or application/xml or text/xml is present in the Headers column the body of the request will be formatted accordingly. 112 |
113 |

Secrets

114 | Default: On 115 | 116 | If selected a "Secrets" results table is added to the UI and the extension will use jsluice's secrets mode on the file after the urls mode has finished, if any unique secrets are found the host will be colored red with a 🤫 emoji next to it, example: 117 | 118 | ![Colored Hosts](https://raw.githubusercontent.com/0x999-x/jsluicepp/main/.github/images/hosts.png) 119 | 120 | if you wish to use a custom patterns config you can do so by modifying the 'secrets_command' variable in the code. 121 |
122 |

Secret Notifications

123 | Default: Off 124 | 125 | If selected and the Secrets checkbox is also selected you will be notified with a popup dialog when a new unique secret is found, the dialog is closed automatically after 15 seconds , example dialog:
126 | ![Example secret notification](https://raw.githubusercontent.com/0x999-x/jsluicepp/main/.github/images/secret_notification.png) 127 |
128 |

Copy URL

129 | 130 | The URL of every processed file can be copied to clipboard by right-clicking the file in the Files list and selecting the "Copy URL" option from the popup menu. 131 |
132 |

Positive/Negative Match

133 | 134 | The positive/negative match filter feature is designed to target the URL/Path column within the results table, When adding a positive filter, only rows that contain the positive match filter within the URL/Path column will be included in the results table. Conversely, when adding a negative filter, rows that have the negative match filter in the URL/Path column will be excluded from the results, 135 | Multiple filters can be applied. 136 | ![Positive-Negative Match](https://raw.githubusercontent.com/0x999-x/jsluicepp/main/.github/images/jsluicepp-filters.gif) 137 |
138 |

In-scope only

139 | Default: Off 140 | 141 | If selected the extension will use Burp Suite's scope to determine whether a URL should be processed, additionally out of scope hosts will not be displayed in the hosts list. 142 |
143 |

In-line Tags

144 | Default: Off 145 | 146 | When selected, in addition to URLs with a .js file extension/mime type, the extension will look for responses with HTML mime type, if such response is found the extension will attempt to extract any and all ', response_body, re.DOTALL) 1454 | response_body = "\n".join(script_tags) 1455 | file_name_hash = md5(file_name.encode()).hexdigest() 1456 | file_path = self.directory + "/" + host + "_" + file_name_hash 1457 | monitor_file_path = self.directory + self.monitored_urls_directory + host + "_" + file_name_hash 1458 | urls_command = self.path_to_binary + " urls " + file_path 1459 | with open(file_path, "w") as f: 1460 | f.write(response_body) 1461 | process_urls = subprocess.Popen(urls_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 1462 | urls_stdout, stderr = process_urls.communicate() 1463 | urls_stdout_list = (json.loads(line) for line in urls_stdout.splitlines()) 1464 | sorted_stdout = [] 1465 | for line in urls_stdout_list: 1466 | if "queryParams" in line: 1467 | line["queryParams"] = sorted(line["queryParams"]) 1468 | if "bodyParams" in line: 1469 | line["bodyParams"] = sorted(line["bodyParams"]) 1470 | sorted_stdout.append(json.dumps(line)) 1471 | urls_stdout = "\n".join(sorted_stdout) 1472 | if os.path.exists(monitor_file_path): 1473 | monitored_file = "" 1474 | with open(monitor_file_path, "r") as f: 1475 | monitored_file = json.load(f) if os.stat(monitor_file_path).st_size > 0 else "" 1476 | if urls_stdout.decode("utf-8") != monitored_file.decode("utf-8"): 1477 | self.show_dialogs(url=url_str) 1478 | if os.path.exists(monitor_file_path + ".old"): 1479 | os.remove(monitor_file_path + ".old") 1480 | os.rename(monitor_file_path, monitor_file_path + ".old") 1481 | with open(monitor_file_path, "w") as f: 1482 | f.write(json.dumps(urls_stdout)) 1483 | if self.secrets_checkbox.isSelected(): 1484 | secrets_command = self.path_to_binary + " secrets " + file_path 1485 | process_secrets = subprocess.Popen(secrets_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 1486 | secrets_stdout, stderr2 = process_secrets.communicate() 1487 | 1488 | if (not self.secrets_checkbox.isSelected() and urls_stdout == '') or (self.secrets_checkbox.isSelected() and urls_stdout == '' and secrets_stdout == ''): 1489 | try: 1490 | os.remove(file_path) 1491 | except OSError: 1492 | pass 1493 | if url_str in self.monitored_urls: 1494 | self.show_dialogs(host=host, url=url_str, reason_label="Returned no endpoints or secrets") 1495 | self.seen_endpoints.add(url_split) 1496 | return 1497 | 1498 | if host not in self.hosts_list: 1499 | if self.in_scope_checkbox.isSelected(): 1500 | if self._callbacks.isInScope(url): 1501 | self.hosts_list.add(host) 1502 | self.host_list_model.addElement(host) 1503 | else: 1504 | self.hosts_list.add(host) 1505 | self.host_list_model.addElement(host) 1506 | self.tabbed_pane_hosts.setTitleAt(0, str(self.host_list_model.size()) + " Hosts") 1507 | 1508 | if self.secrets_checkbox.isSelected(): 1509 | processed_file_details = {"host": host, "file_name": file_name, "output": urls_stdout.encode('utf-8'), "secrets": secrets_stdout.encode('utf-8'), "origin_url": url_str} 1510 | lines = secrets_stdout.strip().split("\n") 1511 | found_secret = False 1512 | for line in lines: 1513 | if line == '': 1514 | continue 1515 | data = json.loads(line, encoding='utf-8') 1516 | match = data["data"].get("match") 1517 | key = data["data"].get("key") 1518 | if match and str(match.encode('utf-8')) not in self.unique_secrets: 1519 | self.unique_secrets.add(match) 1520 | found_secret = True 1521 | elif key and str(key.encode('utf-8')) not in self.unique_secrets: 1522 | self.unique_secrets.add(key) 1523 | found_secret = True 1524 | else: 1525 | pass 1526 | if found_secret: 1527 | if host not in self.hosts_with_secrets: 1528 | self.hosts_with_secrets.add(host) 1529 | self.add_image_to_host(self.hosts_with_secrets, self.secret_image_base64data) 1530 | if self.secret_notifications_checkbox.isSelected(): 1531 | self.show_dialogs(host=host) 1532 | else: 1533 | processed_file_details = {"host": host, "file_name": file_name, "output": urls_stdout.encode('utf-8'), "origin_url": url_str} 1534 | self.processed_file_details[url_split] = processed_file_details 1535 | try: 1536 | os.remove(file_path) 1537 | except OSError: 1538 | pass 1539 | self.seen_endpoints.add(url_split) 1540 | if host == self.selected_host: 1541 | if file_name not in self.file_names_model.toArray(): 1542 | self.file_names_model.addElement(file_name) 1543 | self.file_names_list.setCellRenderer(ColorFiles(self.monitored_urls, self.processed_file_details, self.selected_host, self.directory, self.monitored_urls_directory)) 1544 | 1545 | def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo): 1546 | if not messageIsRequest and ((self.on_off_button.isSelected() and toolFlag == self._callbacks.TOOL_PROXY)): 1547 | self.process_with_jsluice(messageInfo) 1548 | 1549 | def createMenuItems(self, invocation): 1550 | menuList = [] 1551 | if invocation.getToolFlag() == self._callbacks.TOOL_TARGET: 1552 | if invocation.getInvocationContext() in [invocation.CONTEXT_TARGET_SITE_MAP_TREE, invocation.CONTEXT_TARGET_SITE_MAP_TABLE]: 1553 | menuItem = swing.JMenuItem("Process selected item(s)", actionPerformed=lambda x: self.schedule_event(0, self.action, args=[invocation])) 1554 | menuList.append(menuItem) 1555 | return menuList 1556 | 1557 | def show_dialog2(self, site_map, invocation): 1558 | self.dialog = swing.JDialog() 1559 | self.dialog.setTitle("jsluice++") 1560 | self.dialog.setDefaultCloseOperation(swing.JDialog.DISPOSE_ON_CLOSE) 1561 | self.dialog.setSize(300, 124) 1562 | self.dialog.setLocationRelativeTo(None) 1563 | self.dialog.setLayout(swing.BoxLayout(self.dialog.getContentPane(), swing.BoxLayout.Y_AXIS)) 1564 | self.dialog.setModal(True) 1565 | self.dialog.setAlwaysOnTop(True) 1566 | self.dialog.setResizable(False) 1567 | 1568 | self.label1 = swing.JLabel("Processing " + str(len(site_map)) + " items...") 1569 | self.dialog.add(self.label1) 1570 | 1571 | self.progress_bar = swing.JProgressBar(0, len(site_map)) 1572 | self.progress_bar.setStringPainted(True) 1573 | self.progress_bar.setString("0/" + str(len(site_map))) 1574 | self.dialog.add(self.progress_bar) 1575 | 1576 | self.buttons_panel = swing.JPanel() 1577 | self.buttons_panel.setLayout(swing.BoxLayout(self.buttons_panel, swing.BoxLayout.X_AXIS)) 1578 | self.buttons_panel.add(swing.Box.createHorizontalGlue()) 1579 | 1580 | self.threads_panel = swing.JPanel() 1581 | self.threads_panel.setLayout(swing.BoxLayout(self.threads_panel, swing.BoxLayout.X_AXIS)) 1582 | self.threads_panel.add(swing.Box.createHorizontalGlue()) 1583 | 1584 | self.threads_label = swing.JLabel("Threads:") 1585 | spinner_model = swing.SpinnerNumberModel(1, 1, 20, 1) 1586 | self.threads_spinner = swing.JSpinner(spinner_model) 1587 | self.threads_spinner.preferredSize = java.awt.Dimension(64, 24) 1588 | self.threads_spinner.setMaximumSize(java.awt.Dimension(64, 24)) 1589 | 1590 | self.threads_panel.add(self.threads_label) 1591 | self.threads_panel.add(self.threads_spinner) 1592 | self.threads_panel.add(swing.Box.createHorizontalGlue()) 1593 | self.dialog.add(self.threads_panel) 1594 | 1595 | self.start_button = swing.JButton("Start", actionPerformed=lambda x: self.schedule_event(0, self.process_site_map, args=(site_map, invocation))) 1596 | self.start_button.setForeground(awt.Color.BLACK) 1597 | self.start_button.setBackground(awt.Color(110, 204, 154)) 1598 | self.buttons_panel.add(self.start_button) 1599 | self.buttons_panel.add(swing.Box.createHorizontalGlue()) 1600 | self.cancel_button = swing.JButton("Cancel", actionPerformed=lambda x: (self.cancel_progress(site_map, invocation), self.dialog.dispose())) 1601 | self.buttons_panel.add(self.cancel_button) 1602 | self.buttons_panel.add(swing.Box.createHorizontalGlue()) 1603 | self.dialog.add(self.buttons_panel) 1604 | self.dialog.setVisible(True) 1605 | 1606 | def cancel_progress(self, site_map, invocation): 1607 | self.cancelled = True 1608 | self.start_button.setText("Start") 1609 | self.start_button.setBackground(awt.Color(110, 204, 154)) 1610 | self.start_button.actionPerformed = lambda x: self.schedule_event(0, self.process_site_map, args=(site_map, invocation)) 1611 | self.threads_spinner.setEnabled(True) 1612 | 1613 | def process_site_map(self, site_map, invocation): 1614 | self.cancelled = False 1615 | self.threads_spinner.setEnabled(False) 1616 | self.start_button.setText("Stop") 1617 | self.start_button.setBackground(awt.Color(240,128,128)) 1618 | self.start_button.actionPerformed = lambda x: self.cancel_progress(site_map, invocation) 1619 | if len(site_map) < self.threads_spinner.getValue(): 1620 | self.threads_spinner.setValue(len(site_map)) 1621 | start_time = time() 1622 | semaphore = Semaphore(int(self.threads_spinner.getValue())) 1623 | def process_item(item): 1624 | self.process_with_jsluice(item) 1625 | self.update_progress(index, len(site_map), invocation, start_time) 1626 | semaphore.release() 1627 | for index, item in enumerate(site_map, start=1): 1628 | semaphore.acquire() 1629 | if self.cancelled: 1630 | semaphore.release() 1631 | break 1632 | Thread(target=process_item, args=(item,)).start() 1633 | return 1634 | 1635 | def update_progress(self, current, total, invocation, start_time): 1636 | if current == total: 1637 | self.dialog.remove(self.threads_spinner) 1638 | self.threads_panel.remove(self.threads_spinner) 1639 | self.threads_panel.remove(self.threads_label) 1640 | self.buttons_panel.remove(self.start_button) 1641 | end_time = time() 1642 | if invocation.getInvocationContext() in [invocation.CONTEXT_TARGET_SITE_MAP_TREE, invocation.CONTEXT_TARGET_SITE_MAP_TABLE]: 1643 | self.label1.setText(str(total) + " items processed in " + str(round(end_time - start_time, 2)) + " seconds") 1644 | self.cancel_button.setText("Close") 1645 | self.dialog.setSize(self.dialog.getPreferredSize()) 1646 | self.dialog.revalidate() 1647 | self.dialog.repaint() 1648 | percent = int((float(current) / float(total)) * 100) 1649 | self.progress_bar.setValue(current) 1650 | self.progress_bar.setString(str(current) + "/" + str(total) + " (" + str(percent) + "%)") 1651 | self.dialog.revalidate() 1652 | self.dialog.repaint() 1653 | 1654 | def action(self, invocation): 1655 | self.cancelled = False 1656 | selected_messages = invocation.getSelectedMessages() 1657 | if invocation.getInvocationContext() in [invocation.CONTEXT_TARGET_SITE_MAP_TREE]: 1658 | if len(selected_messages) == 1: 1659 | site_map = [item for item in self._callbacks.getSiteMap(str(selected_messages[0].getUrl())) if item.getResponse() != None] 1660 | else: 1661 | selected_messages = [msg.getUrl() for msg in selected_messages if urlparse(unicode(msg.getUrl())).path == "/"] 1662 | site_map = [item for item in [self._callbacks.getSiteMap(str(selected_messages[i])) for i in range(len(selected_messages))] for item in item if item.getResponse() != None] 1663 | elif invocation.getInvocationContext() in [invocation.CONTEXT_TARGET_SITE_MAP_TABLE]: 1664 | site_map = [item for item in selected_messages if item.getResponse() != None] 1665 | swing.SwingUtilities.invokeLater(lambda: self.show_dialog2(site_map, invocation)) 1666 | 1667 | def on_host_selected(self, event): 1668 | if self.host_list.getSelectedValue() == None: 1669 | return 1670 | self.file_names_model.clear() 1671 | self.loading_label.setVisible(True) 1672 | self.tabbed_pane_files.setTitleAt(0, "Loading...") 1673 | swing.SwingUtilities.invokeLater(lambda: self.handle_selected_host(event)) 1674 | return 1675 | 1676 | def handle_selected_host(self, event): 1677 | if isinstance(event, swing.event.ListSelectionEvent) and event.getValueIsAdjusting(): 1678 | return 1679 | selected_host = self.host_list.getSelectedValue() 1680 | if selected_host: 1681 | self.file_names_model.clear() 1682 | file_names = self.get_processed_file_names(selected_host) 1683 | 1684 | def update_file_names(): 1685 | self.loading_label.setVisible(False) 1686 | for file_name in file_names: 1687 | if file_name not in self.file_names_model.toArray(): 1688 | self.file_names_model.addElement(file_name) 1689 | if file_names and not self.autoselectall_button.isSelected(): 1690 | self.file_names_list.setSelectedIndex(0) 1691 | elif file_names and self.autoselectall_button.isSelected(): 1692 | self.file_names_list.setSelectionInterval(0, self.file_names_model.size() - 1) 1693 | self.file_list_popupmenu.selected_host = selected_host 1694 | self.file_list_popupmenu.file_names_list = self.file_names_list 1695 | self.file_list_popupmenu.processed_file_details = self.processed_file_details 1696 | self.file_list_popupmenu.monitored_urls = self.monitored_urls 1697 | self.file_names_list.setCellRenderer(ColorFiles(self.monitored_urls, self.processed_file_details, self.selected_host, self.directory, self.monitored_urls_directory)) 1698 | self.file_names_list.revalidate() 1699 | self.file_names_list.repaint() 1700 | self.tabbed_pane_files.setTitleAt(0, str(len(file_names)) + " File" + ("s" if len(file_names) > 1 else "")) 1701 | self.file_names_list.setValueIsAdjusting(False) 1702 | swing.SwingUtilities.invokeLater(lambda: update_file_names()) 1703 | 1704 | def toggle_autoselectall(self, event): 1705 | if self.autoselectall_button.isSelected(): 1706 | self.autoselectall_button.setBorder(self.green_border) 1707 | else: 1708 | self.autoselectall_button.setBorder(self.red_border) 1709 | if self.host_list.getSelectedValue(): 1710 | self.loading_label.setVisible(True) 1711 | swing.SwingUtilities.invokeLater(lambda: self.handle_selected_host(event)) 1712 | 1713 | def process_lines(self, lines, selected_files_length, selected_file_name): 1714 | processed_rows = [] 1715 | for line in lines: 1716 | if line == '': 1717 | continue 1718 | try: 1719 | data = json.loads(line) 1720 | queryParams = '' if data["queryParams"] == [] else json.dumps(data["queryParams"]) 1721 | bodyParams = '' if data["bodyParams"] == [] else json.dumps(data["bodyParams"]) 1722 | method = '' if data["method"] == '' else json.dumps(data["method"]) 1723 | headers = json.dumps(data["headers"]) if data.get("headers") is not None else '' 1724 | row_data = ( 1725 | json.dumps(data["url"]).replace('"', ''), 1726 | queryParams[1:-1], 1727 | bodyParams, 1728 | method[1:-1], 1729 | headers, 1730 | json.dumps(data["type"]).replace('"', ''), 1731 | ) 1732 | if selected_files_length > 1: 1733 | row_data = row_data + (selected_file_name,) 1734 | processed_rows.append(row_data) 1735 | except ValueError: 1736 | print("Error parsing the line: " + line) 1737 | return processed_rows 1738 | 1739 | def display_result(self, event, file_names_list): 1740 | if isinstance(event, swing.event.ListSelectionEvent) and "invalid" in str(event) and event.getValueIsAdjusting() and len(self.file_names_model.toArray()) != 0: 1741 | return 1742 | deleted_lines = set() 1743 | differences = set() 1744 | self.selected_host = self.host_list.getSelectedValue() 1745 | self.results_table_popupmenu.selected_host = self.selected_host 1746 | selected_files = file_names_list.getSelectedValuesList() 1747 | selected_files_length = len(selected_files) 1748 | if self.selected_host: 1749 | self.result_table_model.setRowCount(0) 1750 | self.tabbed_pane_results.setTitleAt(0, str(self.selected_host) + " - Loading...") 1751 | if self.secrets_checkbox.isSelected(): 1752 | if self.tabbed_pane_results.indexOfComponent(self.scroll_pane_secrets) == -1: 1753 | self.tabbed_pane_results.addTab("Secrets", self.scroll_pane_secrets) 1754 | self.tabbed_pane_results.setTitleAt(1, "Secrets - Loading...") 1755 | rows = [] 1756 | if selected_files_length > 0: 1757 | rows = self.get_results_rows(selected_files, selected_files_length, rows, differences, deleted_lines) 1758 | for row in rows: 1759 | self.result_table_model.addRow(row) 1760 | for column in range(self.result_table_model.getColumnCount()): 1761 | self.result_table.getColumnModel().getColumn(column).setCellRenderer(ColorResults(differences, deleted_lines)) 1762 | self.tabbed_pane_results.setTitleAt(0, self.get_result_tab_name()) 1763 | if self.secrets_checkbox.isSelected(): 1764 | rows = self.get_secrets_rows(selected_files_length, selected_files) 1765 | for row in rows: 1766 | self.secrets_table_model.addRow(row) 1767 | self.tabbed_pane_results.setTitleAt(1, "Secrets ({} rows)".format(self.secrets_table_model.getRowCount())) 1768 | self.tabbed_pane_results.revalidate() 1769 | self.tabbed_pane_results.repaint() 1770 | 1771 | def get_results_rows(self, selected_files, selected_files_length, rows, differences, deleted_lines): 1772 | for selected_file_name in selected_files: 1773 | selected_file_details = None 1774 | for file in self.processed_file_details.values(): 1775 | if file["host"] == self.selected_host and file["file_name"] == selected_file_name: 1776 | selected_file_details = file 1777 | break 1778 | if selected_file_details and selected_file_details["output"]: 1779 | monitor_filter_name_hash = md5(selected_file_details["file_name"].encode('utf-8')).hexdigest() 1780 | monitor_file_name = selected_file_details["host"] + "_" + monitor_filter_name_hash 1781 | monitor_file_path = self.directory + self.monitored_urls_directory + monitor_file_name + ".old" if os.path.exists(self.directory + self.monitored_urls_directory + monitor_file_name + ".old") else self.directory + self.monitored_urls_directory + monitor_file_name 1782 | lines = selected_file_details["output"].strip().split("\n") 1783 | rows.extend(self.process_lines(lines=lines, selected_files_length=selected_files_length, selected_file_name=selected_file_name)) 1784 | if os.path.exists(monitor_file_path): 1785 | with open(monitor_file_path, "r") as f: 1786 | monitor_file_content = json.loads(f.read()) 1787 | try: 1788 | monitor_file_lines = set(monitor_file_content.strip().split("\n")) 1789 | except ValueError: 1790 | print("Error parsing the monitor file. Please ensure it contains valid JSON.") 1791 | monitor_file_lines = set() 1792 | selected_file_lines = set(selected_file_details["output"].strip().split("\n")) 1793 | new_or_changed_lines = selected_file_lines - monitor_file_lines 1794 | removed_lines = monitor_file_lines - selected_file_lines 1795 | new_or_changed_rows = self.process_lines(lines=new_or_changed_lines, selected_files_length=selected_files_length, selected_file_name=selected_file_name) 1796 | removed_rows = self.process_lines(lines=removed_lines, selected_files_length=selected_files_length, selected_file_name=selected_file_name) 1797 | differences.update(new_or_changed_rows) 1798 | deleted_lines.update(removed_rows) 1799 | if len(removed_rows) > 0: 1800 | rows.extend(removed_rows) 1801 | self.result_table_model.setColumnIdentifiers(["URL/Path", "Query Params", "Body Params", "Method", "Headers", "Type"] + (["File"] if selected_files_length > 1 else [])) 1802 | if self.hide_duplicates_checkbox.isSelected(): 1803 | type_column_index = self.result_table_model.findColumn("Type") 1804 | seen_rows = set() 1805 | rows = [row for row in rows if tuple(row[:type_column_index]) not in seen_rows and not seen_rows.add(tuple(row[:type_column_index]))] 1806 | if len(self.results_filters) > 0: 1807 | filtered_rows = [] 1808 | for row in rows: 1809 | if all((self.results_filters.get(f) == 'negative' and f not in row[0]) or 1810 | (self.results_filters.get(f) == 'positive' and f in row[0]) for f in self.results_filters): 1811 | filtered_rows.append(row) 1812 | rows = filtered_rows 1813 | if self.show_parameterized.isSelected(): 1814 | rows = [row for row in rows if row[1] != '' or row[2] != '' or row[4] != ''] 1815 | return rows 1816 | 1817 | def get_secrets_rows(self, selected_files_length, selected_files): 1818 | rows = [] 1819 | if self.selected_host and selected_files_length > 0: 1820 | self.secrets_table_model.setRowCount(0) 1821 | for selected_file_name in selected_files: 1822 | selected_file_details = None 1823 | for key, value in self.processed_file_details.items(): 1824 | if value["host"] == self.selected_host and value["file_name"] == selected_file_name: 1825 | selected_file_details = value 1826 | break 1827 | if selected_file_details and "secrets" in selected_file_details and selected_file_details["secrets"]: 1828 | self.secrets_table_model.setColumnIdentifiers(["kind", "data", "severity", "context"] + (["File"] if selected_files_length > 1 else [])) 1829 | lines = selected_file_details["secrets"].strip().split("\n") 1830 | for line in lines: 1831 | if line == "": 1832 | continue 1833 | data = json.loads(line, encoding='utf-8') 1834 | data_string = ", ".join(["{}:{}".format(key.encode('utf-8'), value.encode('utf-8')) for key, value in data["data"].items()]) 1835 | context_string = "" 1836 | if data.get("context") is not None: 1837 | context_string = ", ".join([u"{}:{}".format(key.encode('utf-8'), value.encode('utf-8')) for key, value in data["context"].items()]) 1838 | row_data = [ 1839 | data["kind"], 1840 | data_string, 1841 | data["severity"], 1842 | context_string, 1843 | selected_file_name 1844 | ] 1845 | rows.append(row_data) 1846 | if self.hide_duplicates_checkbox.isSelected(): 1847 | if selected_files_length > 1: 1848 | file_column_index = self.secrets_table_model.findColumn("File") 1849 | seen_rows = set() 1850 | rows = [row for row in rows if tuple(row[:file_column_index] + row[file_column_index+1:]) not in seen_rows and not seen_rows.add(tuple(row[:file_column_index] + row[file_column_index+1:]))] 1851 | else: 1852 | rows = [list(x) for x in set(tuple(x) for x in rows)] 1853 | return rows 1854 | 1855 | def filter_hosts(self): 1856 | selected_hosts = list(self.hosts_list) 1857 | current_selection = self.host_list.getSelectedValue() 1858 | in_scope_hosts = [] 1859 | if self.in_scope_checkbox.isSelected(): 1860 | in_scope_hosts = [host for host in selected_hosts if self._callbacks.isInScope(URL(str("http://" + host))) or self._callbacks.isInScope(URL(str("https://" + host)))] 1861 | current_selection = current_selection if current_selection in in_scope_hosts else None 1862 | self.host_list_model.clear() 1863 | for host in in_scope_hosts: 1864 | self.host_list_model.addElement(host) 1865 | else: 1866 | current_selection = current_selection if current_selection in selected_hosts else None 1867 | self.host_list_model.clear() 1868 | for host in selected_hosts: 1869 | self.host_list_model.addElement(host) 1870 | self.tabbed_pane_hosts.setTitleAt(0, str(self.host_list_model.size()) + " Hosts" if self.host_list_model.size() > 0 else "Hosts") 1871 | self.host_list_scroll_pane.setViewportView(self.host_list) 1872 | if current_selection: 1873 | index = in_scope_hosts.index(current_selection) if self.in_scope_checkbox.isSelected() else selected_hosts.index(current_selection) 1874 | self.host_list.setSelectedIndex(index) 1875 | elif len(in_scope_hosts) > 0: 1876 | self.host_list.setSelectedIndex(0) 1877 | else: 1878 | self.file_names_model.clear() 1879 | self.file_names_scroll_pane.setViewportView(self.file_names_list) 1880 | self.result_table_model.setRowCount(0) 1881 | self.tabbed_pane_results.setTitleAt(0, "Results") 1882 | self.tabbed_pane_files.setTitleAt(0, "Files") 1883 | if self.secrets_checkbox.isSelected(): 1884 | self.secrets_table_model.setRowCount(0) 1885 | self.tabbed_pane_results.setTitleAt(1, "Secrets") 1886 | self.host_list.clearSelection() 1887 | 1888 | def get_processed_file_names(self, selected_host): 1889 | return [value["file_name"] for value in self.processed_file_details.values() if value["host"] == selected_host] 1890 | 1891 | def get_result_tab_name(self): 1892 | selected_host = self.host_list.getSelectedValue() 1893 | selected_files = self.file_names_list.getSelectedValuesList() 1894 | num_rows = self.result_table_model.getRowCount() 1895 | if len(selected_files) == 1: 1896 | return "{} - {} ({} rows)".format(selected_host, selected_files[0], num_rows) 1897 | else: 1898 | return "{} - {} Files ({} rows)".format(selected_host, len(selected_files), num_rows) 1899 | 1900 | def getTabCaption(self): 1901 | return "jsluice++" 1902 | 1903 | def getUiComponent(self): 1904 | return self.panel 1905 | 1906 | 1907 | 1908 | 1909 | 1910 | 1911 | 1912 | 1913 | 1914 | --------------------------------------------------------------------------------