├── .gitignore ├── BappDescription.html ├── DefectDojoPlugin.py ├── LICENSE ├── README.md ├── docs ├── add-finding-to-test.gif ├── add-test-as-finding.gif └── install-plugin.gif └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | .vscode/ -------------------------------------------------------------------------------- /BappDescription.html: -------------------------------------------------------------------------------- 1 |

This extension exports findings directly to DefectDojo, 2 | an open-source application vulnerability management tool.

3 | -------------------------------------------------------------------------------- /DefectDojoPlugin.py: -------------------------------------------------------------------------------- 1 | from burp import IBurpExtender 2 | from thread import start_new_thread 3 | from burp import ITab 4 | from burp import IContextMenuFactory 5 | from burp import IContextMenuInvocation 6 | from burp import IScanIssue 7 | from burp import IExtensionStateListener 8 | from java.awt import Component 9 | from java.awt import Dimension 10 | from org.python.core.util import RelativeFile 11 | from java.io import PrintWriter 12 | from java.awt.event import ActionListener 13 | from java.awt.event import MouseAdapter 14 | import javax.swing.AbstractAction 15 | from jarray import array 16 | from java.util import ArrayList 17 | import javax.swing.Action 18 | from java.util import List 19 | from javax.swing import JScrollPane, GroupLayout, LayoutStyle 20 | from javax.swing import JSplitPane 21 | from javax.swing import JTabbedPane 22 | from javax.swing import JTable 23 | from javax.swing import JPanel 24 | from javax.swing import JLabel 25 | from javax.swing import JMenuItem 26 | from javax.swing import DefaultListModel 27 | from javax.swing import JList, JOptionPane 28 | from javax.swing import JTextField 29 | from javax.swing import JComboBox 30 | from javax.swing import JButton 31 | from javax.swing import JMenu 32 | from javax.swing import SwingUtilities 33 | from javax.swing.table import AbstractTableModel 34 | import random 35 | import string 36 | import ssl 37 | import time 38 | import json 39 | import sys 40 | import os 41 | import httplib 42 | from datetime import datetime 43 | from utils import EngListener, ProdListener, TestListener, ProdMouseListener 44 | from utils import SendReportToDojo, SendToDojo, html2text, ClickableLink, linkDialog 45 | 46 | __author__ = 'Alexandru Dracea' 47 | 48 | 49 | class DDTabUi(): 50 | def __init__(self, ext): 51 | self._panel = JPanel() 52 | layout = GroupLayout(self._panel) 53 | innerPanel = JPanel() 54 | innerPanelLayout = GroupLayout(innerPanel) 55 | self._panel.setLayout(layout) 56 | innerPanel.setLayout(innerPanelLayout) 57 | self.labelDojoURL = JLabel("DefectDojo :") 58 | self.defectDojoURL = JTextField("") 59 | self.searchConnectButton = JButton('Connect',actionPerformed=ext.getProducts) 60 | self.labelApiKey = JLabel("API Key :") 61 | self.apiKey = JTextField("") 62 | self.labelUsername = JLabel("Username :") 63 | self.user = JTextField("admin") 64 | self.labelProductID = JLabel("Product :") 65 | self.productID = JTextField(focusLost=ext.getEngagements) 66 | self.labelProductName = JLabel("Product Name :") 67 | self.productName = JComboBox() 68 | self.prodMan = ProdListener(ext) 69 | self.prodMouse = ProdMouseListener(ext) 70 | self.productName.addMouseListener(self.prodMouse) 71 | self.productName.addActionListener(self.prodMan) 72 | self.labelEngagementID = JLabel("Engagement (In Progress) :") 73 | self.engagementID = JTextField(focusLost=ext.getTests) 74 | self.engagementName = JComboBox() 75 | self.engMan = EngListener(ext) 76 | self.engagementName.addActionListener(self.engMan) 77 | self.labelTestID = JLabel("Test :") 78 | self.testID = JTextField() 79 | self.testName = JComboBox() 80 | self.testMan = TestListener(ext) 81 | self.testName.addActionListener(self.testMan) 82 | self.search = JTextField() 83 | self.search.setVisible(False) 84 | 85 | self.searchProductButton = JButton('Product Search', 86 | actionPerformed=ext.getProducts) 87 | innerPanelLayout.setHorizontalGroup( 88 | innerPanelLayout.createParallelGroup().addGroup( 89 | GroupLayout.Alignment.TRAILING, 90 | innerPanelLayout.createSequentialGroup().addContainerGap(). 91 | addGroup( 92 | innerPanelLayout.createParallelGroup( 93 | GroupLayout.Alignment.TRAILING). 94 | addGroup(innerPanelLayout.createParallelGroup().addGroup( 95 | innerPanelLayout.createParallelGroup( 96 | GroupLayout.Alignment.TRAILING).addGroup( 97 | innerPanelLayout.createParallelGroup( 98 | GroupLayout.Alignment.TRAILING).addGroup( 99 | innerPanelLayout.createSequentialGroup( 100 | ).addComponent( 101 | self.labelUsername, 102 | GroupLayout.PREFERRED_SIZE, 168, 103 | GroupLayout.PREFERRED_SIZE).addGap( 104 | 105, 105, 105)). 105 | addComponent(self.labelProductName, 106 | GroupLayout.Alignment.LEADING, 107 | GroupLayout.PREFERRED_SIZE, 168, 108 | GroupLayout.PREFERRED_SIZE)). 109 | addGroup( 110 | GroupLayout.Alignment.LEADING, 111 | innerPanelLayout.createSequentialGroup().addGroup( 112 | innerPanelLayout.createParallelGroup( 113 | GroupLayout.Alignment.TRAILING). 114 | addComponent( 115 | self.labelEngagementID, 116 | GroupLayout.Alignment.LEADING, 117 | GroupLayout.PREFERRED_SIZE, 168, 118 | GroupLayout.PREFERRED_SIZE).addComponent( 119 | self.labelDojoURL, 120 | GroupLayout.Alignment.LEADING, 121 | GroupLayout.PREFERRED_SIZE, 168, 122 | GroupLayout.PREFERRED_SIZE)). 123 | addPreferredGap( 124 | LayoutStyle.ComponentPlacement.RELATED)) 125 | ).addGroup(innerPanelLayout.createSequentialGroup( 126 | ).addGroup( 127 | innerPanelLayout.createParallelGroup().addComponent( 128 | self.labelTestID, GroupLayout.PREFERRED_SIZE, 168, 129 | GroupLayout.PREFERRED_SIZE).addComponent( 130 | self.searchProductButton, 131 | GroupLayout.PREFERRED_SIZE, 160, 132 | GroupLayout.PREFERRED_SIZE)).addPreferredGap( 133 | LayoutStyle.ComponentPlacement.RELATED))). 134 | addGroup( 135 | GroupLayout.Alignment.LEADING, 136 | innerPanelLayout.createSequentialGroup().addComponent( 137 | self.labelApiKey, GroupLayout.PREFERRED_SIZE, 168, 138 | GroupLayout.PREFERRED_SIZE).addPreferredGap( 139 | LayoutStyle.ComponentPlacement.RELATED))). 140 | addGroup(innerPanelLayout.createParallelGroup().addGroup( 141 | innerPanelLayout.createSequentialGroup().addComponent( 142 | self.engagementID, GroupLayout.PREFERRED_SIZE, 44, 143 | GroupLayout.PREFERRED_SIZE).addGap( 144 | 18, 18, 145 | 18).addComponent(self.engagementName, 146 | GroupLayout.PREFERRED_SIZE, 260, 147 | GroupLayout.PREFERRED_SIZE) 148 | ).addGroup(innerPanelLayout.createSequentialGroup().addGap( 149 | 54, 54, 54).addGroup( 150 | innerPanelLayout.createParallelGroup().addComponent( 151 | self.defectDojoURL, GroupLayout.PREFERRED_SIZE, 152 | 260, GroupLayout.PREFERRED_SIZE).addComponent( 153 | self.apiKey, GroupLayout.PREFERRED_SIZE, 260, 154 | GroupLayout.PREFERRED_SIZE).addComponent( 155 | self.user, GroupLayout.PREFERRED_SIZE, 260, 156 | GroupLayout.PREFERRED_SIZE).addComponent( 157 | self.productName, 158 | GroupLayout.PREFERRED_SIZE, 260, 159 | GroupLayout.PREFERRED_SIZE) 160 | )).addGroup( 161 | innerPanelLayout.createSequentialGroup().addComponent( 162 | self.testID, GroupLayout.PREFERRED_SIZE, 44, 163 | GroupLayout.PREFERRED_SIZE).addGap(18, 18, 18). 164 | addGroup(innerPanelLayout.createParallelGroup( 165 | ).addComponent( 166 | self.search, GroupLayout.PREFERRED_SIZE, 260, 167 | GroupLayout.PREFERRED_SIZE).addComponent( 168 | self.testName, GroupLayout.PREFERRED_SIZE, 260, 169 | GroupLayout.PREFERRED_SIZE)))).addGap( 170 | 348, 348, 348))) 171 | innerPanelLayout.setVerticalGroup(innerPanelLayout.createParallelGroup( 172 | ).addGroup(innerPanelLayout.createSequentialGroup().addContainerGap( 173 | ).addGroup( 174 | innerPanelLayout.createParallelGroup( 175 | GroupLayout.Alignment.LEADING, 176 | False).addComponent(self.defectDojoURL).addComponent( 177 | self.labelDojoURL, GroupLayout.DEFAULT_SIZE, 178 | GroupLayout.DEFAULT_SIZE, sys.maxint) 179 | ).addPreferredGap(LayoutStyle.ComponentPlacement.RELATED).addGroup( 180 | innerPanelLayout.createParallelGroup( 181 | GroupLayout.Alignment.BASELINE).addComponent( 182 | self.apiKey, GroupLayout.PREFERRED_SIZE, 183 | GroupLayout.DEFAULT_SIZE, 184 | GroupLayout.PREFERRED_SIZE).addComponent( 185 | self.labelApiKey, GroupLayout.PREFERRED_SIZE, 19, 186 | GroupLayout.PREFERRED_SIZE) 187 | ).addPreferredGap(LayoutStyle.ComponentPlacement.RELATED).addGroup( 188 | innerPanelLayout.createParallelGroup( 189 | GroupLayout.Alignment.BASELINE).addComponent( 190 | self.user, GroupLayout.PREFERRED_SIZE, 191 | GroupLayout.DEFAULT_SIZE, 192 | GroupLayout.PREFERRED_SIZE).addComponent( 193 | self.labelUsername, GroupLayout.PREFERRED_SIZE, 19, 194 | GroupLayout.PREFERRED_SIZE) 195 | ).addPreferredGap(LayoutStyle.ComponentPlacement.RELATED).addGroup( 196 | innerPanelLayout.createParallelGroup( 197 | GroupLayout.Alignment.BASELINE).addComponent( 198 | self.productName, GroupLayout.PREFERRED_SIZE, 199 | GroupLayout.DEFAULT_SIZE, 200 | GroupLayout.PREFERRED_SIZE).addComponent( 201 | self.labelProductName, GroupLayout.PREFERRED_SIZE, 19, 202 | GroupLayout.PREFERRED_SIZE) 203 | ).addPreferredGap(LayoutStyle.ComponentPlacement.RELATED).addGroup( 204 | innerPanelLayout.createParallelGroup( 205 | GroupLayout.Alignment.BASELINE).addComponent( 206 | self.engagementName, GroupLayout.PREFERRED_SIZE, 207 | GroupLayout.DEFAULT_SIZE, 208 | GroupLayout.PREFERRED_SIZE).addComponent( 209 | self.engagementID, GroupLayout.PREFERRED_SIZE, 210 | GroupLayout.DEFAULT_SIZE, 211 | GroupLayout.PREFERRED_SIZE).addComponent( 212 | self.labelEngagementID, GroupLayout.PREFERRED_SIZE, 213 | 19, GroupLayout.PREFERRED_SIZE) 214 | ).addPreferredGap(LayoutStyle.ComponentPlacement.RELATED).addGroup( 215 | innerPanelLayout.createParallelGroup( 216 | GroupLayout.Alignment.BASELINE).addComponent( 217 | self.testName, GroupLayout.PREFERRED_SIZE, 218 | GroupLayout.DEFAULT_SIZE, 219 | GroupLayout.PREFERRED_SIZE).addComponent( 220 | self.testID, GroupLayout.PREFERRED_SIZE, 221 | GroupLayout.DEFAULT_SIZE, 222 | GroupLayout.PREFERRED_SIZE).addComponent( 223 | self.labelTestID, GroupLayout.PREFERRED_SIZE, 19, 224 | GroupLayout.PREFERRED_SIZE) 225 | ).addPreferredGap(LayoutStyle.ComponentPlacement.RELATED).addGroup( 226 | innerPanelLayout.createParallelGroup( 227 | GroupLayout.Alignment.LEADING, False).addComponent( 228 | self.search, GroupLayout.DEFAULT_SIZE, 229 | GroupLayout.DEFAULT_SIZE, sys.maxint).addComponent( 230 | self.searchProductButton)).addContainerGap( 231 | 131, sys.maxint))) 232 | layout.setHorizontalGroup(layout.createParallelGroup().addGroup( 233 | layout.createSequentialGroup().addComponent( 234 | innerPanel, GroupLayout.PREFERRED_SIZE, 633, 235 | GroupLayout.PREFERRED_SIZE).addGap(0, 312, sys.maxint))) 236 | layout.setVerticalGroup(layout.createParallelGroup().addGroup( 237 | layout.createSequentialGroup().addComponent( 238 | innerPanel, GroupLayout.PREFERRED_SIZE, 239 | GroupLayout.DEFAULT_SIZE, 240 | GroupLayout.PREFERRED_SIZE).addGap(0, 199, sys.maxint))) 241 | 242 | 243 | class BurpExtender(IBurpExtender, ITab, IExtensionStateListener): 244 | def registerExtenderCallbacks(self, callbacks): 245 | self._callbacks = callbacks 246 | self._helpers = callbacks.getHelpers() 247 | callbacks.setExtensionName("DefectDojo") 248 | self.ddui = DDTabUi(self) 249 | self.sender = HttpData(self.ddui.defectDojoURL.getText(), 250 | self.ddui.user.getText(), 251 | self.ddui.apiKey.getText()) 252 | self.ddui.defectDojoURL.setText(callbacks.loadExtensionSetting("DD_URL")) 253 | self.ddui.apiKey.setText(callbacks.loadExtensionSetting("DD_API")) 254 | callbacks.registerExtensionStateListener(self) 255 | self.contextMenu = SendToDojo(self) 256 | self.contextMenu2 = SendReportToDojo(self) 257 | callbacks.registerContextMenuFactory(self.contextMenu) 258 | callbacks.registerContextMenuFactory(self.contextMenu2) 259 | callbacks.customizeUiComponent(self.ddui._panel) 260 | callbacks.addSuiteTab(self) 261 | return 262 | 263 | def extensionUnloaded(self): 264 | callbacks = self._callbacks 265 | callbacks.saveExtensionSetting("DD_URL", self.ddui.defectDojoURL.getText()) 266 | callbacks.saveExtensionSetting("DD_API", self.ddui.apiKey.getText()) 267 | 268 | def getTabCaption(self): 269 | return "DefectDojo" 270 | 271 | def getUiComponent(self): 272 | return self.ddui._panel 273 | 274 | def getProducts(self, event): 275 | """ 276 | Updates the list of products from the API , and also makes the call 277 | to retreive the userId behind the scenes . 278 | """ 279 | self.ddui.productName.removeAllItems() 280 | start_new_thread(self.doGetProducts, ()) 281 | 282 | def doGetProducts(self): 283 | self.checkUpdateSender() 284 | self.sender.makeRequest( 285 | 'GET', '/api/v2/products/' + 286 | self._helpers.urlEncode(self.ddui.search.getText())+"?limit=4000&offset=0") 287 | data = self.sender.req_data 288 | test = DefectDojoResponse(message="Done", 289 | data=data, 290 | success=True) 291 | self.ddui.products = test 292 | if test.data: 293 | for objects in test.data['results']: 294 | self.ddui.productName.addItem(objects['name']) 295 | start_new_thread(self.getUserId, ()) 296 | else: 297 | JOptionPane.showMessageDialog(None, "Error connecting to DefectDojo, check the API key and Username.", 298 | "Error", 299 | JOptionPane.WARNING_MESSAGE) 300 | 301 | def getEngagements(self, event): 302 | """ 303 | Updates the list of engagements from the API 304 | """ 305 | self.ddui.engagementName.removeAllItems() 306 | start_new_thread(self.doGetEngagements, ()) 307 | 308 | def doGetEngagements(self): 309 | self.checkUpdateSender() 310 | selected = self.ddui.productName.selectedIndex 311 | selectedProduct = str(self.ddui.products.data['results'][selected]['id']) 312 | self.sender.makeRequest( 313 | 'GET', '/api/v2/engagements/?product=' + 314 | self._helpers.urlEncode(selectedProduct) + 315 | '&status=In%20Progress&limit=4000&offset=0') 316 | data = self.sender.req_data 317 | test = DefectDojoResponse(message="Done", 318 | data=data, 319 | success=True) 320 | self.ddui.engagements = test 321 | if test.data: 322 | for objects in test.data['results']: 323 | self.ddui.engagementName.addItem(objects['name']) 324 | 325 | def getTests(self, event): 326 | """ 327 | Updates the list containing test names based on test_type+date created 328 | so that there is some visual indicator . 329 | """ 330 | self.ddui.testName.removeAllItems() 331 | self.checkUpdateSender() 332 | start_new_thread(self.doGetTests, ()) 333 | 334 | def doGetTests(self): 335 | self.checkUpdateSender() 336 | self.sender.makeRequest( 337 | 'GET', '/api/v2/tests/?engagement=' + 338 | self._helpers.urlEncode(self.ddui.engagementID.getText())+"&limit=4000&offset=0") 339 | data = self.sender.req_data 340 | test = DefectDojoResponse(message="Done", 341 | data=data, 342 | success=True) 343 | self.ddui.tests = test 344 | if test.data: 345 | for objects in test.data['results']: 346 | d = datetime.strptime(str(objects['created'][:-1]), '%Y-%m-%dT%H:%M:%S.%f') 347 | self.ddui.testName.addItem(str(objects['test_type_name']) + " (" + d.strftime("%Y/%m/%d-%H:%M:%S") + ")") 348 | 349 | def getUserId(self): 350 | self.checkUpdateSender() 351 | self.sender.makeRequest('GET', '/api/v2/users/') 352 | data = self.sender.req_data 353 | test = DefectDojoResponse(message="Done", 354 | data=data, 355 | success=True) 356 | if test.data["count"] > 0: 357 | for objects in test.data['results']: 358 | if self.ddui.user.getText() == objects['username']: 359 | self.ddui.userID = objects['id'] 360 | break 361 | else: 362 | self.ddui.userID = -1 363 | if self.ddui.userID == -1: 364 | linkDialog(self.ddui.user.getText()+" does not exist !","", JOptionPane, self.getUiComponent().parent) 365 | 366 | def sendAsReport(self, event): 367 | """ 368 | This sends selected issues(>=1) to DefectDojo bundled as a report , 369 | this will mean that they will be imported into a new test each time . 370 | """ 371 | checkMessage = self.checkSelection("engagement") 372 | if checkMessage: 373 | JOptionPane.showMessageDialog(self.getUiComponent().parent, checkMessage, 374 | "Error", 375 | JOptionPane.WARNING_MESSAGE) 376 | return 377 | if hasattr(self.ddui, 'userID'): 378 | pass 379 | else: 380 | self.getUserId() 381 | f = RelativeFile("Scan.xml") 382 | f.createNewFile() 383 | if event.getActionCommand() == "Send All Issues to DefectDojo (New Burp Test)": 384 | ctr = 0 385 | for urls in self.contextMenu._invoker.getSelectedMessages(): 386 | url = "" 387 | if urls.getHttpService().getProtocol( 388 | ) == 'http' and urls.getHttpService().getPort() == 80: 389 | url_loc = str(urls.getUrl()).split('/') 390 | url_loc = '/'.join(url_loc[3:]) 391 | url = "http://" + urls.getHttpService().getHost( 392 | ) + '/' + url_loc 393 | elif urls.getHttpService().getProtocol( 394 | ) == 'https' and urls.getHttpService().getPort() == 443: 395 | url_loc = str(urls.getUrl()).split('/') 396 | url_loc = '/'.join(url_loc[3:]) 397 | url = "https://" + urls.getHttpService().getHost( 398 | ) + '/' + url_loc 399 | else: 400 | url = str(urls.getUrl()) 401 | if ctr == 0: 402 | issues = self._callbacks.getScanIssues(url) 403 | ctr += 1 404 | else: 405 | for iss in self._callbacks.getScanIssues(url): 406 | issues.append(iss) 407 | ctr += 1 408 | self._callbacks.generateScanReport("XML", issues, f) 409 | else: 410 | self._callbacks.generateScanReport( 411 | "XML", self.contextMenu._invoker.getSelectedIssues(), f) 412 | ct_boundry = ''.join(random.SystemRandom().choice(string.hexdigits) 413 | for _ in range(30)) 414 | self.sender.headers[ 415 | 'Content-Type'] = 'multipart/form-data; boundary=----------' \ 416 | + ct_boundry 417 | import datetime 418 | now = datetime.datetime.now() 419 | content = { 420 | "minimum_severity": "Info", 421 | "scan_date": "%d-%d-%d" % (now.year, now.month, now.day), 422 | "tags": "BurpPlugin", 423 | "active": "True", 424 | "verified": "True", 425 | "engagement": self.ddui.engagementID.getText(), 426 | "scan_type": "Burp Scan" 427 | } 428 | nl = '\r\n' 429 | data = [] 430 | for (key, value) in content.iteritems(): 431 | data.append('------------' + ct_boundry) 432 | data.append('Content-Disposition: form-data; name="%s";' % (key)) 433 | data.append('') 434 | data.append(value) 435 | data.append('------------' + ct_boundry) 436 | data.append( 437 | 'Content-Disposition: form-data;name="file"; filename="Scan.xml";') 438 | data.append('Content-Type: application/xml') 439 | data.append('') 440 | with open("./Scan.xml") as a: 441 | for line in a: 442 | data.append(line.encode('utf8', 'replace')) 443 | os.remove("./Scan.xml") 444 | data.append('------------' + ct_boundry + '--') 445 | body_s = nl.join(data) 446 | data2 = open("./Data.txt", "w") 447 | data2.write(body_s) 448 | data2.close() 449 | data2 = open('./Data.txt', "r") 450 | self.checkUpdateSender() 451 | start_new_thread(self.sender.makeRequest,('POST', '/api/v2/import-scan/', data2, self.getUiComponent().parent)) 452 | def checkSelection(self, action): 453 | message = "" 454 | if action=="test": 455 | if self.ddui.testID.getText() is None or self.ddui.testID.getText() == "": 456 | message = "No test selected, please select a test in the DefectDojo configuration tab. " 457 | elif action=="engagement": 458 | if self.ddui.engagementID.getText() is None or self.ddui.engagementID.getText() == "": 459 | message = "No engagement selected, please select a test in the DefectDojo configuration tab. " 460 | 461 | return message 462 | 463 | def sendIssue(self, event): 464 | """ 465 | This sends selected issues(>=1) to DefectDojo be they selected from 466 | the DefectDojo Tab or the Context Menu in the Target Tab . 467 | Due to the current limitations in DefectDojo API request/response 468 | pairs cannot be added *yet* . 469 | """ 470 | checkMessage = self.checkSelection("test") 471 | if checkMessage: 472 | JOptionPane.showMessageDialog(self.getUiComponent().parent, checkMessage, 473 | "Error", 474 | JOptionPane.WARNING_MESSAGE) 475 | return 476 | if hasattr(self.ddui, 'userID'): 477 | pass 478 | else: 479 | self.getUserId() 480 | if event.getActionCommand() == 'Send To DefectDojo (Existing Test)': 481 | lgt = len(self.contextMenu._invoker.getSelectedIssues()) 482 | issues = self.contextMenu._invoker.getSelectedIssues() 483 | elif event.getActionCommand() == 'Send Issue': 484 | lgt = len(self.ddui._listTargetIss.getSelectedIndices()) 485 | issues = self.ddui._listTargetIss.getSelectedIndices() 486 | for i in range(lgt): 487 | ureqresp = [] 488 | if event.getActionCommand() == 'Send To DefectDojo (Existing Test)': 489 | title = issues[i].getIssueName() 490 | description = issues[i].getIssueDetail( 491 | ) if issues[i].getIssueDetail( 492 | ) else issues[i].getIssueBackground() 493 | severity = issues[i].getSeverity() 494 | if severity == 'Information' or severity == 'Informational': 495 | severity = "Info" 496 | impact = issues[i].getIssueBackground() 497 | if issues[i].getRemediationBackground(): 498 | mitigation = issues[i].getRemediationBackground() + '\n' 499 | if issues[i].getRemediationDetail(): 500 | mitigation += issues[i].getRemediationDetail() 501 | else: 502 | mitigation = str(issues[i].getIssueType()) 503 | for mess in issues[i].getHttpMessages(): 504 | ureqresp.append({ 505 | "req": 506 | self._helpers.bytesToString(mess.getRequest()), 507 | "resp": 508 | self._helpers.bytesToString(mess.getResponse()) 509 | }) 510 | url = str(issues[i].getUrl()) 511 | description = html2text(description) 512 | impact = html2text(impact) 513 | mitigation = html2text(mitigation) 514 | try: 515 | json.loads(description) 516 | except: 517 | description = description.replace("\'", "") 518 | try: 519 | json.loads(impact) 520 | except: 521 | impact = impact.replace("\'", "") 522 | try: 523 | json.loads(mitigation) 524 | except: 525 | mitigation = mitigation.replace("\'", "") 526 | data = { 527 | 'title': title, 528 | 'description': description, 529 | 'severity': severity, 530 | "found_by": [self.ddui.userID], 531 | 'test': int(self._helpers.urlEncode(self.ddui.testID.getText())), 532 | 'impact': impact, 533 | 'active': True, 534 | 'verified': True, 535 | 'mitigation': mitigation, 536 | 'static_finding': False, 537 | 'dynamic_finding': False, 538 | "false_p" : False, 539 | "duplicate" : False, 540 | "numerical_severity":"S0", 541 | } 542 | data = json.dumps(data) 543 | self.checkUpdateSender() 544 | start_new_thread(self.sender.makeRequest, 545 | ('POST', '/api/v2/findings/', data)) 546 | 547 | message = str("Successfully imported (" + str(i+1) + ") selected issue(s). Access Test : " 548 | + self.ddui.testID.getText()) 549 | link = str(self.ddui.defectDojoURL.getText() + "test/" + self.ddui.testID.getText()) 550 | linkDialog(message, link, JOptionPane, self.getUiComponent().parent) 551 | 552 | def checkUpdateSender(self): 553 | if self.sender.ddurl != self.ddui.defectDojoURL.getText().lower( 554 | ).split('://'): 555 | self.sender.setUrl(self.ddui.defectDojoURL.getText()) 556 | if self.sender.user != self.ddui.user.getText(): 557 | self.sender.setUser(self.ddui.user.getText()) 558 | if self.ddui.apiKey.getText() != self.sender.apikey: 559 | self.sender.setApiKey(self.ddui.apiKey.getText()) 560 | 561 | 562 | class HttpData(): 563 | req_data = '' 564 | ddurl = '' 565 | user = '' 566 | apikey = '' 567 | 568 | def __init__(self, ddurl, user, apikey): 569 | self.ddurl = ddurl.lower().split('://') 570 | self.user = user 571 | self.apikey = apikey 572 | self.headers = { 573 | 'Content-Type': 'application/json', 574 | 'User-Agent': 'Defectdojo burpsuite plugin', 575 | 'Authorization': "Token "+ self.apikey 576 | } 577 | 578 | def setUrl(self, ddurl): 579 | ddurl = ddurl.rstrip("/") 580 | self.ddurl = ddurl.lower().split('://') 581 | 582 | def setApiKey(self, apikey): 583 | self.apikey = apikey 584 | self.headers['Authorization'] = "Token " + apikey 585 | 586 | def setUser(self, user): 587 | self.user = user 588 | 589 | def makeRequest(self, method, url, data=None, src=None): 590 | if self.ddurl[0] == 'http': 591 | conn = httplib.HTTPConnection(self.ddurl[1]) 592 | elif self.ddurl[0] == 'https': 593 | conn = httplib.HTTPSConnection(self.ddurl[1]) 594 | 595 | try: 596 | # url = url.rstrip("/") 597 | 598 | if data: 599 | conn.request(method, url, body=data, headers=self.headers) 600 | else: 601 | conn.request(method, url, headers=self.headers) 602 | response = conn.getresponse() 603 | self.req_data = response.read() 604 | conn.close() 605 | if url == '/api/v2/import-scan/': 606 | try: 607 | message = str("Successfully imported selected issues.") 608 | link = str(self.ddurl[0] + "://" + self.ddurl[1] + "/test/" ) 609 | linkDialog(message, link, JOptionPane, src) 610 | except Exception as ex: 611 | JOptionPane.showMessageDialog(src, "Import possibly failed!", 612 | "Error", 613 | JOptionPane.WARNING_MESSAGE) 614 | print "Error: " + str(ex) 615 | except Exception as ex: 616 | JOptionPane.showMessageDialog(src, "Error connecting to DefectDojo, double check the URL.", 617 | "Error", 618 | JOptionPane.WARNING_MESSAGE) 619 | print "Error: " + str(ex) 620 | pass 621 | try: 622 | os.remove("./Data.txt") 623 | except: 624 | pass 625 | if self.headers['Content-Type'] != 'application/json': 626 | self.headers['Content-Type'] = 'application/json' 627 | return 628 | 629 | 630 | class DefectDojoResponse(object): 631 | """ 632 | Container for all DefectDojo API responses, even errors. 633 | """ 634 | 635 | def __init__(self, message, success, data=None, response_code=-1): 636 | self.message = message 637 | self.data = None 638 | if data: 639 | self.data = json.loads(data) 640 | self.success = success 641 | self.response_code = response_code 642 | 643 | def __str__(self): 644 | if self.data: 645 | return str(self.data) 646 | else: 647 | return self.message 648 | 649 | def id(self): 650 | if self.response_code == 400: # Bad Request 651 | raise ValueError('Object not created:' + json.dumps( 652 | self.data, sort_keys=True, indent=4, separators=(',', ': '))) 653 | return int(self.data) 654 | 655 | def count(self): 656 | return self.data["meta"]["total_count"] 657 | 658 | def data_json(self, pretty=False): 659 | """Returns the data as a valid JSON string.""" 660 | if pretty: 661 | return json.dumps(self.data, 662 | sort_keys=True, 663 | indent=4, 664 | separators=(',', ': ')) 665 | else: 666 | return json.dumps(self.data) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 DefectDojo 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 | # THIS REPO HAS BEEN ARCHIVED 2 | 3 | It will be marked as read-only in case someone wants to fork and restart the effort. 4 | 5 | Currently, the Burp plug-in depends on jython, which is now outdated and it has been a year since a formal release. It's likely this will require a complete re-write and/or creating a plugin in Java. 6 | 7 | ### Defecto-Burp 8 | 9 | Defecto-Burp is a Burp Suite plugin used with Burpsuite Professional that support the defectdojo **API v2**. 10 | 11 | This extension is designed to export findings to [DefectDojo](https://github.com/DefectDojo/django-DefectDojo). 12 | 13 | Features : 14 | * Send findings to existing test 15 | * Create a new test via the Burp plugin 16 | * Supports the defectdojo **API v2** 17 | 18 | #### Contribute 19 | Feedback, testing and issue reporting is welcome. 20 | 21 | ### Installation 22 | In order for the plugin to work , you will need to have Jython set up in Burp Suite Pro . 23 | To use this plugin before it appears in the BApp Store you will need to do the following : 24 | 1. Go to `Extender` and select the `Extensions` tab 25 | 2. Click on `Add` , select `Extension Type:` to be `Python` and select the `DefectDojoPlugin.py` 26 | 27 | ![Install Plugin](https://raw.githubusercontent.com/ihebski/Burp-Plugin/master/docs/install-plugin.gif) 28 | 29 | ### Usage 30 | * Send finding to existing test 31 | 32 | ![Add finding](https://raw.githubusercontent.com/ihebski/Burp-Plugin/master/docs/add-finding-to-test.gif) 33 | 34 | * Send issue As Report (add test) 35 | 36 | ![send issue As Report](https://raw.githubusercontent.com/ihebski/Burp-Plugin/master/docs/add-test-as-finding.gif) 37 | 38 | -------------------------------------------------------------------------------- /docs/add-finding-to-test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefectDojo/Burp-Plugin/deae81a89c51025097a389de89fadccbb0a3abb5/docs/add-finding-to-test.gif -------------------------------------------------------------------------------- /docs/add-test-as-finding.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefectDojo/Burp-Plugin/deae81a89c51025097a389de89fadccbb0a3abb5/docs/add-test-as-finding.gif -------------------------------------------------------------------------------- /docs/install-plugin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefectDojo/Burp-Plugin/deae81a89c51025097a389de89fadccbb0a3abb5/docs/install-plugin.gif -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | from java.awt.event import ActionListener, MouseAdapter 2 | from thread import start_new_thread 3 | from java.awt.event import MouseAdapter 4 | from burp import IContextMenuFactory 5 | from burp import IContextMenuInvocation 6 | from javax.swing import JMenuItem 7 | from javax.swing import JButton 8 | from java.awt import Desktop 9 | from java.lang import Runtime 10 | import platform 11 | import os 12 | from java import net 13 | 14 | 15 | class ClickableLink(): 16 | """ 17 | This method generates a clickable button inside the prompt 18 | after the report is uploaded . It supposedly opens 19 | your default browser on the test's page . 20 | """ 21 | def __init__(self,text,url): 22 | self.text = text 23 | self.url = url 24 | def getClickAbleLink(self): 25 | self.link = JButton(actionPerformed=self.openURL) 26 | self.link.setText(self.text) 27 | return self.link 28 | def openURL(self,event): 29 | try: 30 | Desktop.getDesktop().browse(net.URI(self.url)) 31 | except: 32 | Runtime.getRuntime().exec("xdg-open "+self.url) 33 | 34 | def linkDialog(message, link, JOptionPane, src): 35 | lbl = ClickableLink(message, link) 36 | JOptionPane.showMessageDialog(src, lbl.getClickAbleLink()) 37 | 38 | def html2text(strText): 39 | """ 40 | This method transforms burp's HTML tags into normal 41 | text . 42 | TODO tranform into markdown . 43 | """ 44 | html = str(strText).encode('utf8', 'replace') 45 | int2 = html.lower().find(" 0: 47 | html = html[int2:] 48 | int2 = html.lower().find("") 49 | if int2 > 0: 50 | html = html[:int2] 51 | list1 = ['
', '', 'span>', 'li>', ''] 52 | list2 = [ 53 | chr(13), 54 | chr(13), 55 | chr(9), 56 | chr(13), 57 | chr(13), 58 | chr(13), 59 | chr(13), 60 | chr(13) 61 | ] 62 | f1 = True 63 | f2 = True 64 | toText = "" 65 | for int1 in range(len(html)): 66 | str2 = html[int1] 67 | for int2 in range(len(list1)): 68 | if html[int1:int1 + len(list1[int2])].lower() == list1[int2]: 69 | toText = toText + list2[int2] 70 | if str2 == '<': 71 | f2 = False 72 | if f1 and f2 and (ord(str2) != 10): 73 | toText = toText + str2 74 | if str2 == '>': 75 | f2 = True 76 | if f1 and f2: 77 | toText = toText.replace(chr(32) + chr(13), chr(13)) 78 | toText = toText.replace(chr(9) + chr(13), chr(13)) 79 | toText = toText.replace(chr(13) + chr(32), chr(13)) 80 | toText = toText.replace(chr(13) + chr(9), chr(13)) 81 | toText = toText.replace(chr(13) + chr(13), chr(13)) 82 | return toText 83 | 84 | 85 | class SendToDojo(IContextMenuFactory): 86 | """ 87 | SendToDojo implements the class needed to create the context 88 | menu when rightclicking an issue . 89 | """ 90 | 91 | def __init__(self, data): 92 | self.a = data 93 | 94 | def createMenuItems(self, invoker): 95 | self._invoker = invoker 96 | context = self._invoker.getInvocationContext() 97 | if not context == self._invoker.CONTEXT_SCANNER_RESULTS: 98 | return None 99 | self.selection = JMenuItem("Send To DefectDojo (Existing Test)", 100 | actionPerformed=self.a.sendIssue) 101 | return [self.selection] 102 | 103 | 104 | class SendReportToDojo(IContextMenuFactory): 105 | """ 106 | SendReportToDojo implements the class needed to create 107 | the context menu when rightclicking an issue(s) in order 108 | to send them as a report to Defect Dojo . 109 | """ 110 | 111 | def __init__(self, data): 112 | self.a = data 113 | 114 | def createMenuItems(self, invoker): 115 | self._invoker = invoker 116 | context = self._invoker.getInvocationContext() 117 | if not (context == self._invoker.CONTEXT_SCANNER_RESULTS 118 | or context == self._invoker.CONTEXT_TARGET_SITE_MAP_TREE): 119 | return None 120 | if context == self._invoker.CONTEXT_SCANNER_RESULTS: 121 | self.selection = JMenuItem("Send Report To DefectDojo (New Burp Test)", 122 | actionPerformed=self.a.sendAsReport) 123 | return [self.selection] 124 | else: 125 | self.selection = JMenuItem("Send All Issues to DefectDojo (New Burp Test)", 126 | actionPerformed=self.a.sendAsReport) 127 | return [self.selection] 128 | 129 | 130 | class ProdListener(ActionListener): 131 | """ 132 | Updates the productID field based on the selection from the ComboBox 133 | """ 134 | 135 | def __init__(self, data): 136 | self.a = data 137 | 138 | def actionPerformed(self, e): 139 | cmd = e.getActionCommand() 140 | if cmd == 'comboBoxChanged': 141 | selected = self.a.ddui.productName.selectedIndex 142 | if selected >= 0: 143 | self.a.ddui.productID.setText( 144 | str(self.a.ddui.products.data['results'][selected]['id'])) 145 | start_new_thread(self.a.getEngagements, (e, )) 146 | 147 | class ProdMouseListener(MouseAdapter): 148 | """ 149 | Fetches products for the product ComboBox 150 | """ 151 | 152 | def __init__(self, data): 153 | self.a = data 154 | 155 | def mousePressed(self, event): 156 | self.a.getProducts(event) 157 | 158 | class EngListener(ActionListener): 159 | """ 160 | Updates the engagementID field based on the selection from the ComboBox 161 | """ 162 | 163 | def __init__(self, data): 164 | self.a = data 165 | 166 | def actionPerformed(self, e): 167 | cmd = e.getActionCommand() 168 | if cmd == 'comboBoxChanged': 169 | selected = self.a.ddui.engagementName.selectedIndex 170 | if selected >= 0: 171 | self.a.ddui.engagementID.setText( 172 | str(self.a.ddui.engagements.data['results'][selected]['id'])) 173 | start_new_thread(self.a.getTests, (e, )) 174 | 175 | 176 | class TestListener(ActionListener): 177 | """ 178 | Updates the testID field based on the selection from the ComboBox 179 | """ 180 | 181 | def __init__(self, data): 182 | self.a = data 183 | 184 | def actionPerformed(self, e): 185 | cmd = e.getActionCommand() 186 | if cmd == 'comboBoxChanged': 187 | selected = self.a.ddui.testName.selectedIndex 188 | if selected >= 0: 189 | self.a.ddui.testID.setText( 190 | str(self.a.ddui.tests.data['results'][selected]['id'])) 191 | --------------------------------------------------------------------------------