├── .classpath ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── doubleclick.gif └── google.gif ├── .gitignore ├── .gitmodules ├── .project ├── .settings └── org.eclipse.buildship.core.prefs ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── build.gradle ├── config.json ├── docs ├── configuration.md └── technical-details.md ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── release └── eslinter-all.jar └── src ├── burp ├── BurpExtender.java ├── CheckPaths.java ├── Config.java ├── Detective.java └── Extractor.java ├── database ├── Database.java └── SelectResult.java ├── gui └── BurpTab.java ├── lint ├── Beautify.java ├── Lint.java ├── LintTask.java ├── LintingThread.java ├── Metadata.java ├── ProcessRequestTask.java ├── ProcessResponseTask.java └── UpdateTableThread.java ├── linttable ├── LintResult.java ├── LintTable.java └── LintTableModel.java ├── resources └── db │ ├── add_row.sql │ ├── check_already_processed-trigger.sql │ ├── create_table.sql │ ├── get-all_rows.sql │ ├── get_new_rows.sql │ ├── update_hash-trigger.sql │ └── update_row.sql └── utils ├── BurpLog.java ├── Constants.java ├── CustomException.java ├── Exec.java ├── FileChooser.java ├── Header.java ├── PausableExecutor.java ├── ReqResp.java ├── Resources.java ├── StringUtils.java └── SystemUtils.java /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Please fill the bug report information as much as you can 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Error Message** 14 | ``` 15 | Paste the error message from Burp here. 16 | ``` 17 | 18 | **Platform** 19 | E.g., Windows, Linux, OpenFreeNetBSD. 20 | 21 | **Config file** 22 | Please remove any identifying information from the config file (especially in 23 | the paths). 24 | 25 | ```json 26 | Paste your config file here. 27 | ``` 28 | 29 | **Screenshots (optional)** 30 | Remove this section if it's not used. If applicable, add screenshots to help 31 | explain your problem. Not necessary. 32 | 33 | **Additional context (optional)** 34 | Remove this section if it's not used. Add any other context about the problem 35 | here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **If the feature is already mentioned in the FAQ. (e.g., "Creating Burp issues from ESLint results."), why is it important?** 11 | Please tell me why you need this feature and why it's important. 12 | 13 | **Is your feature request related to a problem? Please describe.** 14 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 15 | 16 | **Describe the solution you'd like** 17 | A clear and concise description of what you want to happen. 18 | -------------------------------------------------------------------------------- /.github/doubleclick.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parsiya/eslinter/f2b58eaee43cd724678e04c602f3844a67a69558/.github/doubleclick.gif -------------------------------------------------------------------------------- /.github/google.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parsiya/eslinter/f2b58eaee43cd724678e04c602f3844a67a69558/.github/google.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | # *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | # project specific ignores 26 | .gradle 27 | build 28 | bin 29 | release/config.json 30 | 31 | # burp console and error output files 32 | console-output.txt 33 | error-output.txt 34 | *.exe 35 | 36 | # test directories 37 | nem 38 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "eslint-security"] 2 | path = eslint-security 3 | url = https://github.com/parsiya/eslint-security 4 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | eslint-burp 4 | Project eslint-burp created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | 25 | 1614986489126 26 | 27 | 30 28 | 29 | org.eclipse.core.resources.regexFilterMatcher 30 | node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | arguments= 2 | auto.sync=false 3 | build.scans.enabled=false 4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) 5 | connection.project.dir= 6 | eclipse.preferences.version=1 7 | gradle.user.home= 8 | java.home=C\:/Program Files/AdoptOpenJDK/jdk-11.0.7.10-hotspot 9 | jvm.arguments= 10 | offline.mode=false 11 | override.workspace.settings=true 12 | show.console.view=true 13 | show.executions.view=true 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "java", 6 | "name": "BurpExtension", 7 | "request": "attach", 8 | "hostName": "localhost", 9 | "port": 8000 // Change this if you had set a different debug port. 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.configuration.updateBuildConfiguration": "automatic", 3 | "files.exclude": { 4 | "**/.classpath": true, 5 | "**/.project": true, 6 | "**/.settings": true, 7 | "**/.factorypath": true 8 | } 9 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "gradle", 8 | "type": "shell", 9 | "command": "gradlew.bat bigjar", // Wrapper on Windows 10 | // "command": "gradlew bigjar", // Wrapper on *nix 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | } 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Manual JavaScript Linting is a Bug 2 | `ESLinter` is a Burp extension that extracts JavaScript from responses and lints 3 | them with [ESLint][eslint-org] while you do your manual testing. 4 | 5 | [eslint-org]: https://eslint.org/ 6 | 7 | ## Features 8 | 9 | 1. Use your own artisanal hand-crafted ESLint rules. 10 | * Extend Burp's JavaScript analysis engine. 11 | 2. Pain-free setup. 12 | * Get up and running with three commands. 13 | 3. Results Are stored in two different places. 14 | * SQLite is forever. 15 | 4. It doesn't interrupt your work flow. 16 | * Let the extension lint while you do your magic. 17 | 5. It's hella configurable. 18 | * Running Burp on a slow machine? Reduce the number of threads. 19 | * Don't want to lint now? Click that shiny `Process` button to pause it. 20 | * Want to close Burp? No problem. Unfinished tasks will be read from the 21 | database and executed when the extension is loaded again. 22 | * Want to only process requests from certain hosts? Add it to the scope and 23 | set the associated key in the config file to `true`. 24 | * Don't like large JavaScript files? Set the max size in the config. 25 | * Want to process requests from another extension? See [Process Requests Made by Other Extensions](#process-requests-made-by-other-extensions). 26 | 6. Filter results by host. 27 | * Start typing in the text field in the extension tab. 28 | 29 | ![ESLinter in action](.github/google.gif) 30 | 31 | ## Quickstart 32 | 33 | 1. Install `git`, `npm` and `JDK 11`. 34 | 1. [AdoptOpenJDK 11][adoptopenjdk11] is recommended. Make sure `JAVA_HOME` 35 | is set. 36 | 2. Clone the repository. 37 | 3. `gradlew -q clean`. Not needed for a fresh installation. 38 | 4. `gradlew -q install` 39 | 1. Clones the `eslint-security` git submodule. 40 | 2. Runs `npm install` in `eslint-security`. 41 | 5. `gradlew -q config -Ptarget=/relative/or/absolute/path/to/your/desired/project/location` 42 | 1. E.g., `gradlew -q config -Ptarget=testproject` creates a directory named 43 | `testproject` inside the `eslinter` directory. 44 | 2. Creates `config.json` in the `release` directory with a sane configuration. 45 | 6. Add the extension jar at `release/eslint-all.jar` to Burp. 46 | 1. The first time a new config is loaded, you might get an error not being 47 | able to connect to the database, this is OK. 48 | 7. Navigate to the `ESLinter` tab and click on the `Process` button. 49 | 8. Browse the target website normally with Burp as proxy. 50 | 9. Observe the extracted JavaScript being linted. 51 | 10. Look in the project directory to view all extracted and linted files. 52 | 11. Double-click on any result to open a dialog box. Choose a path to save both 53 | the beautified JavaScript and lint results. 54 | 55 | * For build troubleshooting please see [Building the 56 | Extension](#building-the-extension) below. 57 | 58 | **Double click in action** 59 | 60 | ![Doubleclick](.github/doubleclick.gif) 61 | 62 | [adoptopenjdk11]: https://adoptopenjdk.net/?variant=openjdk11&jvmVariant=hotspot 63 | 64 | ## Table of Content 65 | 66 | - [Features](#features) 67 | - [Quickstart](#quickstart) 68 | - [Extension Configuration](#extension-configuration) 69 | - [Change the ESLint Rules](#change-the-eslint-rules) 70 | - [Change the ESLint Rule File](#change-the-eslint-rule-file) 71 | - [Change the Number of Linting Threads](#change-the-number-of-linting-threads) 72 | - [Process Requests Made by Other Extensions](#process-requests-made-by-other-extensions) 73 | - [Process Requests Made by Other Burp Tools](#process-requests-made-by-other-burp-tools) 74 | - [Customize ESLint Rules](#customize-eslint-rules) 75 | - [Triage The Results](#triage-the-results) 76 | - [Technical Details](#technical-details) 77 | - [Common Bugs](#common-bugs) 78 | - [Supported Platforms](#supported-platforms) 79 | - [The Connection to the Database Is Not Closed](#the-connection-to-the-database-is-not-closed) 80 | - [My Selected Row is Gone](#my-selected-row-is-gone) 81 | - [FAQ](#faq) 82 | - [Why Doesn't the Extension Create Burp Issues?](#why-doesnt-the-extension-create-burp-issues) 83 | - [SHA-1 Is Broken](#sha-1-is-broken) 84 | - [Development](#development) 85 | - [Building the Extension](#building-the-extension) 86 | - [Development](#development-1) 87 | - [Diagnostics](#diagnostics) 88 | - [Debugging](#debugging) 89 | - [Credits](#credits) 90 | - [Lewis Ardern](#lewis-ardern) 91 | - [Jacob Wilkin](#jacob-wilkin) 92 | - [Tom Limoncelli](#tom-limoncelli) 93 | - [Similar Unreleased Extension by David Rook](#similar-unreleased-extension-by-david-rook) 94 | - [Source Code Credit](#source-code-credit) 95 | - [Future Work and Feedback](#future-work-and-feedback) 96 | - [License](#license) 97 | 98 | ## Extension Configuration 99 | It's recommended to use the `config` Gradle task. You can also create your own 100 | extension configs. Open the config file in any text editor and change the 101 | values. For in-depth configuration, please see 102 | [docs/configuration.md](docs/configuration.md). 103 | 104 | ### Change the ESLint Rules 105 | 106 | **Option 1:** If you used the config Gradle task. 107 | 108 | 1. Edit the `eslint-security/eslintrc-parsia.js` file and add/remove rules. 109 | 1. Make a copy first if you want to use it as a guideline. 110 | 2. Reload the extension. 111 | 112 | **Option 2:** If you want to keep your ESLint rules in a different path. 113 | 114 | 1. Create your own rules and store them at any path. 115 | 2. Edit the `release/config.json` file. 116 | 3. Change the `eslint-config-path` to the ESLint rule path from step 1. 117 | 4. Reload the extension. 118 | 119 | ### Change the ESLint Rule File 120 | Edit the `eslint-config-path` key in the `release/config.json` file and point it 121 | to your custom ESLint rule file. 122 | 123 | ### Change the Number of Linting Threads 124 | The number of linting threads can be configured. For slower machines, it might 125 | need to be reduced. 126 | 127 | 1. Edit the extension config file. 128 | 2. Change the value of `number-of-linting-threads`. 129 | 130 | ### Process Requests Made by Other Extensions 131 | 132 | 1. Add `extender` to the `process-tool-list` in the config file. 133 | 2. Move ESLinter to the bottom of your extension list in the Extender tab. 134 | 3. Reload the extension. 135 | 4. ESLinter should be able to see requests created by other extensions. 136 | 137 | ### Process Requests Made by Other Burp Tools 138 | 139 | 1. Add the tool name to the `process-tool-list` in the config file. E.g., 140 | `Scanner`. 141 | 2. Move ESLinter to the bottom of your extension list in the Extender tab. 142 | 3. Reload the extension. 143 | 4. ESLinter should be able to see requests created by other Burp tools. 144 | 145 | ### Customize ESLint Rules 146 | Start by modifying one of the ESLint rule files in the 147 | [eslint-security][eslint-security] repository. 148 | 149 | To disable a rule either comment it out or change the numeric value of its key 150 | to `0`. 151 | 152 | If you are adding a rule that needs a new plugin you have to add it manually 153 | (usually via npm) to the location of your `eslint` and `js-beautify` commands. 154 | 155 | If you want to contribute your custom ESLint rules please feel free to create 156 | pull requests in [eslint-security][eslint-security]. 157 | 158 | [eslint-security]: https://github.com/parsiya/eslint-security 159 | 160 | For more information on configuring ESLint and writing custom rules please see: 161 | 162 | * https://eslint.org/docs/user-guide/configuring 163 | * https://eslint.org/docs/developer-guide/working-with-rules 164 | 165 | ## Triage The Results 166 | 167 | 1. Open the project directory in your editor (set in the config command). 168 | 2. Open any file in the `linted` sub-directory. These files contain the results. 169 | 3. Alternatively, double-click any row in the extension's tab to select a 170 | directory to save both the original JavaScript and lint results for an 171 | individual request. 172 | 4. The extension uses the ESLint [codeframe][eslint-codeframe] output format. 173 | This format includes a few lines of code before and after what was flagged by 174 | ESLint. You can use these results to understand the context. This is usually 175 | not enough. 176 | 5. To view the corresponding JavaScript file, open the file with the same name 177 | (minus `-linted`) in the `beautified` sub-directory. 178 | 6. The json object at the top of every file contains the URL and the referer of 179 | the request that contained the JavaScript. Use this information to figure out 180 | where this JavaScript was located. 181 | 182 | [eslint-codeframe]: https://eslint.org/docs/user-guide/formatters/#codeframe 183 | 184 | ## Technical Details 185 | The innerworkings of the extension are discussed in 186 | [docs/technical-details.md](docs/technical-details.md). 187 | 188 | ## Common Bugs 189 | Make a Github issue if you encounter a bug. Please use the Bug issue template 190 | and fill it as much as you can. Be sure to remove any identifying information 191 | from the config file. 192 | 193 | ### Supported Platforms 194 | ESLinter was developed and tested on Windows and Burp 2.1. It should work on 195 | most platforms. If it does not please make a Github issue. 196 | 197 | ### The Connection to the Database Is Not Closed 198 | You cannot delete the database if you unload the extension. 199 | 200 | Workaround: 201 | 202 | * Close Burp and delete the file. 203 | 204 | ### My Selected Row is Gone 205 | The table in the extension tab is updated every few seconds (controlled via the 206 | `update-table-delay` key in the config file). This means your selected row will 207 | be unselected when the table updates. This is not an issue. 208 | 209 | This might look odd when double-clicking a row. The FileChooser dialog pops up 210 | to select a path. When the table is updated, the selection is visually gone. 211 | This is not an issue. The data in the row is retrieved when you double-click 212 | and is not interrupted when the row is deselected after the table update. 213 | 214 | ## FAQ 215 | 216 | ### Why Doesn't the Extension Create Burp Issues? 217 | 218 | 1. This is not a Burp pro extension. Burp Issues are supported in the pro 219 | version. 220 | 2. Depending on the ESLint rules, this will create a lot of noise. 221 | 222 | ### SHA-1 Is Broken 223 | Yes, but the extension uses SHA-1 to create a hash of JavaScript text. This hash 224 | is an identifier to detect duplicates. Adversarial collisions are not important 225 | here. 226 | 227 | ## Development 228 | 229 | ### Building the Extension 230 | 231 | 1. Install [AdoptOpenJDK 11][adoptopenjdk11] 232 | 1. Run `gradlew bigjar`. 233 | 2. The jar file will be stored inside the `release` directory. 234 | 235 | ### Development 236 | 237 | 1. Fork the repository. 238 | 2. Create a new branch. 239 | 3. Modify the extension. 240 | 4. Run `gradlew bigjar` to build it. Then test it in Burp. 241 | 5. Create a pull request. Please mention what has been modified. 242 | 243 | ### Diagnostics 244 | Set `"diagnostics": true` in the config file to see debug messages. These 245 | messages are useful when you are testing a single file in Burp Repeater. For 246 | more information, please see the `The Diagnostics Flag` section in 247 | [docs/configuration.md](docs/configuration.md). 248 | 249 | ### Debugging 250 | See the following blog post to see how you can debug Java Burp extensions in 251 | [Visual Studio Code][vscode-website]. The instructions can be adapted to use in 252 | other IDEs/editors. 253 | 254 | * https://parsiya.net/blog/2019-12-02-developing-and-debugging-java-burp-extensions-with-visual-studio-code/ 255 | 256 | [vscode-website]: https://code.visualstudio.com/ 257 | 258 | ## Credits 259 | 260 | ### Lewis Ardern 261 | For being a [Solid 5/7 JavaScript guy][lewis-twitter]. 262 | 263 | See his presentation [Manual JavaScript Analysis is a Bug][lewis-slides]. 264 | 265 | [lewis-twitter]: https://twitter.com/lewisardern 266 | [lewis-slides]: https://www.slideshare.net/LewisArdern/manual-javascript-anaylsis-is-a-bug-176308491 267 | 268 | ### Jacob Wilkin 269 | The original idea for the ESLinting JavaScript received in Burp was from the 270 | following blog post by [Jacob Wilkin][jacob-wilkin-twitter]: 271 | 272 | * https://medium.com/greenwolf-security/linting-for-bugs-vulnerabilities-49bc75a61c6 273 | 274 | Summary: 275 | 276 | 1. Browse the target and perform manual testing as usual. 277 | 2. Extract JavaScript from Burp. 278 | 3. Clean them up a bit and remove minified standard libraries. 279 | 4. Run ESLint with some security rules on the remaining JavaScript. 280 | 5. Triage the results. 281 | 6. ??? 282 | 7. Profit. 283 | 284 | [jacob-wilkin-twitter]: https://twitter.com/jacob_wilkin 285 | 286 | ### Tom Limoncelli 287 | My main drive for automation comes from reading the amazing article named 288 | [Manual Work is a Bug][manual-work] by [Thomas Limoncelli][tom-twitter]. 289 | **READ IT**. 290 | 291 | The article defines four levels of automation: 292 | 293 | 1. Document the steps. 294 | * Jacob's post above. 295 | 2. Create automation equivalents. 296 | * I created a prototype that linted JavaScript files after I extracted them 297 | from Burp manually. 298 | 3. Create automation. 299 | * This extension. 300 | 4. Self-service and autonomous systems. 301 | * Almost there in future work. 302 | 303 | [manual-work]: https://queue.acm.org/detail.cfm?id=3197520 304 | [tom-twitter]: https://twitter.com/yesthattom 305 | 306 | ### Similar Unreleased Extension by David Rook 307 | Searching for ["eslint burp" on Twitter][eslint-burp-twitter] returns a series 308 | of tweets from 2015 by [David Rook][david-rook-twitter]. It appears that he was 309 | working on a Burp extension that used ESLint to create issues. The extension was 310 | never released. 311 | 312 | [eslint-burp-twitter]: https://twitter.com/search?q=eslint%20burp&src=typed_query 313 | [david-rook-twitter]: https://twitter.com/davidrook 314 | 315 | ### Source Code Credit 316 | This extension uses a few open source libraries. You can see them in the 317 | `dependencies` section of the [build.gradle](build.gradle) file. 318 | 319 | In addition, it uses code copied from Apache Commons libraries. I copied 320 | individual files instead of the complete Apache Commons-Lang library. 321 | 322 | * [src/utils/StringUtils.java](src/utils/StringUtils.java) uses code from the 323 | Apache commons-lang.StringUtils. 324 | * [src/utils/SystemUtils](src/utils/SystemUtils.java) is an almost exact copy of 325 | Apache commons-lang.SystemUtils. 326 | 327 | ## Future Work and Feedback 328 | Please see the Github issues. If you have an idea, please make a Github issue 329 | and use the `Feature request` template. 330 | 331 | ## License 332 | Opensourced under the "GNU General Public License v3.0" and later. Please see 333 | [LICENSE](LICENSE) for details. 334 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Apply the Java plugin 2 | apply plugin: 'java' 3 | 4 | // Use Maven (because Burp Extender is on Maven) 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | dependencies { 10 | // Add the Burp Extender interface 11 | // https://mvnrepository.com/artifact/net.portswigger.burp.extender/burp-extender-api 12 | compile group: 'net.portswigger.burp.extender', name: 'burp-extender-api', version: '2.1' 13 | 14 | // https://mvnrepository.com/artifact/com.google.code.gson/gson 15 | compile group: 'com.google.code.gson', name: 'gson', version: '2.8.6' 16 | 17 | // https://mvnrepository.com/artifact/commons-io/commons-io 18 | compile group: 'commons-io', name: 'commons-io', version: '2.6' 19 | 20 | // https://mvnrepository.com/artifact/org.apache.commons/commons-exec 21 | compile group: 'org.apache.commons', name: 'commons-exec', version: '1.3' 22 | 23 | // https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc 24 | // Adds almost 6 MBs to the final jar file :( 25 | compile group: 'org.xerial', name: 'sqlite-jdbc', version: '3.30.1' 26 | } 27 | 28 | sourceSets { 29 | main { 30 | java { 31 | // Set the source directory to "src" 32 | srcDir 'src' 33 | // Exclude 'resources' 34 | exclude 'resources/' 35 | } 36 | } 37 | main { 38 | resources { 39 | // Set the resource directory to "src/resources" 40 | srcDir 'src/resources' 41 | } 42 | } 43 | } 44 | 45 | // Put the final jar file in a different location 46 | libsDirName = '../release' 47 | 48 | // Create a task for bundling all dependencies into a jar file. 49 | task bigJar(type: Jar) { 50 | baseName = project.name + '-all' 51 | from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } 52 | with jar 53 | } 54 | 55 | // Extend clean to delete eslint-security 56 | clean.doFirst { 57 | println 'Deleting eslint-security' 58 | delete 'eslint-security' 59 | println 'Deleting release' 60 | delete 'release' 61 | } 62 | 63 | // Create the install task here. 64 | 65 | // OS Detection. Source: https://stackoverflow.com/a/54315477 66 | import org.apache.tools.ant.taskdefs.condition.Os 67 | 68 | // How to use example. 69 | // task executeCommand(type: Exec) { 70 | // commandLine osAdaptiveCommand('aws', 'ecr', 'get-login', '--no-include-email') 71 | // } 72 | 73 | private static Iterable osAdaptiveCommand(String... commands) { 74 | def newCommands = [] 75 | if (Os.isFamily(Os.FAMILY_WINDOWS)) { 76 | newCommands = ['cmd', '/c'] 77 | } 78 | 79 | newCommands.addAll(commands) 80 | return newCommands 81 | } 82 | // End of OS detection. 83 | 84 | 85 | 86 | // The install task will: 87 | // 1. Build the extension jar. 88 | // 2. Get the git submodule in `eslint-security`. 89 | // 3. Runs `npm-install` in `eslint-security`. 90 | task install() { 91 | 92 | // 2. Build the project via gradle. 93 | dependsOn 'bigJar' 94 | 95 | // If these are not in doLast, they will be executed in the configuration 96 | // phase. AKA every time any task is run. 97 | doLast { 98 | println '\ngit submodule update --init --recursive --remote' 99 | exec { 100 | // 2. Get the eslint-security git submodule. 101 | commandLine osAdaptiveCommand('git', 'submodule', 'update', '--init', '--recursive', '--remote') 102 | } 103 | 104 | println '\ncd eslint-security & npm install' 105 | exec { 106 | // 3. Navigate to eslint-security and run npm install. 107 | commandLine osAdaptiveCommand('cd', 'eslint-security', '&&', 'npm', 'install') 108 | } 109 | } 110 | } 111 | 112 | import java.nio.file.Files 113 | import java.nio.file.Paths 114 | import java.io.File 115 | 116 | task config() { 117 | doLast { 118 | // Check if target was provided. 119 | // 1. Read a command-line parameter. This will be the project path. 120 | if (!project.hasProperty('target')) { 121 | // Return an error if target is not provided. 122 | println 'Please provide the target path in this format' 123 | println 'Relative path: `gradlew config -Ptarget=/relative/path/to/target' 124 | println 'Absolute path: `gradlew config -Ptarget=c:/absolute/path/to/target' 125 | throw new GradleException('target parameter not provided') 126 | } 127 | 128 | 129 | String currentDir = System.properties['user.dir'] 130 | // 2. Check if install has been called by checking for the existence of 131 | // currentDir/eslint-security/node_modules. If it's not there, return 132 | // an error. 133 | if (Files.isDirectory(Paths.get(currentDir, 'eslint-security/node_modules'))) { 134 | println 'eslint-security/node_modules exists' 135 | } else { 136 | throw new GradleException('`eslint-security/node_modules` does not exist, have you run `gradlew install') 137 | } 138 | 139 | // 3. Check if target is an absolute path. 140 | String targetDir = project.target 141 | File tar = new File(targetDir) 142 | if (!tar.isAbsolute()) { 143 | // If target is not absolute, concat target with the current working 144 | // directory and normalize it. 145 | targetDir = Paths.get(currentDir, targetDir).normalize() 146 | } 147 | // 4.1 Path to store extracted JavaScript files: `target/beautified` 148 | String beautified = Paths.get(targetDir, "beautified").toString().replace("\\", "/") 149 | 150 | // 4.2 Path to store ESLint results: `target/linted` 151 | String linted = Paths.get(targetDir, "linted").toString().replace("\\", "/") 152 | 153 | // 4.3 Location of the target database: `target/eslinter.sqlite` 154 | String db = Paths.get(targetDir, "eslinter.sqlite").toString().replace("\\", "/") 155 | 156 | // 4.4 Path to the eslint command: `currentDir/eslint-security/node_modules/.bin/eslint` 157 | String eslint = Paths.get(currentDir, 'eslint-security/node_modules/.bin/eslint').toString().replace("\\", "/") 158 | 159 | // 4.5 Path to the js-beautify command: `currentDir/eslint-security/node_modules/.bin/js-beautify` 160 | String jsbeautify = Paths.get(currentDir, 'eslint-security/node_modules/.bin/js-beautify').toString().replace("\\", "/") 161 | 162 | // 4.6 Detect OS. If Windows, add ".cmd" to the end of commands. 163 | if (System.properties['os.name'].toLowerCase().contains('windows')) { 164 | eslint += ".cmd" 165 | jsbeautify += ".cmd" 166 | } 167 | 168 | // 4.7 Path to the ESLint configuration file: `currentDir/eslint-security/configs/eslintrc-light.js` 169 | String cfgFile = Paths.get(currentDir, 'eslint-security/configs/eslintrc-parsia.js').toString().replace("\\", "/") 170 | 171 | // 5.0 Create the json file. 172 | String cfgStr = 173 | """ 174 | { 175 | "beautified-javascript-path": "${beautified}", 176 | "lint-result-path": "${linted}", 177 | "database-path": "${db}", 178 | "eslint-config-path": "${cfgFile}", 179 | "eslint-command-path": "${eslint}", 180 | "jsbeautify-command-path": "${jsbeautify}", 181 | "only-process-in-scope": false, 182 | "highlight": true, 183 | "diagnostics": false, 184 | "process-tool-list": [ 185 | "Proxy", 186 | "Scanner", 187 | "Repeater" 188 | ], 189 | "number-of-linting-threads": 3, 190 | "lint-timeout": 60, 191 | "number-of-request-threads": 10, 192 | "threadpool-timeout": 10, 193 | "lint-task-delay": 10, 194 | "update-table-delay": 5, 195 | "maximum-js-size": 0, 196 | "js-mime-types": [ 197 | "application/javascript", 198 | "application/ecmascript", 199 | "application/x-ecmascript", 200 | "application/x-javascript", 201 | "text/javascript", 202 | "text/ecmascript", 203 | "text/javascript1.0", 204 | "text/javascript1.1", 205 | "text/javascript1.2", 206 | "text/javascript1.3", 207 | "text/javascript1.4", 208 | "text/javascript1.5", 209 | "text/jscript", 210 | "text/livescript", 211 | "text/x-ecmascript", 212 | "text/x-javascript", 213 | "script" 214 | ], 215 | "javascript-file-extensions": [ 216 | "js", 217 | "javascript" 218 | ], 219 | "contains-javascript": [ 220 | "text/html", 221 | "application/xhtml+xml" 222 | ], 223 | "removable-headers": [ 224 | "If-Modified-Since", 225 | "If-None-Match" 226 | ] 227 | } 228 | """ 229 | 230 | File configFile = new File(Paths.get(currentDir, 'release/config.json').toString()) 231 | configFile.write(cfgStr) 232 | 233 | println "Configuration finished." 234 | println "Results will be stored in ${targetDir.toString().toString().replace("\\", "/")}." 235 | println "Config file is stored at ${Paths.get(currentDir,'release/config.json').toString().replace("\\", "/")}." 236 | println "Add the extension jar file to Burp and start linting." 237 | 238 | } 239 | } -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "beautified-javascript-path": "CHANGEME", 3 | "lint-result-path": "CHANGEME", 4 | "eslint-command-path": "CHANGEME", 5 | "eslint-config-path": "CHANGEME", 6 | "jsbeautify-command-path": "CHANGEME", 7 | "database-path": "CHANGEME", 8 | "only-process-in-scope": false, 9 | "highlight": true, 10 | "diagnostics": false, 11 | "process-tool-list": [ 12 | "Proxy", 13 | "Scanner", 14 | "Repeater" 15 | ], 16 | "number-of-linting-threads": 3, 17 | "lint-timeout": 60, 18 | "number-of-request-threads": 10, 19 | "threadpool-timeout": 10, 20 | "lint-task-delay": 10, 21 | "update-table-delay": 5, 22 | "maximum-js-size": 0, 23 | "js-mime-types": [ 24 | "application/javascript", 25 | "application/ecmascript", 26 | "application/x-ecmascript", 27 | "application/x-javascript", 28 | "text/javascript", 29 | "text/ecmascript", 30 | "text/javascript1.0", 31 | "text/javascript1.1", 32 | "text/javascript1.2", 33 | "text/javascript1.3", 34 | "text/javascript1.4", 35 | "text/javascript1.5", 36 | "text/jscript", 37 | "text/livescript", 38 | "text/x-ecmascript", 39 | "text/x-javascript", 40 | "script" 41 | ], 42 | "javascript-file-extensions": [ 43 | "js", 44 | "javascript" 45 | ], 46 | "contains-javascript": [ 47 | "text/html", 48 | "application/xhtml+xml" 49 | ], 50 | "removable-headers": [ 51 | "If-Modified-Since", 52 | "If-None-Match" 53 | ] 54 | } -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | `ESLinter` uses a config file in json format. 3 | 4 | - [Loading and Storing Configurations](#loading-and-storing-configurations) 5 | - [The Default Configuration File](#the-default-configuration-file) 6 | - [Saving and Loading Configuration Files](#saving-and-loading-configuration-files) 7 | - [Manual Configuration Steps](#manual-configuration-steps) 8 | - [Configuration File Elements](#configuration-file-elements) 9 | - [Storage Paths](#storage-paths) 10 | - [Command Paths](#command-paths) 11 | - [Highlight Requests](#highlight-requests) 12 | - [Process Requests Created by Specific Burp Tools](#process-requests-created-by-specific-burp-tools) 13 | - [Only Process Requests in Scope](#only-process-requests-in-scope) 14 | - [Performance](#performance) 15 | - [Configuring JavaScript Detection](#configuring-javascript-detection) 16 | - [Pure JavaScript](#pure-javascript) 17 | - [Embedded JavaScript](#embedded-javascript) 18 | - [Removing Request Headers](#removing-request-headers) 19 | - [The Diagnostics Flag](#the-diagnostics-flag) 20 | 21 | ## Loading and Storing Configurations 22 | 23 | ### The Default Configuration File 24 | At startup, the extension looks for a file named `config.json` in the same 25 | path as the jar file. That file will override the current configuration. If 26 | that file is modified, you need to reload the extension for the changes to take 27 | effect. 28 | 29 | For testing different configurations, create such a file and store it beside 30 | the jar file. This will ensure that you are always using a configuration that 31 | you have set. 32 | 33 | ### Saving and Loading Configuration Files 34 | To create a prepopulated config file, use the `gradlew config` task. 35 | 36 | To load a config file, use the `Load Config` button. `Save Config` saves the 37 | current configuration to a file. 38 | 39 | The extension saves the configuration to Burp's extension settings. There is no 40 | need to load the configuration file every time the extension starts. After a 41 | config is loaded, it will be reused (absent the existence of `config.json` 42 | explained above). 43 | 44 | ## Manual Configuration Steps 45 | It's recommended to use the `config` Gradle task. But you can also create your 46 | own config files. 47 | 48 | 1. Create a sample config file. This could be an existing one or a new one 49 | created by the config. 50 | 2. Edit the config file in your favorite editor. 51 | 3. At a minimum, you need to provide paths to (see 52 | [docs/configuration.md](docs/configuration.md) for more information): 53 | * `beautified-javascript-path`: Path to store extracted JavaScript files. 54 | * `lint-result-path`: Path to store ESLint results. 55 | * `database-path`: Location of the target database (it will be created if it 56 | does not exist). 57 | * `eslint-config-path`: Path to the ESLint configuration file. 58 | * `eslint-command-path`: Path to the `eslint` command. 59 | * `jsbeautify-command-path`: Path to the `js-beautify` command. 60 | 4. Modify any other settings. See the 61 | [Configuration File Elements](#configuration-file-elements) section. 62 | 5. Put the config file in the `release` directory or where the jar 63 | file is located. 64 | 65 | Note that Windows accepts paths with forward slashes. So 66 | `c:/eslint-security/node_modules/.bin/eslint.cmd` is a valid path. If you are 67 | providing paths with backslashes be sure to escape them. E.g., 68 | `c:\\eslint-security\\nod_modules\\.bin\\eslint.cmd`. 69 | 70 | ## Configuration File Elements 71 | The configuration file provides several options to control the behavior of the 72 | extension. 73 | 74 | ### Storage Paths 75 | The extension stores every extracted JavaScript and every ESLint result on 76 | the file system, too. This can be used to quickly see every result without 77 | having to export it from the database. 78 | 79 | * `beautified-javascript-path`: Where all beautified JavaScripts are stored. 80 | Each file contains the extracted JavaScript for one request. It will be 81 | created (including any parent directories) if it does not exist. 82 | * `lint-result-path`: Where all ESLint results are stored. Each file contains 83 | the results for one file from above. These files have the same name as their 84 | JavaScript counterparts with `-linted` appended. For example, the results for 85 | `google.com-whatever.js` will be in `google.com-whatever-linted.js`. It will 86 | be created (including any parent directories) if it does not exist. 87 | * `database-path`: Path to the SQLite database file. If the file does not exist, 88 | it will be created. 89 | 90 | Inside each JavaScript file (and ESLint result file), there is a comment that 91 | identifies the URL and the referer. Using this information you can figure 92 | out where this JavaScript came from and how to apply the results. 93 | 94 | ### Command Paths 95 | The extension needs to know where it can run `eslint` and `js-beautify` 96 | commands. This information is in the following keys: 97 | 98 | * `eslint-command-path` 99 | * `jsbeautify-command-path` 100 | 101 | The git submodule [eslint-security][eslint-security] takes care of 102 | installing these commands and the ESLint plugins. The commands will be located 103 | in `eslint-security/node_modules/.bin/`. 104 | 105 | On Windows be sure to point these to `eslint.cmd` and `js-beautify.cmd` and not 106 | just `eslint` and `js-beautify`. 107 | 108 | [eslint-security]: https://github.com/parsiya/eslint-security 109 | 110 | ### Highlight Requests 111 | The extension can highlight requests in Burp's HTTP History. `"highlight" :true` 112 | enables this behavior. This can help you quickly figure out which requests have 113 | JavaScript. 114 | 115 | * `cyan`: Requests that point to a JavaScript resource. E.g., 116 | `https://example.net/whatever.js`. 117 | * `yellow`: Requests that contain JavaScript but are not JavaScript files. These 118 | are mostly `text/html` files. 119 | 120 | The default value is `false`. 121 | 122 | ### Process Requests Created by Specific Burp Tools 123 | It's possible to tell the extension to only process requests/responses sent from 124 | certain Burp tools. For example, if you do not want to process anything coming 125 | from Proxy and are only interested in output from your own extension (or another 126 | Burp tool like Repeater), you can set it. 127 | 128 | This is controlled by the `process-tool-list` key in the config file. It 129 | contains an array where each element is the **name of the tool**. This is the 130 | result from the [IBurpExtenderCallbacks.getToolName][getToolName-doc] function. 131 | The following table shows all available options. 132 | 133 | | ToolFlag | getToolName | 134 | |----------------|-------------| 135 | | TOOL_SUITE | Suite | 136 | | TOOL_TARGET | Target | 137 | | TOOL_PROXY | Proxy | 138 | | TOOL_SPIDER | Scanner | 139 | | TOOL_SCANNER | Scanner | 140 | | TOOL_INTRUDER | Intruder | 141 | | TOOL_REPEATER | Repeater | 142 | | TOOL_SEQUENCER | Sequencer | 143 | | TOOL_DECODER | null | 144 | | TOOL_COMPARER | null | 145 | | TOOL_EXTENDER | Extender | 146 | 147 | Default values are: 148 | 149 | ```json 150 | "Proxy", 151 | "Scanner", 152 | "Repeater" 153 | ``` 154 | 155 | [getToolName-doc]: https://portswigger.net/burp/extender/api/burp/IBurpExtenderCallbacks.html#getToolName(int) 156 | 157 | ### Only Process Requests in Scope 158 | By setting the `only-process-in-scope` key to `true`. The extension only 159 | processes requests set in the scope tab. This useful when you are only 160 | interested in JavaScript files in a specific scope. 161 | 162 | The extension uses the [IBurpExtenderCallbacks.isInScope][isinscope-doc] 163 | function to decide if a request is in scope. 164 | 165 | Note: This setting is not retroactive. Setting this to `false` or changing the 166 | scope does not go back and process all previous files that were received 167 | earlier. The extension only process requests when they are received and does 168 | look back in history. 169 | 170 | [isinscope-doc]: https://portswigger.net/burp/extender/api/burp/IBurpExtenderCallbacks.html#isInScope(java.net.URL) 171 | 172 | ### Performance 173 | linting and beautifying commands are computationally expensive. A single web 174 | page could load a few dozen JavaScript files or a large vendored file. You can 175 | configure the number of threads used by the extension to configure the load for 176 | your machine. 177 | 178 | `number-of-linting-threads` is the most important item in this section. You can 179 | most likely keep the default values. If you are running Burp on a slow machine 180 | or one without a lot of RAM, reduce this number. 181 | 182 | Note that most concurrent operations use threadpools. Meaning if you set a low 183 | number, nothing is lost and the work is queued. The results are also 184 | stored in the database, so if you unload the extension (or close Burp) in the 185 | middle of processing nothing is lost. Items can be processed when the 186 | extension is loaded again. 187 | 188 | * `number-of-linting-threads`: Number of concurrent thread beautifying and 189 | linting JavaScript. This is the most expensive operation. By default, the 190 | value of this key is `3`. Note that you can also stop processing using the 191 | `Process` toggle button in the extension interface. 192 | * `number-of-request-threads`: Every request and response is processed in a 193 | separate thread. This element controls the number of concurrent request and 194 | response processing threads. 195 | * `lint-timeout`: The maximum number of seconds for each beautifying and 196 | linting task. Increase this number if you are processing large JavaScript 197 | files. 198 | * `maximum-js-size`: Files over this number (in KiloBytes) are not processed. 199 | `0` disables this setting. This is useful if you are dealing a lot of 3rd 200 | party libraries or vendor files on a slower machine or you are not interested 201 | in large files. 202 | * `lint-task-delay`: Number of seconds to wait before reading new rows from the 203 | database and adding them to the linting threadpool. Increase this number if 204 | you are not dealing with a lot of JavaScript. An extension thread constantly 205 | reads from the database and adds the rows that are not processed to the 206 | linting threadpool. This is the number of the delay in seconds between reads. 207 | * `update-table-delay`: Number of seconds to wait before updating the table in 208 | the extension tab. Increase this number if you are not processing a lot of 209 | JavaScript files. 210 | * `threadpool-timeout`: Number of seconds to wait for the threadpool tasks to 211 | finish before shutdown. The threadpools are shutdown when a new config is 212 | loaded and when the extension is unloaded. Decrease this number if you are 213 | experimenting with new configurations or are testing the extension. 214 | 215 | ### Configuring JavaScript Detection 216 | You can configure what responses are looked at. The default values do a great 217 | job for most web applications. But if you have JavaScript in non-traditional 218 | files/extensions/content-types/MIME types you can add them here. 219 | 220 | From the extension's perspective, there are three kinds of request/response 221 | pairs: 222 | 223 | * "Pure" JavaScript: All of the content in the body of the response is 224 | JavaScript. E.g., js files. 225 | * Embedded JavaScript: The response contains some JavaScript. E.g., HTML files. 226 | * No JavaScript: The response does not have any JavaScript. 227 | 228 | #### Pure JavaScript 229 | All files with MIME types included in `js-mime-types` and URLs ending in 230 | extensions in `javascript-file-extensions` are considered pure JavaScript. The 231 | complete body of these responses will be stored in a file and linted. 232 | 233 | Note: This is different from files that have embedded JavaScript like HTML 234 | files. If your response body is not pure JavaScript, do not include them here. 235 | Including these files in these settings will only result in parsing errors. For 236 | these files see the [Embedded JavaScript](#embedded-javascript) section below. 237 | 238 | Burp has two MIME type detection methods for responses: 239 | 240 | * `getInferredMimeType()` 241 | * `getStatedMimeType()` 242 | 243 | These methods return `script` if Burp thinks the response is a JavaScript file. 244 | It's not always accurate but it's usually correct. If you are looking to process 245 | MIME types (returned by Burp) that are not in the default list (see the sample 246 | config file). Add them to the end of the list in your extension config file. 247 | 248 | The following URL lists all JavaScript MIME types (search for `text/javascript` 249 | in the page). It appears that `text/javascript` is the most common. 250 | 251 | * https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types 252 | 253 | ```json 254 | "js-mime-types": [ 255 | "application/javascript", 256 | "application/ecmascript", 257 | "application/x-ecmascript", 258 | "application/x-javascript", 259 | "text/javascript", 260 | "text/ecmascript", 261 | "text/javascript1.0", 262 | "text/javascript1.1", 263 | "text/javascript1.2", 264 | "text/javascript1.3", 265 | "text/javascript1.4", 266 | "text/javascript1.5", 267 | "text/jscript", 268 | "text/livescript", 269 | "text/x-ecmascript", 270 | "text/x-javascript", 271 | "script" 272 | ] 273 | ``` 274 | 275 | Any URL with extensions in `javascript-file-extensions` is considered pure 276 | JavaScript. If you have extensions that have JavaScript, add them here. 277 | 278 | ```json 279 | "javascript-file-extensions": [ 280 | "js", 281 | "javascript" 282 | ], 283 | ``` 284 | 285 | #### Embedded JavaScript 286 | The extension extracts the JavaScript in these responses. All text 287 | between `script` HTML tags is grabbed, beautified and ESLinted. 288 | 289 | The extension detects these responses through their `Content-Type` headers. Any 290 | content-type included in the `contains-javascript` item will be considered to 291 | have embedded JavaScript. 292 | 293 | ```json 294 | "contains-javascript": [ 295 | "text/html", 296 | "application/xhtml+xml" 297 | ] 298 | ``` 299 | 300 | Note: Adding pure JavaScript responses here will result in their JavaScript not 301 | detected. Because pure JavaScript files do not wrap their content in `script` 302 | HTML tags. 303 | 304 | ### Removing Request Headers 305 | The extension supports removing headers from requests. Any header included in 306 | `removable-headers` will be removed from requests processed by the 307 | extension. 308 | 309 | This is useful for removing cache-control headers. Applications and browsers 310 | usually try to re-use cached assets. If a cached asset is requested and these 311 | headers are not removed, the response will be a [304 Not Modified][304-docs] 312 | with no content. Such a response is useless to the extension. 313 | 314 | Note: The extension does a good job of detecting duplicate resources and reusing 315 | lint results. See the [technical-details.md](technical-details.md) file for 316 | details. 317 | 318 | [304-docs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304 319 | 320 | ### The Diagnostics Flag 321 | Setting `diagnostics` to `true` will print diagnostics messages to the 322 | extension's console and add headers to the responses. 323 | 324 | These headers are `Is-Script`, `Contains-Script` and `MIMETYPEs` and can be used 325 | to see how the JavaScript detection works on responses. 326 | 327 | It is intended for troubleshooting and testing. The best way to use it is to 328 | isolate a single request/response that is causing the error. Enable diagnostics 329 | and send the request in Repeater to see how it is processed. 330 | -------------------------------------------------------------------------------- /docs/technical-details.md: -------------------------------------------------------------------------------- 1 | # Technical Details 2 | This section talks about the technical details of the extension. 3 | 4 | - [Optimizations](#optimizations) 5 | - [JavaScript Detection](#javascript-detection) 6 | - [Duplicate Asset Detection](#duplicate-asset-detection) 7 | - [Using Threadpools](#using-threadpools) 8 | - [SQLite Database to Persists Data](#sqlite-database-to-persists-data) 9 | - [Request/Response Processing Logic](#requestresponse-processing-logic) 10 | 11 | ## Optimizations 12 | 13 | ### JavaScript Detection 14 | The extension uses some simple ways to detect JavaScript in responses. Most of 15 | it is guided by the items in the config file (see "Configuring JavaScript 16 | Detection" in [configuration.md](configuration.md)). 17 | 18 | * MIME type returned by Burp. 19 | * `Content-Type` response header. 20 | * URL extension. E.g., everything that ends in `.js`. 21 | 22 | ### Duplicate Asset Detection 23 | The extension generates the hash of all JavaScript in a response and uses it to 24 | detect duplicates. If a certain JavaScript file (or content) is processed before 25 | and exists in the database, it's not processed again its record is updated when 26 | it's entered into the database. This is done with the trigger 27 | `resources/db/update_hash-trigger.sql`. 28 | 29 | ### Using Threadpools 30 | Each request, response and compute task is added to a threadpool. But 31 | configuring the number of threads, we can optimize the extension for the machine 32 | and load. Data are queued and submitted to the threadpool and are not lost. 33 | 34 | ### SQLite Database to Persists Data 35 | Each request and response is stored in a SQLite database. Closing the extension 36 | before some are processed does not lose the data. When the extension is loaded 37 | again and the `Process` button is toggled, all rows will be read from the 38 | database and processed again. 39 | 40 | Every beautified JavaScript and its ESLint results are also stored on the file 41 | system. 42 | 43 | ## Request/Response Processing Logic 44 | 45 | 1. Check if we got a request. 46 | 2. If it's a request, remove the headers and return. 47 | 3. If it's a response, check for JavaScript. 48 | 4. Extract the JavaScript. 49 | 5. Check the database to see if the hash of the body is already in the table. 50 | 6. If the hash exists. 51 | 1. Copy `beautified_javascript`, `status`, `results`, `is_processed` and 52 | `number_of_results`. 53 | 2. If `is_processed == 0`, then the rest of the columns do not have valid 54 | data and will be populated when this hash is processed. 55 | 3. Store the beautified JS file and results in their correct places. 56 | 4. Go to 8. 57 | 7. If the hash does not exist. 58 | 1. Beautify the extracted JS. 59 | 2. Populate the rest of the columns. 60 | 1. `beautified_javascript`: Beautified extracted JS. 61 | 2. `status` = pending. 62 | 3. `results` = empty. Don't care. 63 | 4. `is_processed` = 0. 64 | 5. `number_of_results` = 0. Don't care. 65 | 3. Store the beautified JS file and results in their correct places. 66 | 4. Go to 8. 67 | 8. Add the request to the table. 68 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parsiya/eslinter/f2b58eaee43cd724678e04c602f3844a67a69558/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /release/eslinter-all.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parsiya/eslinter/f2b58eaee43cd724678e04c602f3844a67a69558/release/eslinter-all.jar -------------------------------------------------------------------------------- /src/burp/BurpExtender.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import java.awt.Component; 4 | import java.io.File; 5 | import java.io.FileNotFoundException; 6 | import java.io.IOException; 7 | import java.security.NoSuchAlgorithmException; 8 | import java.sql.SQLException; 9 | import java.util.concurrent.ExecutorService; 10 | import java.util.concurrent.Executors; 11 | import java.util.concurrent.TimeUnit; 12 | import org.apache.commons.io.FileUtils; 13 | import org.apache.commons.io.FilenameUtils; 14 | import database.Database; 15 | import gui.BurpTab; 16 | import lint.Metadata; 17 | import lint.LintingThread; 18 | import lint.ProcessRequestTask; 19 | import lint.ProcessResponseTask; 20 | import lint.UpdateTableThread; 21 | import utils.BurpLog; 22 | import utils.CustomException; 23 | import utils.PausableExecutor; 24 | import utils.ReqResp; 25 | import utils.StringUtils; 26 | 27 | public class BurpExtender implements IBurpExtender, ITab, IHttpListener, IExtensionStateListener { 28 | 29 | public static IBurpExtenderCallbacks callbacks; 30 | public static IExtensionHelpers helpers; 31 | public static Config extensionConfig; 32 | public static BurpLog log; 33 | public static BurpTab mainTab; 34 | public static Database db; 35 | public static volatile boolean keepThread; 36 | public static PausableExecutor lintPool; 37 | 38 | private static ExecutorService requestPool; 39 | private static ExecutorService responsePool; 40 | private static UpdateTableThread updateThread; 41 | private static LintingThread lintThread; 42 | 43 | 44 | /** 45 | * Implement IBurpExtender. 46 | */ 47 | @Override 48 | public void registerExtenderCallbacks(final IBurpExtenderCallbacks burpCallbacks) { 49 | 50 | callbacks = burpCallbacks; 51 | helpers = callbacks.getHelpers(); 52 | // Set the extension name. 53 | callbacks.setExtensionName(Config.extensionName); 54 | 55 | // Create the logger. 56 | log = new BurpLog(true); 57 | 58 | // Use the default config. This is needed in case there is no config 59 | // saved or there is no default config file. 60 | extensionConfig = getDefaultConfig(); 61 | 62 | // Check if the default config file exists. 63 | String defaultConfigFile = Config.getDefaultConfigFullPath(); 64 | 65 | // This means that if the default config file contains a bad config, we 66 | // will not silenty fallback to the saved extension settings. 67 | if (CheckPaths.fileExists(defaultConfigFile)) { 68 | // Search for the default config file and load it if it exists. 69 | loadDefaultConfigFile(Config.defaultConfigName); 70 | } else { 71 | // Load the saved config file from the extension settings (if any). 72 | loadSavedConfig(); 73 | } 74 | 75 | // Check and create paths. We might get errors if we are loading a new 76 | // config but users should be able diagnose that. 77 | try { 78 | CheckPaths.checkAndCreatePaths(extensionConfig); 79 | } catch (Exception e) { 80 | // Issue41 - catch everything here. 81 | log.error("%s", StringUtils.getStackTrace(e)); 82 | log.alert("Bad config file, check the extension's error tab"); 83 | } 84 | log.debug("Finished path check."); 85 | 86 | // Set the debug flag from the loaded config. 87 | log.setDebugMode(extensionConfig.diagnostics); 88 | 89 | // Configure the process request and response threadpools. 90 | requestPool = Executors.newFixedThreadPool(extensionConfig.numRequestThreads); 91 | responsePool = Executors.newFixedThreadPool(extensionConfig.numRequestThreads); 92 | 93 | // Configure the beautify executor service. 94 | // pool = Executors.newFixedThreadPool(extensionConfig.numberOfThreads); 95 | lintPool = new PausableExecutor(extensionConfig); 96 | log.debug("Using %d threads.", extensionConfig.numLintThreads); 97 | // Default is paused. 98 | lintPool.pause(); 99 | // Create the ProcessLintQueue object and assigned the threadpool. 100 | lintThread = new LintingThread(extensionConfig, lintPool); 101 | lintThread.start(); 102 | 103 | // Connect to the database (or create it if it doesn't exist). If the 104 | // database connection is not established before the threads start, we 105 | // might have issues. 106 | try { 107 | databaseConnect(extensionConfig.dbPath); 108 | } catch (SQLException | IOException e) { 109 | log.error("%s", StringUtils.getStackTrace(e)); 110 | log.alert( 111 | "Could not connect to the database: %s", 112 | e.getMessage() 113 | ); 114 | } 115 | 116 | // Create the table update thread. 117 | updateThread = new UpdateTableThread(extensionConfig.updateTableDelay); 118 | updateThread.start(); 119 | 120 | log.debug("Started both threads."); 121 | 122 | // Create the main tab. 123 | mainTab = new BurpTab(); 124 | log.debug("Created the main tab."); 125 | callbacks.customizeUiComponent(mainTab.panel); 126 | 127 | // Add the tab to Burp. 128 | callbacks.addSuiteTab(BurpExtender.this); 129 | // Register the listener. 130 | callbacks.registerHttpListener(BurpExtender.this); 131 | // Register the extension state listener to handle extension unload. 132 | callbacks.registerExtensionStateListener(BurpExtender.this); 133 | 134 | log.debug("Loaded the extension. End of registerExtenderCallbacks."); 135 | } 136 | 137 | @Override 138 | public String getTabCaption() { 139 | return Config.tabName; 140 | } 141 | 142 | @Override 143 | public Component getUiComponent() { 144 | // Return the tab here. 145 | return mainTab.panel; 146 | } 147 | 148 | @Override 149 | public void processHttpMessage(final int toolFlag, final boolean isRequest, IHttpRequestResponse requestResponse) { 150 | 151 | if (requestResponse == null) return; 152 | 153 | // If it's a request, spawn a new thread and process it. We do not need 154 | // to worry about responses being processed before their requests 155 | // because the response will only arrive after the request is processed 156 | // and sent out. D'oh. 157 | if (isRequest) { 158 | // Use process requests so we can shut it down when unload the 159 | // extension. 160 | ProcessRequestTask processRequest = 161 | new ProcessRequestTask( 162 | toolFlag, requestResponse, extensionConfig 163 | ); 164 | 165 | requestPool.execute(processRequest); 166 | return; 167 | } 168 | 169 | // Here we have responses. 170 | log.debug("----------"); 171 | log.debug("Got a response."); 172 | // Create the metadata, it might not be needed if there's nothing in the 173 | // response but this is a small overhead for more readable code. 174 | Metadata metadata = new Metadata(); 175 | try { 176 | metadata = ReqResp.getMetadata(requestResponse); 177 | } catch (final NoSuchAlgorithmException e) { 178 | // This should not happen because we are passing "SHA-1" to the 179 | // digest manually. If we do not have the algorithm in Burp then 180 | // we have bigger problems. 181 | final String errMsg = StringUtils.getStackTrace(e); 182 | log.alert(errMsg); 183 | log.error( 184 | "Error creating metadata, algo name is probably wrong: %s.", 185 | errMsg 186 | ); 187 | log.debug("Returning from processHttpMessage because of %s", errMsg); 188 | return; 189 | } 190 | log.debug("Response metadata:\n%s", metadata.toString()); 191 | 192 | // Here we have responses. 193 | final IResponseInfo respInfo = helpers.analyzeResponse(requestResponse.getResponse()); 194 | 195 | String scriptHeader = "false"; 196 | String containsScriptHeader = "false"; 197 | String javascript = ""; 198 | 199 | if (Detective.isScript(requestResponse)) { 200 | log.debug("Detected a script response."); 201 | 202 | if (extensionConfig.highlight) { 203 | scriptHeader = "true"; 204 | requestResponse.setHighlight("cyan"); 205 | } 206 | 207 | // Get the response body. 208 | final byte[] bodyBytes = ReqResp.getResponseBody(requestResponse); 209 | if (bodyBytes.length == 0) { 210 | log.debug("Empty response, returning from processHttpMessage."); 211 | return; 212 | } 213 | javascript = StringUtils.bytesToString(bodyBytes); 214 | } else if (Detective.containsScript(requestResponse)) { 215 | // Not a JavaScript file, but it might contain JavaScript. 216 | log.debug("Detected a contains-script response."); 217 | 218 | if (extensionConfig.highlight) { 219 | containsScriptHeader = "true"; 220 | requestResponse.setHighlight("yellow"); 221 | } 222 | 223 | // Extract any JavaScript from the response. 224 | javascript = Extractor.getJS(requestResponse.getResponse()); 225 | } 226 | 227 | if (StringUtils.isEmpty(javascript)) { 228 | log.debug("Cound not find any in-line JavaScript, returning."); 229 | return; 230 | } 231 | 232 | // Don't uncomment this unless you are debugging in Repeater. It will 233 | // fill the debug log with noise. 234 | // log.debug("Extracted JavaScript:\n%s", javascript); 235 | // log.debug("End of extracted JavaScript ----------"); 236 | 237 | // Set the debug headers. Having this after checking if extracted 238 | // JavaScript is empty prevents highlighting requests that do not have 239 | // any extracted JavaScript. 240 | if (extensionConfig.diagnostics) { 241 | requestResponse = ReqResp.addHeader(isRequest, requestResponse, "Is-Script", scriptHeader); 242 | requestResponse = ReqResp.addHeader(isRequest, requestResponse, "Contains-Script", containsScriptHeader); 243 | requestResponse = ReqResp.addHeader(isRequest, requestResponse, "MIMETYPEs", 244 | String.format("%s -- %s", respInfo.getInferredMimeType(), respInfo.getStatedMimeType())); 245 | } 246 | 247 | // Check for jsMaxSize. 248 | if (javascript.length() >= (extensionConfig.jsMaxSize * 1024) && extensionConfig.jsMaxSize != 0) { 249 | log.debug("Length of JavaScript: %d > %d threshold, returning.", 250 | javascript.length(), extensionConfig.jsMaxSize * 1024); 251 | return; 252 | } 253 | 254 | try { 255 | // Spawn a new processResponse task that adds the captured 256 | // JavaScript to the db. 257 | final Runnable processResponse = new ProcessResponseTask( 258 | javascript, metadata 259 | ); 260 | 261 | responsePool.execute(processResponse); 262 | 263 | } catch (final Exception e) { 264 | log.debug("%s", StringUtils.getStackTrace(e)); 265 | } 266 | } 267 | 268 | // Returns the default config. Default config is the default values for the 269 | // Config object as set in Config.java. 270 | private static Config getDefaultConfig() { 271 | return new Config(); 272 | } 273 | 274 | // Get saved config. 275 | private static void loadSavedConfig() { 276 | // See if the extension config was saved in extension settings. If 277 | // default config was loaded from the file above, it will be saved. 278 | final String savedConfig = callbacks.loadExtensionSetting("config"); 279 | String decodedConfig = ""; 280 | 281 | if (StringUtils.isEmpty(savedConfig)) { 282 | // No saved config. Use the default version and prompt the user. 283 | log.alert("No saved config found, please choose one after the extension has loaded."); 284 | } else { 285 | // Base64 decode the config string. 286 | decodedConfig = StringUtils.base64Decode(savedConfig); 287 | extensionConfig = Config.configBuilder(decodedConfig); 288 | StringUtils.print("Config loaded from extension settings."); 289 | log.debug("Decoded config (if any):\n%s", decodedConfig); 290 | // log.debug("savedConfig: %s", savedConfig); 291 | } 292 | } 293 | 294 | private static void loadDefaultConfigFile(String cfgFileName) { 295 | // Check if there is a file named extensionConfig.defaultConfigName in 296 | // the current directory, if so, load it and overwrite the extension. 297 | try { 298 | String defaultConfigFullPath = Config.getDefaultConfigFullPath(); 299 | File f = new File(defaultConfigFullPath); 300 | 301 | String cfgFile = FileUtils.readFileToString(f, StringUtils.UTF8); 302 | extensionConfig = Config.loadConfig(cfgFile); 303 | log.debug("Config loaded from default config file %s", defaultConfigFullPath); 304 | } catch (FileNotFoundException e) { 305 | log.debug( 306 | "Default config file '%s' was not found.", 307 | Config.defaultConfigName 308 | ); 309 | } catch (Exception e) { 310 | // If anything goes wrong here, then something else was wrong other 311 | // than the file not having the correct content. 312 | log.debug( 313 | "Error loading default config file %s: %s", 314 | Config.defaultConfigName, 315 | StringUtils.getStackTrace(e) 316 | ); 317 | log.debug("This is not a show stopper, the extension is will continue loading"); 318 | } 319 | } 320 | 321 | // Connects to the database (or creates it if it does not exist). 322 | public static void databaseConnect(String dbPath) throws SQLException, IOException { 323 | // Create the database. 324 | db = new Database(dbPath); 325 | log.debug("Created a connection to the database: %s", dbPath); 326 | } 327 | 328 | // Invoked when the extension is unloaded. 329 | @Override 330 | public void extensionUnloaded() { 331 | log.debug("Starting to unload the extension"); 332 | unloadExtension(); 333 | // Stop the threads. 334 | updateThread.stop(); 335 | lintThread.stop(); 336 | 337 | log.debug("Unloaded the extension."); 338 | } 339 | 340 | // Shutdowns the threadpool and waits for the active threads to finish. 341 | // Closes the DB connection. 342 | public static void unloadExtension() { 343 | // Shutdown the threadpool and wait for termination. 344 | // https://stackoverflow.com/a/1250655 345 | 346 | if (requestPool != null) { 347 | // Shutdown requestPool. This should be quick. 348 | requestPool.shutdown(); 349 | try { 350 | requestPool.awaitTermination(extensionConfig.threadpoolTimeout, TimeUnit.SECONDS); 351 | log.debug("requestPool terminated."); 352 | } catch (Exception e) { 353 | log.error("Could not terminate requestPool: %s", StringUtils.getStackTrace(e)); 354 | } 355 | } 356 | 357 | if (responsePool != null) { 358 | // Shutdown responsePool. This should be quick. 359 | responsePool.shutdown(); 360 | try { 361 | responsePool.awaitTermination(extensionConfig.threadpoolTimeout, TimeUnit.SECONDS); 362 | log.debug("responsePool terminated."); 363 | } catch (Exception e) { 364 | log.error("Could not terminate responsePool: %s", StringUtils.getStackTrace(e)); 365 | } 366 | } 367 | 368 | if (lintPool != null) { 369 | // Losing the tasks in the pool but everything is stored in the db. 370 | lintPool.shutdownNow(); 371 | try { 372 | lintPool.awaitTermination(extensionConfig.threadpoolTimeout, TimeUnit.SECONDS); 373 | log.debug("All threads are terminated."); 374 | } catch (InterruptedException e) { 375 | log.error("Could not terminate all threads: %s", StringUtils.getStackTrace(e)); 376 | } 377 | } 378 | 379 | try { 380 | if (db != null) { 381 | db.close(); 382 | log.debug("Closed the database connection"); 383 | } 384 | } catch (SQLException e) { 385 | log.error("Error closing the database connection: %s", StringUtils.getStackTrace(e)); 386 | } 387 | } 388 | } -------------------------------------------------------------------------------- /src/burp/CheckPaths.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import java.io.File; 4 | import java.nio.file.Path; 5 | import java.nio.file.Paths; 6 | import utils.CustomException; 7 | import utils.Exec; 8 | import utils.StringUtils; 9 | import java.nio.file.Files; 10 | 11 | /** 12 | * CheckPaths has static methods to check paths in the config file. 13 | */ 14 | public class CheckPaths { 15 | 16 | // Returns true if all paths checkout, otherwise throws an exception with all 17 | // the errors. 18 | public static boolean checkAndCreatePaths(Config extensionConfig) throws CustomException { 19 | 20 | String err = ""; 21 | 22 | // Create storagePath and check access. 23 | if (createDirectory(extensionConfig.storagePath)) { 24 | // storagePath is created or exists. 25 | } else { 26 | // storagePath was not created. 27 | // Check if we can write to it. 28 | if (!canWrite(extensionConfig.storagePath)) { 29 | err += String.format( 30 | "Could not create or write to storagePath at %s.\n", 31 | extensionConfig.storagePath 32 | ); 33 | } 34 | } 35 | 36 | // Create eslintOutputPath and check access. 37 | if (createDirectory(extensionConfig.lintOutputPath)) { 38 | // eslintOutputPath is created or exists. 39 | } else { 40 | // eslintOutputPath was not created. 41 | // Check if we can write to it. 42 | if (!canWrite(extensionConfig.lintOutputPath)) { 43 | err += String.format( 44 | "Could not create or write to eslintOutputPath at %s.\n", 45 | extensionConfig.lintOutputPath 46 | ); 47 | } 48 | } 49 | 50 | // Run eslintCommandPath to check if eslint exists. 51 | String runESLint = commandExists(extensionConfig.eslintCommandPath); 52 | if (StringUtils.isNotEmpty(runESLint)) { 53 | err += String.format( 54 | "Could not run ESLint at %s: %s.\n", 55 | extensionConfig.eslintCommandPath, 56 | runESLint 57 | ); 58 | } 59 | 60 | // Run jsBeautifyCommandPath to check if eslint exists. 61 | String runJSBeautify = commandExists( 62 | extensionConfig.jsBeautifyCommandPath, 63 | 1 // js-beautify returns 1 if run without any input or parameters. 64 | ); 65 | 66 | if (StringUtils.isNotEmpty(runJSBeautify)) { 67 | err += String.format( 68 | "Could not run js-beautify at %s: %s.\n", 69 | extensionConfig.jsBeautifyCommandPath, 70 | runJSBeautify 71 | ); 72 | } 73 | 74 | // Check if eslintConfigPath exists. 75 | if (!fileExists(extensionConfig.eslintConfigPath)) { 76 | err += String.format( 77 | "Could not find the ESLint rule file at %s.\n", 78 | extensionConfig.eslintConfigPath 79 | ); 80 | } 81 | 82 | // Check if the directory with the database is writable. Connect will 83 | // take care of creating the file. 84 | String dbDirectory = StringUtils.getParentDirectory(extensionConfig.dbPath); 85 | if (createDirectory(dbDirectory)) { 86 | // dbDirectory is created or exists. 87 | } else { 88 | // dbDirectory was not created, check if we can write to it. 89 | if (!canWrite(dbDirectory)) { 90 | err += String.format( 91 | "Could not write to the database directory at %s.\n", 92 | dbDirectory 93 | ); 94 | } 95 | } 96 | 97 | if (StringUtils.isNotEmpty(err)) { 98 | throw new CustomException(err); 99 | } else { 100 | return true; 101 | } 102 | } 103 | 104 | // Return an empty string if command exists and executes successfully. 105 | // Otherwise, it returns the exception. 106 | private static String commandExists(String path, int ...exitValues) { 107 | Exec cmd = new Exec( 108 | path, 109 | new String[] {""}, 110 | StringUtils.getParentDirectory(path), 111 | exitValues 112 | ); 113 | 114 | try { 115 | cmd.exec(); 116 | } catch (Exception e) { 117 | return StringUtils.getStackTrace(e); 118 | } 119 | return ""; 120 | } 121 | 122 | 123 | // Returns true if the application can write to the directory or file. 124 | private static boolean canWrite(String dir) { 125 | Path p = Paths.get(dir); 126 | return Files.isWritable(p); 127 | } 128 | 129 | // Creates the directory in dir. Returns true if directory was created. What 130 | // does it return if it already existed? 131 | private static boolean createDirectory(String dir) { 132 | File f = new File(dir); 133 | return f.mkdirs(); 134 | } 135 | 136 | // Returns true if a file exists. 137 | public static boolean fileExists(String file) { 138 | return new File(file).exists(); 139 | } 140 | } -------------------------------------------------------------------------------- /src/burp/Config.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import static burp.BurpExtender.callbacks; 4 | import static burp.BurpExtender.log; 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.sql.SQLException; 8 | import com.google.gson.Gson; 9 | import com.google.gson.GsonBuilder; 10 | import com.google.gson.JsonSyntaxException; 11 | import com.google.gson.annotations.SerializedName; 12 | import org.apache.commons.io.FileUtils; 13 | import org.apache.commons.io.FilenameUtils; 14 | import utils.StringUtils; 15 | 16 | 17 | /** 18 | * Config 19 | */ 20 | public class Config { 21 | 22 | // Transient fields are not serialized or deserialized. 23 | // This appears in Extender. 24 | final public static transient String extensionName = "ESLinter"; 25 | // This is the extension's tab name. 26 | final public static transient String tabName = "ESLinter"; 27 | // Table's column names. 28 | final public static transient String[] lintTableColumnNames = 29 | new String[] {"Host", "URL", "Status", "Number of Findings"}; 30 | // Table's column classes. 31 | final public static transient Class[] lintTableColumnClasses = 32 | new Class[] {java.lang.String.class, java.lang.String.class, java.lang.String.class, 33 | java.lang.String.class // Although last column is int, we want it to be left-aligned. 34 | }; 35 | // Maximum number of characters from the URL. 36 | final public static transient int urlFileNameLimit = 50; 37 | // Default config file name. 38 | final public static transient String defaultConfigName = "config.json"; 39 | 40 | // End transient fields. 41 | 42 | // Storage path for extracted beautified JavaScript files. 43 | @SerializedName("beautified-javascript-path") 44 | public String storagePath = ""; 45 | 46 | // Where lint results are stored. 47 | @SerializedName("lint-result-path") 48 | public String lintOutputPath = ""; 49 | 50 | // Full path to the sqlite database file. It will be created if it does not 51 | // exist. 52 | @SerializedName("database-path") 53 | public String dbPath = ""; 54 | 55 | // Path to the ESLint configuration file. 56 | @SerializedName("eslint-config-path") 57 | public String eslintConfigPath = ""; 58 | 59 | // ESLint binary full path. [path]/node_modules/.bin/eslint 60 | @SerializedName("eslint-command-path") 61 | public String eslintCommandPath = ""; 62 | 63 | // Full path to the js-beautify binary/command. [path]/node_modules/.bin/eslint 64 | @SerializedName("jsbeautify-command-path") 65 | public String jsBeautifyCommandPath = ""; 66 | 67 | // If true, only in-scope requests will be processed. 68 | @SerializedName("only-process-in-scope") 69 | public boolean processInScope = false; 70 | 71 | // If true, requests containing JavaScript will be highlighted in history. 72 | @SerializedName("highlight") 73 | public boolean highlight = false; 74 | 75 | // If set to true, the extension will print extra information. This can be 76 | // used for troubleshooting. 77 | @SerializedName("diagnostics") 78 | public boolean diagnostics = true; 79 | 80 | // Only lint requests made by these tools. The names here must be the same 81 | // as the getToolName column (case-insensitive): 82 | // | ToolFlag | getToolName | 83 | // |----------------|-------------| 84 | // | TOOL_SUITE | Suite | 85 | // | TOOL_TARGET | Target | 86 | // | TOOL_PROXY | Proxy | 87 | // | TOOL_SPIDER | Scanner | 88 | // | TOOL_SCANNER | Scanner | 89 | // | TOOL_INTRUDER | Intruder | 90 | // | TOOL_REPEATER | Repeater | 91 | // | TOOL_SEQUENCER | Sequencer | 92 | // | TOOL_DECODER | null | 93 | // | TOOL_COMPARER | null | 94 | // | TOOL_EXTENDER | Extender | 95 | @SerializedName("process-tool-list") 96 | public String[] processToolList = new String[] { 97 | "Proxy", 98 | "Scanner", 99 | "Repeater" 100 | }; 101 | 102 | // Maximum number of linting threads. 103 | @SerializedName("number-of-linting-threads") 104 | public int numLintThreads = 3; 105 | 106 | // How many seconds to wait for a linting task to complete. Increase this if 107 | // you are beautifying and linting huge files. 108 | @SerializedName("lint-timeout") 109 | public int lintTimeout = 60; 110 | 111 | // Maximum number of request/response processing threads. 112 | // These tasks are light-weight. 113 | @SerializedName("number-of-request-threads") 114 | public int numRequestThreads = 10; 115 | 116 | // Threadpool shutdown timeout in seconds. How many seconds to wait before 117 | // shutting down threadpools when unloading the extension. 118 | @SerializedName("threadpool-timeout") 119 | public int threadpoolTimeout = 10; 120 | 121 | // The number of seconds the lint task sleeps between reading new lint tasks 122 | // from the database. 123 | @SerializedName("lint-task-delay") 124 | public int lintTaskDelay = 10; 125 | 126 | // Update table frequency in seconds. The number of seconds the updat table 127 | // task sleeps between updates. 128 | @SerializedName("update-table-delay") 129 | public int updateTableDelay = 5; 130 | 131 | // Maximum size of JavaScript to process in KBs. 0 == unlimited. 132 | @SerializedName("maximum-js-size") 133 | public int jsMaxSize = 0; 134 | 135 | /** 136 | * JavaScript MIME types. Search for "text/javascript" here 137 | * https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types Only 138 | * "text/javascript" is supposedly supported but who knows. Should be entered as lowercase here. 139 | * Burp returns "script" for JavaScript. 140 | */ 141 | @SerializedName("js-mime-types") 142 | public String[] jsTypes = new String[] { 143 | "application/javascript", 144 | "application/ecmascript", 145 | "application/x-ecmascript", 146 | "application/x-javascript", 147 | "text/javascript", 148 | "text/ecmascript", 149 | "text/javascript1.0", 150 | "text/javascript1.1", 151 | "text/javascript1.2", 152 | "text/javascript1.3", 153 | "text/javascript1.4", 154 | "text/javascript1.5", 155 | "text/jscript", 156 | "text/livescript", 157 | "text/x-ecmascript", 158 | "text/x-javascript", 159 | "script" // This is what Burp returns as the MIMEType if it detects js. 160 | }; 161 | 162 | // File extensions that might contain JavaScript. 163 | @SerializedName("javascript-file-extensions") 164 | public String[] fileExtensions = new String[] { 165 | "js", 166 | "javascript" 167 | }; 168 | 169 | // Content-Types that might contain scripts, the JavaScript inside these 170 | // will be extracted and used. 171 | // Should be entered as lowercase here. 172 | @SerializedName("contains-javascript") 173 | public String[] containsScriptTypes = new String[] { 174 | "text/html", 175 | "application/xhtml+xml" // XHTML, be sure to remove the CDATA tags. 176 | }; 177 | 178 | /** 179 | * Removable headers. These headers will be removed from the requests. The 180 | * change will not appear in Burp history but the outgoing request will not 181 | * have these headers. 182 | */ 183 | @SerializedName("removable-headers") 184 | public String[] headersToRemove = new String[] { 185 | "If-Modified-Since", 186 | "If-None-Match" 187 | }; 188 | 189 | // Convert the config to JSON. 190 | public String toString() { 191 | return new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(this); 192 | } 193 | 194 | // No-args constructor for Gson. 195 | public Config() { 196 | } 197 | 198 | // Creates a config object from the json string. 199 | public static Config configBuilder(String json) throws JsonSyntaxException { 200 | return new Gson().fromJson(json, Config.class); 201 | } 202 | 203 | // Writes the config files to file. 204 | public void writeToFile(File path) throws IOException { 205 | FileUtils.writeStringToFile(path, toString(), StringUtils.UTF8); 206 | } 207 | 208 | // 1. Converts the Config object to a json string. 209 | // 2. Encodes it in base64. 210 | // 3. Saves the config to extension settings. 211 | public void saveConfigToExtensionSettings() { 212 | // 1. Convert to string. 213 | String cfgStr = toString(); 214 | // 2. Base64 encode. 215 | String cfgBase64 = StringUtils.base64Encode(cfgStr); 216 | // 3. Save it to extension settings. 217 | callbacks.saveExtensionSetting("config", cfgBase64); 218 | } 219 | 220 | // Creates a new config from the json string and returns it. Also waits for 221 | // the threadpool to shutdown, closes the DB connection and establishes a 222 | // connection to the new DB set in the new config file. 223 | public static Config loadConfig(String json) throws IOException { 224 | // Unload the extension, because we are loading a new config. 225 | BurpExtender.unloadExtension(); 226 | // Read the json string and create a new config. 227 | Config cfg = configBuilder(json); 228 | try { 229 | // Connect to the new database file. 230 | BurpExtender.databaseConnect(cfg.dbPath); 231 | } catch (SQLException e) { 232 | log.debug( 233 | "Could not create the database file: %s", 234 | e.getMessage() 235 | ); 236 | } 237 | // Save the config file in extension settings. 238 | cfg.saveConfigToExtensionSettings(); 239 | return cfg; 240 | } 241 | 242 | // Returns the full path to the default config file. 243 | public static String getDefaultConfigFullPath(){ 244 | // Get the extension jar path. 245 | String jarPath = callbacks.getExtensionFilename(); 246 | // Get the parent directory of the jar path. 247 | String jarDirectory = StringUtils.getParentDirectory(jarPath); 248 | // Create the full path for the default config file. 249 | // jarDirectory/Config.defaultConfigName. 250 | return FilenameUtils.concat(jarDirectory, defaultConfigName); 251 | } 252 | } -------------------------------------------------------------------------------- /src/burp/Detective.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import static burp.BurpExtender.extensionConfig; 4 | import static utils.Constants.EMPTY_STRING; 5 | import java.net.URL; 6 | import java.util.ArrayList; 7 | import utils.ReqResp; 8 | 9 | 10 | /** 11 | * Detective contains the JavaScript detection functions. 12 | */ 13 | public class Detective { 14 | 15 | private static final String JAVASCRIPT_MIMETYPE = "text/javascript"; 16 | 17 | public static boolean isScript(IHttpRequestResponse requestResponse) { 18 | 19 | // 1. Check the requests' extension. 20 | if (isJSURL(requestResponse)) { 21 | return true; 22 | } 23 | 24 | // 2. Check the MIMEType 25 | String mType = getMIMEType(requestResponse); 26 | if ((isJSMimeType(mType)) && (mType != EMPTY_STRING)){ 27 | return true; 28 | } 29 | return false; 30 | } 31 | 32 | public static String getMIMEType(IHttpRequestResponse requestResponse) { 33 | // 0. Process the response. 34 | IResponseInfo respInfo = BurpExtender.helpers.analyzeResponse(requestResponse.getResponse()); 35 | 36 | // 1. Try to get the MIME type from the response using Burp. 37 | String mimeType = respInfo.getStatedMimeType(); 38 | if (mimeType != EMPTY_STRING) return mimeType; 39 | mimeType = respInfo.getInferredMimeType(); 40 | if (mimeType != EMPTY_STRING) return mimeType; 41 | 42 | // I do not think we can do better at Burp but that is not tested yet. 43 | // 2. Get the "Content-Type" header of the response. 44 | ArrayList contentTypes = ReqResp.getHeader("Content-Type", false, requestResponse); 45 | if (contentTypes == null) return EMPTY_STRING; 46 | for (String cType : contentTypes) { 47 | // if (cType == null) continue; 48 | // Check if cType is in Config.JSMIMETypes. 49 | if (isJSMimeType(cType)) { 50 | return JAVASCRIPT_MIMETYPE; 51 | } 52 | } 53 | 54 | // 3. guessContentTypeFromName does not detect *.js files. 55 | 56 | // TODO Anything else? 57 | return EMPTY_STRING; 58 | } 59 | 60 | public static boolean containsScript(IHttpRequestResponse requestResponse) { 61 | // Get the Content-Type and check it against ContainsScriptTypes. 62 | // If so, get everything between "(.*)". 63 | ArrayList responseContentType = 64 | ReqResp.getHeader("Content-Type", false, requestResponse); 65 | 66 | if (responseContentType == null) return false; 67 | for (String cType : responseContentType) { 68 | if (cType == null) continue; 69 | if(isContainsScriptType(cType)) { 70 | return true; 71 | } 72 | } 73 | return false; 74 | } 75 | 76 | private static boolean isContainsScriptType(String cType) { 77 | if (cType == null) { 78 | return false; 79 | } 80 | for (String ct : extensionConfig.containsScriptTypes) { 81 | if (cType.contains(ct)) return true; 82 | } 83 | return false; 84 | } 85 | 86 | private static boolean isJSMimeType(String mType) { 87 | // return Arrays.asList(Config.JSTypes).contains(mType.toLowerCase()); 88 | if (mType == null) { 89 | return false; 90 | } 91 | // Loop through all JSTypes and see if they occur in any of the headers. 92 | // This is better because the header usually contains the content-type 93 | // and stuff like charset. 94 | for (String jt : extensionConfig.jsTypes) { 95 | if (mType.contains(jt)) return true; 96 | } 97 | return false; 98 | } 99 | 100 | private static boolean isJSURL(IHttpRequestResponse requestResponse) { 101 | // Get the extension URL. 102 | String ext = ReqResp.getRequestExtension(requestResponse); 103 | // Return true if it's one of the extensions we are looking for. 104 | if (ext == null) return false; 105 | for (String extension : extensionConfig.fileExtensions) { 106 | if (ext.equalsIgnoreCase(extension)) { 107 | return true; 108 | } 109 | } 110 | return false; 111 | } 112 | 113 | public static URL getRequestURL(IHttpRequestResponse requestResponse) { 114 | IRequestInfo reqInfo = BurpExtender.helpers.analyzeRequest(requestResponse); 115 | return reqInfo.getUrl(); 116 | } 117 | 118 | } -------------------------------------------------------------------------------- /src/burp/Extractor.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import java.util.regex.Pattern; 4 | import java.util.regex.Matcher; 5 | 6 | import utils.StringUtils; 7 | 8 | 9 | /** 10 | * Extractor 11 | */ 12 | public class Extractor { 13 | 14 | private static String pattern = "]*>(.*?)<\\/script>"; 15 | private static int flags = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; 16 | 17 | public static String getJS(byte[] data) { 18 | Pattern pt = Pattern.compile(pattern, flags); 19 | Matcher mt = pt.matcher(StringUtils.bytesToString(data)); 20 | 21 | StringBuilder sb = new StringBuilder(); 22 | while (mt.find()) { 23 | if ( !StringUtils.isEmpty(mt.group(1)) ) { 24 | sb.append("\n"); 25 | sb.append(mt.group(1)); 26 | } 27 | } 28 | return sb.toString(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/database/Database.java: -------------------------------------------------------------------------------- 1 | package database; 2 | 3 | import static burp.BurpExtender.log; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.sql.Connection; 7 | import java.sql.DriverManager; 8 | import java.sql.PreparedStatement; 9 | import java.sql.ResultSet; 10 | import java.sql.SQLException; 11 | import java.sql.Statement; 12 | import java.util.ArrayList; 13 | import org.apache.commons.io.IOUtils; 14 | import burp.BurpExtender; 15 | import lint.Metadata; 16 | import linttable.LintResult; 17 | import utils.StringUtils; 18 | 19 | /** 20 | * Database 21 | */ 22 | public class Database { 23 | 24 | private Connection conn; 25 | // TODO Remove this if it's not used. 26 | private String path; 27 | 28 | // Creates the database and populates the connection. 29 | // If the path does not exist, it will be created. 30 | public Database(String path) throws SQLException, IOException { 31 | // To fix the "No suitable driver found for jdbc:sqlite:" error. 32 | try { 33 | Class.forName("org.sqlite.JDBC"); 34 | } catch (ClassNotFoundException e) { 35 | log.error(StringUtils.getStackTrace(e)); 36 | } 37 | // Need "//"" before the full path. 38 | // https://stackoverflow.com/a/32799328 39 | conn = DriverManager.getConnection("jdbc:sqlite://" + path); 40 | 41 | // Run the table create statements and add the triggers. 42 | createTable(); 43 | addTriggers(); 44 | 45 | this.path = path; 46 | } 47 | 48 | // Executes a statement on the datavase and returns the boolean result. 49 | // This can be used to create tables or add triggers. 50 | public boolean executeStatement(String statement) throws SQLException { 51 | 52 | Statement stmt = conn.createStatement(); 53 | return stmt.execute(statement); 54 | } 55 | 56 | // Executes the create table script. 57 | private boolean createTable() throws IOException, SQLException { 58 | return executeResourceFile("/db/create_table.sql"); 59 | } 60 | 61 | // Adds the triggers to the table. 62 | private void addTriggers() throws IOException, SQLException { 63 | executeResourceFile("/db/update_hash-trigger.sql"); 64 | executeResourceFile("/db/check_already_processed-trigger.sql"); 65 | } 66 | 67 | // Read the sql file from resources and execute it as a statement. 68 | private boolean executeResourceFile(String name) throws IOException, SQLException { 69 | 70 | // Get the contents of the file as a string. 71 | String content = getResourceFile(name); 72 | // Execute it as a statement. 73 | return executeStatement(content); 74 | } 75 | 76 | // Adds a new row to the eslint table and returns the number of updated 77 | // records which should usually be 1. 78 | public int addRow(LintResult lr) throws SQLException, IOException { 79 | 80 | String addRowQuery = getResourceFile("/db/add_row.sql"); 81 | 82 | /* 83 | INSERT INTO eslint 84 | (metadata, url, hash, beautified_javascript, status, results, is_processed, number_of_results) 85 | VALUES (?,?,?,?,?,?,?,?); 86 | */ 87 | 88 | PreparedStatement addRow = conn.prepareStatement(addRowQuery); 89 | addRow.setString(1, lr.metadata.toUglyString()); 90 | addRow.setString(2, lr.url); 91 | addRow.setString(3, lr.hash); 92 | addRow.setString(4, lr.beautifiedJavaScript); 93 | addRow.setString(5, lr.status); 94 | addRow.setString(6, lr.results); 95 | addRow.setInt(7, lr.isProcessed); 96 | addRow.setInt(8, lr.numResults); 97 | 98 | int res = addRow.executeUpdate(); 99 | addRow.closeOnCompletion(); 100 | return res; 101 | } 102 | 103 | // Returns all rows in the database where 104 | // (rowid > lastRowID AND is_processed !=1). 105 | public ArrayList getNewRows(long lastRowID) throws IOException, SQLException { 106 | 107 | String getNewRowsQuery = getResourceFile("/db/get_new_rows.sql"); 108 | PreparedStatement getNewRows = conn.prepareStatement(getNewRowsQuery); 109 | getNewRows.setLong(1, lastRowID); 110 | ResultSet rs = getNewRows.executeQuery(); 111 | 112 | /* 113 | SELECT * FROM eslint 114 | WHERE 115 | rowid > (?) AND is_processed != 1 116 | ORDER BY 117 | rowid 118 | */ 119 | 120 | ArrayList results = new ArrayList(); 121 | // Now we can process the results. 122 | while (rs.next()) { 123 | 124 | // ResultSetMetaData rsmd = rs.getMetaData(); 125 | // int columnCount = rsmd.getColumnCount(); 126 | 127 | // for (int i = 1; i <= columnCount; i++) { 128 | // String colName = rsmd.getColumnName(i); 129 | // String colLabel = rsmd.getColumnLabel(i); 130 | // // Object colVal = rs.getOb 131 | // log.debug("zz %d - %s - %s", i, colName, colLabel); 132 | // } 133 | 134 | // Fingers crossed this works. 135 | String metadataString = rs.getString("metadata"); 136 | Metadata metadata = Metadata.fromString(metadataString); 137 | 138 | LintResult lr = new LintResult( 139 | metadata, 140 | metadata.getHost(), 141 | rs.getString("url"), 142 | rs.getString("hash"), 143 | rs.getString("beautified_javascript"), 144 | rs.getString("status"), 145 | rs.getString("results"), 146 | rs.getInt("is_processed"), 147 | rs.getInt("number_of_results") 148 | ); 149 | results.add(new SelectResult(rs.getLong("rowid"), lr)); 150 | } 151 | 152 | // Close the statement. 153 | getNewRows.closeOnCompletion(); 154 | return results; 155 | } 156 | 157 | public int updateRow(LintResult lr) throws IOException, SQLException { 158 | 159 | String updateRowQuery = getResourceFile("/db/update_row.sql"); 160 | 161 | /* 162 | UPDATE eslint 163 | SET 164 | beautified_javascript = ?, 165 | status = ?, 166 | results = ?, 167 | is_processed = ?, 168 | number_of_results = ? 169 | WHERE 170 | metadata = ? 171 | */ 172 | 173 | PreparedStatement updateRow = conn.prepareStatement(updateRowQuery); 174 | updateRow.setString(1, lr.beautifiedJavaScript); 175 | updateRow.setString(2, lr.status); 176 | updateRow.setString(3, lr.results); 177 | updateRow.setInt(4, lr.isProcessed); 178 | updateRow.setInt(5, lr.numResults); 179 | 180 | // WHERE metadata = ? 181 | updateRow.setString(6, lr.metadata.toUglyString()); 182 | 183 | int res = updateRow.executeUpdate(); 184 | updateRow.closeOnCompletion(); 185 | return res; 186 | } 187 | 188 | public ArrayList getAllRows() throws IOException, SQLException { 189 | 190 | String getAllRowsQuery = getResourceFile("/db/get-all_rows.sql"); 191 | 192 | PreparedStatement getAllRows = conn.prepareStatement(getAllRowsQuery); 193 | ResultSet rs = getAllRows.executeQuery(); 194 | 195 | ArrayList results = new ArrayList(); 196 | // Now we can process the results. 197 | while (rs.next()) { 198 | 199 | String metadataString = rs.getString("metadata"); 200 | Metadata metadata = Metadata.fromString(metadataString); 201 | 202 | LintResult lr = new LintResult( 203 | metadata, 204 | metadata.getHost(), 205 | rs.getString("url"), 206 | rs.getString("hash"), 207 | rs.getString("beautified_javascript"), 208 | rs.getString("status"), 209 | rs.getString("results"), 210 | rs.getInt("is_processed"), 211 | rs.getInt("number_of_results") 212 | ); 213 | results.add(lr); 214 | } 215 | 216 | getAllRows.closeOnCompletion(); 217 | return results; 218 | } 219 | 220 | public void close() throws SQLException { 221 | conn.close(); 222 | } 223 | 224 | // Reads a resource file and returns the content as a string. This can be 225 | // used to read sql files from the resources directory. 226 | // Reads a resource and returns it as a string. 227 | // Remember to designate files with a /. 228 | // E.g., to get "resources/whatever.txt", call getResourceFile("/whatever.txt"). 229 | // E.g., "resources/path/whatever.txt" -> getResourceFile("/path/whatever.txt"). 230 | private static String getResourceFile(String name) throws IOException { 231 | 232 | InputStream in = BurpExtender.class.getResourceAsStream(name); 233 | String content = IOUtils.toString(in, StringUtils.UTF8); 234 | in.close(); 235 | return content; 236 | } 237 | } -------------------------------------------------------------------------------- /src/database/SelectResult.java: -------------------------------------------------------------------------------- 1 | package database; 2 | 3 | import linttable.LintResult; 4 | 5 | /** 6 | * SelectResult contains a LintResult with its associated rowid. 7 | */ 8 | public class SelectResult { 9 | 10 | public long rowid; 11 | public LintResult lr; 12 | 13 | public SelectResult(long id, LintResult lintResult) { 14 | 15 | rowid = id; 16 | lr = lintResult; 17 | } 18 | } -------------------------------------------------------------------------------- /src/gui/BurpTab.java: -------------------------------------------------------------------------------- 1 | package gui; 2 | 3 | import static burp.BurpExtender.extensionConfig; 4 | import static burp.BurpExtender.lintPool; 5 | import static burp.BurpExtender.log; 6 | import java.awt.Dimension; 7 | import java.awt.event.ActionEvent; 8 | import java.awt.event.ActionListener; 9 | import java.io.File; 10 | import java.io.IOException; 11 | import javax.swing.AbstractButton; 12 | import javax.swing.GroupLayout; 13 | import javax.swing.JButton; 14 | import javax.swing.JPanel; 15 | import javax.swing.JScrollPane; 16 | import javax.swing.JSeparator; 17 | import javax.swing.JSplitPane; 18 | import javax.swing.JTextField; 19 | import javax.swing.JToggleButton; 20 | import javax.swing.LayoutStyle; 21 | import javax.swing.SwingConstants; 22 | import javax.swing.event.DocumentEvent; 23 | import javax.swing.event.DocumentListener; 24 | import org.apache.commons.io.FileUtils; 25 | import burp.Config; 26 | import linttable.LintTable; 27 | import utils.FileChooser; 28 | import utils.StringUtils; 29 | 30 | /** 31 | * BurpTab 32 | */ 33 | public class BurpTab { 34 | 35 | public JSplitPane panel; 36 | public LintTable lintTable; 37 | 38 | public BurpTab() { 39 | initComponents(); 40 | } 41 | 42 | private void initComponents() { 43 | 44 | // Panel that is returned. 45 | panel = new JSplitPane(JSplitPane.VERTICAL_SPLIT); 46 | 47 | topPanel = new JPanel(); 48 | // configPanel.setBorder(BorderFactory.createBevelBorder(1)); 49 | loadConfigButton = new JButton("Load Config"); 50 | loadConfigButton.addActionListener(new ActionListener() { 51 | public void actionPerformed(ActionEvent evt) { 52 | loadConfigAction(); 53 | } 54 | }); 55 | 56 | saveConfigButton = new JButton("Save Config"); 57 | saveConfigButton.addActionListener(new ActionListener() { 58 | public void actionPerformed(ActionEvent evt) { 59 | saveConfigAction(); 60 | } 61 | }); 62 | 63 | processToggleButton = new JToggleButton("Process"); 64 | processToggleButton.addActionListener(new ActionListener() { 65 | public void actionPerformed(ActionEvent evt) { 66 | processToggleAction(evt); 67 | } 68 | }); 69 | 70 | searchTextField = new JTextField(); 71 | // Every time the textfield changes, update the table. 72 | // https://docs.oracle.com/javase/tutorial/uiswing/examples/components/TableFilterDemoProject/src/components/TableFilterDemo.java 73 | searchTextField.getDocument().addDocumentListener(new DocumentListener() { 74 | public void changedUpdate(DocumentEvent e) { 75 | searchAction(searchTextField.getText()); 76 | } 77 | 78 | public void insertUpdate(DocumentEvent e) { 79 | searchAction(searchTextField.getText()); 80 | } 81 | 82 | public void removeUpdate(DocumentEvent e) { 83 | searchAction(searchTextField.getText()); 84 | } 85 | }); 86 | 87 | // searchButton = new JButton("Search"); 88 | // searchButton.addActionListener(new java.awt.event.ActionListener() { 89 | // public void actionPerformed(java.awt.event.ActionEvent evt) { 90 | // // Get the text from searchTextField. 91 | // String query = searchTextField.getText().toLowerCase(); 92 | // log.debug("Searching for %s.", query); 93 | // searchAction(query); 94 | // } 95 | // }); 96 | 97 | 98 | resetButton = new JButton("Reset"); 99 | resetButton.addActionListener(new java.awt.event.ActionListener() { 100 | public void actionPerformed(java.awt.event.ActionEvent evt) { 101 | // Get the text from searchTextField. 102 | searchTextField.setText(""); 103 | } 104 | }); 105 | 106 | topSeparator = new JSeparator(SwingConstants.VERTICAL); 107 | topSeparator.setMaximumSize(new Dimension(2, 30)); 108 | 109 | GroupLayout topPanelLayout = new GroupLayout(topPanel); 110 | topPanel.setLayout(topPanelLayout); 111 | 112 | /** 113 | * Start GUI generated code. Do not modify. 114 | */ 115 | topPanelLayout.setHorizontalGroup(topPanelLayout 116 | .createParallelGroup(GroupLayout.Alignment.LEADING) 117 | .addGroup(topPanelLayout.createSequentialGroup().addContainerGap() 118 | .addGroup(topPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING) 119 | .addGroup(topPanelLayout.createSequentialGroup() 120 | .addComponent(processToggleButton, 121 | GroupLayout.PREFERRED_SIZE, 200, 122 | GroupLayout.PREFERRED_SIZE) 123 | .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) 124 | .addComponent(topSeparator) 125 | .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) 126 | .addComponent(loadConfigButton) 127 | .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) 128 | .addComponent(saveConfigButton)) 129 | .addGroup(topPanelLayout.createSequentialGroup() 130 | .addComponent(searchTextField, GroupLayout.PREFERRED_SIZE, 131 | 400, GroupLayout.PREFERRED_SIZE) 132 | .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) 133 | // .addComponent(searchButton) 134 | // .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) 135 | .addComponent(resetButton))) 136 | .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))); 137 | topPanelLayout.setVerticalGroup(topPanelLayout 138 | .createParallelGroup(GroupLayout.Alignment.LEADING) 139 | .addGroup(topPanelLayout.createSequentialGroup().addContainerGap().addGroup( 140 | topPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING).addGroup( 141 | topPanelLayout.createSequentialGroup().addGroup(topPanelLayout 142 | .createParallelGroup(GroupLayout.Alignment.LEADING) 143 | .addGroup(topPanelLayout 144 | .createParallelGroup(GroupLayout.Alignment.BASELINE) 145 | .addComponent(loadConfigButton) 146 | .addComponent(saveConfigButton)) 147 | .addComponent(topSeparator))) 148 | .addGroup(topPanelLayout.createSequentialGroup() 149 | .addComponent(processToggleButton) 150 | .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED))) 151 | .addGroup(topPanelLayout.createParallelGroup(GroupLayout.Alignment.BASELINE) 152 | .addComponent(searchTextField, GroupLayout.PREFERRED_SIZE, 153 | GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) 154 | // .addComponent(searchButton) 155 | .addComponent(resetButton)) 156 | .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))); 157 | 158 | // Link size of buttons. 159 | topPanelLayout.linkSize(SwingConstants.HORIZONTAL, loadConfigButton, saveConfigButton); 160 | // topPanelLayout.linkSize(SwingConstants.HORIZONTAL, searchButton, resetButton); 161 | 162 | /** 163 | * End GUI generated code. 164 | */ 165 | 166 | lintTable = new LintTable(); 167 | tableScrollPane = new JScrollPane(lintTable); 168 | 169 | panel.setLeftComponent(topPanel); 170 | panel.setRightComponent(tableScrollPane); 171 | } 172 | 173 | private void loadConfigAction() { 174 | File sf = FileChooser.openFile(panel, FileChooser.getLastWorkingDirectory(), 175 | "Save config file", "json"); 176 | 177 | if (sf != null) { 178 | // Set the last working directory. 179 | FileChooser.setLastWorkingDirectory(sf.getParent()); 180 | 181 | String configFromFile = ""; 182 | // Read the file and load it into extensionConfig. 183 | try { 184 | configFromFile = FileUtils.readFileToString(sf, StringUtils.UTF8); 185 | extensionConfig = Config.loadConfig(configFromFile); 186 | } catch (IOException e) { 187 | log.alert("Could not open config file %s.", sf.getAbsolutePath()); 188 | log.error("Could not open config file %s.", sf.getAbsolutePath()); 189 | log.error("%s", StringUtils.getStackTrace(e)); 190 | } 191 | 192 | log.debug("Loaded extension config from %s and saved it to extension settings", 193 | sf.getAbsolutePath()); 194 | log.debug("Loaded config: %s", configFromFile); 195 | } 196 | } 197 | 198 | private void saveConfigAction() { 199 | File sf = FileChooser.saveFile(panel, FileChooser.getLastWorkingDirectory(), 200 | "Save config file", "json"); 201 | 202 | if (sf != null) { 203 | // Set the last working directory. 204 | FileChooser.setLastWorkingDirectory(sf.getParent()); 205 | // Save the file. 206 | try { 207 | extensionConfig.writeToFile(sf); 208 | } catch (Exception e) { 209 | String errMsg = 210 | String.format("Could not write to file: %s", StringUtils.getStackTrace(e)); 211 | log.alert(errMsg); 212 | log.error(errMsg); 213 | } 214 | } 215 | } 216 | 217 | // Called when the toggle button state changes. 218 | private void processToggleAction(ActionEvent evt) { 219 | // http://www.java2s.com/Tutorials/Java/Java_Swing/0880__Java_Swing_JToggleButton.htm 220 | AbstractButton abstractButton = (AbstractButton) evt.getSource(); 221 | boolean selected = abstractButton.getModel().isSelected(); 222 | if (selected) { 223 | lintPool.resume(); 224 | } else { 225 | lintPool.pause(); 226 | } 227 | } 228 | 229 | // Filter the tablemodel. 230 | private void searchAction(String query) { 231 | // Change 1 to 0 if you want to search by host instead of URL. 232 | lintTable.filter(query, 1); 233 | } 234 | 235 | // GUI Variables 236 | private JButton loadConfigButton; 237 | private JButton saveConfigButton; 238 | private JTextField searchTextField; 239 | private JButton resetButton; 240 | private JPanel topPanel; 241 | private JToggleButton processToggleButton; 242 | private JSeparator topSeparator; 243 | private JScrollPane tableScrollPane; 244 | } -------------------------------------------------------------------------------- /src/lint/Beautify.java: -------------------------------------------------------------------------------- 1 | package lint; 2 | 3 | import static burp.BurpExtender.log; 4 | 5 | import java.io.File; 6 | 7 | import com.google.gson.GsonBuilder; 8 | 9 | import org.apache.commons.io.FileUtils; 10 | import org.apache.commons.io.FilenameUtils; 11 | import utils.CustomException; 12 | import utils.Exec; 13 | import utils.StringUtils; 14 | 15 | /** 16 | * BeautifyTask runs beautify on the input and stores the data at storagepath. 17 | */ 18 | public class Beautify { 19 | 20 | private String data; 21 | private Metadata metadata; 22 | // The directory where beautified files should be stored. 23 | private String storagePath = ""; 24 | // The path to the js-beautify command, this comes from 25 | // extensionConfig.jsBeautifyBinaryPath. 26 | private String jsBeautifyCommandPath = ""; 27 | 28 | public Beautify( 29 | String data, 30 | Metadata metadata, 31 | String storagePath, 32 | String jsBeautifyCommandPath 33 | ) { 34 | 35 | this.data = data; 36 | this.metadata = metadata; 37 | this.storagePath = storagePath; 38 | this.jsBeautifyCommandPath = jsBeautifyCommandPath; 39 | log.debug( 40 | "Created a new BeautifyTask.\nmetadata\n%s\nStorage path: %s", 41 | metadata.toString(), 42 | storagePath 43 | ); 44 | } 45 | 46 | // 1. Beautifies the data. 47 | // 2. Stores the result in the storage path. 48 | // 3. Returns the result. 49 | public String execute() throws CustomException { 50 | 51 | Exec beautify = null; 52 | String jsFilePath = ""; 53 | 54 | try { 55 | // Create the filename for this URL minus the extension. 56 | String jsFileName = metadata.getFileNameWithoutExtension(); 57 | // Add the js extension. 58 | jsFilePath = FilenameUtils.concat(storagePath, jsFileName.concat(".js")); 59 | // Create the File to hold the beautified JavaScript. 60 | File jsFile = new File(jsFilePath); 61 | 62 | // Add the extracted JavaScript to data. 63 | StringBuilder sb = new StringBuilder(data); 64 | // Write the contents to a file in the storage path. 65 | FileUtils.writeStringToFile(jsFile, sb.toString(), StringUtils.UTF8); 66 | 67 | // Get the working directory of the js-beautify command. This is 68 | // usually the root of the `eslint-security` repo. 69 | String workingDirectory = FilenameUtils.getFullPath(jsBeautifyCommandPath); 70 | 71 | // Now we have a file with metadata and not-beautified JavaScript. 72 | // `js-beautify -f [filename] -r` 73 | // -r or --replace replace the same file with the beautified content 74 | // this will hopefully keep the metadata string intact (because it's 75 | // a comment). 76 | 77 | // Execute `js-beautify -f [filename] -r` 78 | String[] beautifyArgs = new String[] { "-f", jsFilePath, "-r" }; 79 | beautify = new Exec(jsBeautifyCommandPath, beautifyArgs, workingDirectory); 80 | 81 | beautify.exec(); 82 | log.debug("Executing %s", beautify.getCommandLine()); 83 | log.debug("Output: %s", beautify.getStdOut()); 84 | 85 | // Read the beautified JavaScript from jsFile. This is the return 86 | // value. However, we must first add the metadata string to the file 87 | // and rewrite it. 88 | String beautifiedJS = FileUtils.readFileToString(jsFile, StringUtils.UTF8); 89 | 90 | // TODO: This might be inefficient, might need to find a better 91 | // way to do it. 92 | 93 | // Create the metadata string. 94 | sb = new StringBuilder(metadata.toCommentString()); 95 | // Add the beautified JavaScript to it. 96 | sb.append(beautifiedJS); 97 | // Write the data to the file. 98 | FileUtils.writeStringToFile(jsFile, sb.toString(), StringUtils.UTF8); 99 | 100 | // Return the beautified JavaScript without the metadata. 101 | return beautifiedJS; 102 | 103 | } catch (Exception e) { 104 | String status = StringUtils.getStackTrace(e); 105 | if (beautify != null) { 106 | if (StringUtils.isNotEmpty(beautify.getStdErr())) { 107 | status += beautify.getStdOut(); 108 | } 109 | } 110 | 111 | String errorMessage = String.format( 112 | "Error beautifying file %s in %s:\n%s", 113 | jsFilePath, 114 | metadata.toUglyString(), 115 | status 116 | ); 117 | 118 | log.error(errorMessage); 119 | throw new CustomException(errorMessage); 120 | } 121 | } 122 | 123 | public String toString() { 124 | return new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(this); 125 | } 126 | } -------------------------------------------------------------------------------- /src/lint/Lint.java: -------------------------------------------------------------------------------- 1 | package lint; 2 | 3 | import static burp.BurpExtender.log; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | import org.apache.commons.io.FileUtils; 11 | import org.apache.commons.io.FilenameUtils; 12 | 13 | import burp.Config; 14 | import linttable.LintResult; 15 | import utils.Exec; 16 | import utils.StringUtils; 17 | 18 | /** 19 | * LintTask lints a bunch of JavaScript text and returns the result. 20 | */ 21 | public class Lint { 22 | 23 | private Metadata metadata; 24 | private String javascript = ""; 25 | private Config extensionConfig; 26 | 27 | public Lint(Metadata metadata, String javascript, Config extensionConfig) { 28 | this.metadata = metadata; 29 | this.javascript = javascript; 30 | this.extensionConfig = extensionConfig; 31 | } 32 | 33 | // Runs the linter. 34 | public LintResult execute() throws IOException { 35 | 36 | // ESLint command's working directory. 37 | String eslintDirectory = FilenameUtils.getFullPath(extensionConfig.eslintCommandPath); 38 | 39 | // Store the JavaScript in a temp file. The method takes care of 40 | // filename uniqueness. 41 | File tempJS = File.createTempFile("eslint", ""); // Throws IOException. 42 | String tempJSPath = tempJS.getAbsolutePath(); 43 | // Store the data in the temp file. 44 | FileUtils.write(tempJS, javascript, StringUtils.UTF8); 45 | 46 | // Create the output filename for this data. 47 | String eslintResultFileName = metadata.getFileNameWithoutExtension().concat("-linted.js"); 48 | // Create the full path for the output file. 49 | String eslintResultFilePath = FilenameUtils.concat(extensionConfig.lintOutputPath, eslintResultFileName); 50 | 51 | // Create linter args to run ESLint. 52 | String[] linterArgs = new String[] { "-c", extensionConfig.eslintConfigPath, "-f", "codeframe", 53 | "--no-color", 54 | // "-o", eslintResultFileName, // We can use this if we want to create the 55 | // output file manually. 56 | "--no-inline-config", tempJSPath }; 57 | 58 | // Create the ESLint Exec. 59 | Exec linter = new Exec( 60 | extensionConfig.eslintCommandPath, 61 | linterArgs, 62 | eslintDirectory, 63 | 0, 1, 2 // Exit values for ESLint. 64 | ); 65 | 66 | log.debug("Executing %s", linter.getCommandLine()); 67 | int exitVal = linter.exec(); 68 | // If exitVal is 2, it means there was a parsing error. In this case 69 | // we do not want an exception but we will log the error. 70 | String results = linter.getStdOut(); 71 | String err = linter.getStdErr(); 72 | 73 | String status = ""; 74 | if (exitVal == 2 || exitVal == 1) { 75 | status += err; 76 | } 77 | 78 | // Add the metadata to the output file contents. 79 | StringBuilder eslintResults = new StringBuilder(metadata.toCommentString()); 80 | // Add the ESLint results. 81 | eslintResults.append(results); 82 | 83 | // Write the results to the output file. 84 | FileUtils.writeStringToFile(new File(eslintResultFilePath), eslintResults.toString(), StringUtils.UTF8); 85 | 86 | // Process results 87 | 88 | // Regex to separate the results. 89 | // (.*?)\n\n\n 90 | 91 | String ptrn = "(.*?)\n\n\n"; 92 | int flags = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; 93 | Pattern pt = Pattern.compile(ptrn, flags); 94 | Matcher mt = pt.matcher(results); 95 | 96 | // Now each item in the matcher is a separate finding. 97 | // TODO Do something with each finding. 98 | int numResults = (int) mt.results().count(); 99 | 100 | log.debug("Results file: %s", eslintResultFilePath); 101 | log.debug("Input file: %s", tempJSPath); 102 | 103 | if (StringUtils.isEmpty(status)) status = "Linted"; 104 | 105 | // Start creating the returning LintResult. 106 | LintResult lr = new LintResult( 107 | metadata, 108 | metadata.getHost(), 109 | metadata.getURL(), 110 | metadata.getHash(), 111 | javascript, 112 | status, 113 | results, 114 | 1, 115 | numResults 116 | ); 117 | 118 | return lr; 119 | } 120 | } -------------------------------------------------------------------------------- /src/lint/LintTask.java: -------------------------------------------------------------------------------- 1 | package lint; 2 | 3 | import static burp.BurpExtender.db; 4 | import static burp.BurpExtender.log; 5 | import java.io.IOException; 6 | import java.sql.SQLException; 7 | import burp.Config; 8 | import linttable.LintResult; 9 | import utils.CustomException; 10 | import utils.StringUtils; 11 | 12 | /** 13 | * LintTask 14 | */ 15 | public class LintTask implements Runnable { 16 | 17 | // private Metadata metadata; 18 | private Config extensionConfig; 19 | private LintResult lr; 20 | private String status = ""; 21 | 22 | public LintTask(Config extensionConfig, LintResult lr) { 23 | this.extensionConfig = extensionConfig; 24 | this.lr = lr; 25 | } 26 | 27 | @Override 28 | public void run() { 29 | 30 | LintResult linted = null; 31 | 32 | try { 33 | // First we need to beautify. 34 | Beautify be = new Beautify(lr.beautifiedJavaScript, lr.metadata, extensionConfig.storagePath, 35 | extensionConfig.jsBeautifyCommandPath); 36 | 37 | String beautifiedJS = be.execute(); 38 | 39 | // Next we need to lint it. 40 | Lint lint = new Lint(lr.metadata, beautifiedJS, extensionConfig); 41 | 42 | linted = lint.execute(); 43 | 44 | } catch (CustomException e) { 45 | log.error("Inside LintTask for %s - %s", e.getMessage(), lr.metadata.toUglyString()); 46 | status = e.getMessage(); 47 | } catch (IOException e) { 48 | log.error("Inside LintTask for %s - %s", StringUtils.getStackTrace(e), lr.metadata.toUglyString()); 49 | status += e.getMessage(); 50 | } catch (Exception e) { // TODO Is this needed? 51 | log.error("Inside LintTask for %s - %s", StringUtils.getStackTrace(e), lr.metadata.toUglyString()); 52 | status += e.getMessage(); 53 | } finally { 54 | 55 | // If linted != null, there was an error. 56 | // We update the status and store the original LintResult. 57 | 58 | // This means that if beautify was executed w/o errors and lint 59 | // failed, we are throwing away the beautify results away. 60 | LintResult updatedRecord; 61 | 62 | if (linted != null) { 63 | updatedRecord = linted; 64 | } else { 65 | updatedRecord = lr; 66 | updatedRecord.status = status; 67 | } 68 | 69 | try { 70 | int up = db.updateRow(updatedRecord); 71 | log.debug("db.updateRow: %d", up); 72 | } catch (IOException | SQLException e) { 73 | log.error("Inside LintTask for %s - %s", StringUtils.getStackTrace(e), lr.metadata.toUglyString()); 74 | } 75 | } 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /src/lint/LintingThread.java: -------------------------------------------------------------------------------- 1 | package lint; 2 | 3 | import static burp.BurpExtender.db; 4 | import static burp.BurpExtender.log; 5 | import java.io.IOException; 6 | import java.sql.SQLException; 7 | import java.util.ArrayList; 8 | import burp.Config; 9 | import database.SelectResult; 10 | import utils.PausableExecutor; 11 | import utils.StringUtils; 12 | 13 | /** 14 | * ProcessLintQueue 15 | */ 16 | public class LintingThread implements Runnable { 17 | 18 | private final PausableExecutor pool; 19 | private final Config extensionConfig; 20 | private long lastRowID = 0; 21 | private volatile boolean running; 22 | 23 | public LintingThread(Config extensionConfig, PausableExecutor pool) { 24 | 25 | this.extensionConfig = extensionConfig; 26 | this.pool = pool; 27 | this.running = true; 28 | } 29 | 30 | // 1. Read every row where (rowid > lastRowID AND is_processed != 1) 31 | // 2. Add each row to the threadpool and update lastRowID if it's larger 32 | // than the current one. 33 | // 3. Update lastRowID with the largest row. Last record will have the 34 | // largest row. 35 | // 4. Sleep for X seconds. 36 | // 5. Go to 1. 37 | public void process() throws IOException, SQLException, InterruptedException { 38 | 39 | while(running) { 40 | if (db != null) { 41 | // 1. Read every row where (rowid > lastRowID AND is_processed != 1) 42 | final ArrayList results = db.getNewRows(lastRowID); 43 | log.debug("Inside ProcessLintQueue - Reading the results."); 44 | 45 | // 2. Add each row to the threadpool. 46 | for (SelectResult res : results) { 47 | 48 | log.debug("Inside ProcessLintQueue - res.rowid: %d.", res.rowid); 49 | // Check if res.lr.hash exists in the threadpool. 50 | // Add each res to the threadpool. 51 | pool.execute(new LintTask(extensionConfig, res.lr)); 52 | // 3. Update lastRowID. 53 | // Update the lastRowID because things might go bad in the middle. 54 | if (res.rowid > lastRowID) lastRowID = res.rowid; 55 | } 56 | log.debug("Inside ProcessLintQueue - Finished iterating through the results."); 57 | log.debug("Inside ProcessLintQueue - lastrowid %d.", lastRowID); 58 | 59 | // We could also update lastRowID here with the rowID of the last record 60 | // in results. Foreach does not change the order. 61 | 62 | log.debug("Inside ProcessLintQueue - Sleeping for %d seconds.", extensionConfig.lintTaskDelay); 63 | // 4. Sleep for X seconds. 64 | Thread.sleep(extensionConfig.lintTaskDelay * 1000); 65 | 66 | /// 5. Go to 1. 67 | } 68 | } 69 | 70 | log.debug("Inside ProcessLintQueue - Done with the ProcessLintQueue thread."); 71 | pool.pause(); 72 | // Let's see if closing the database here fixes issue #30. 73 | db.close(); 74 | log.debug("Inside ProcessLintQueue - Finished after pool is paused."); 75 | } 76 | 77 | @Override 78 | public void run() { 79 | try { 80 | process(); 81 | } catch (Exception e) { 82 | log.error("Inside ProcessLintQueue - %s", StringUtils.getStackTrace(e)); 83 | } 84 | } 85 | 86 | // Stop the thread. 87 | public void stop() { 88 | running = false; 89 | } 90 | 91 | // Wrap it in a thread. 92 | public void start() { 93 | new Thread(this).start(); 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /src/lint/Metadata.java: -------------------------------------------------------------------------------- 1 | package lint; 2 | 3 | import java.net.MalformedURLException; 4 | import java.net.URL; 5 | 6 | import com.google.gson.Gson; 7 | import com.google.gson.GsonBuilder; 8 | 9 | import burp.Config; 10 | import utils.StringUtils; 11 | 12 | /** 13 | * Metadata stores the metadata for each JS file. 14 | */ 15 | public class Metadata { 16 | private String url; 17 | private String referer; 18 | private String hash; 19 | 20 | public Metadata() {} 21 | 22 | public Metadata(String u, String ref, String hsh) { 23 | url = u; 24 | referer = ref; 25 | hash = hsh; 26 | } 27 | 28 | public static Metadata fromString(String jsonString) { 29 | return new Gson().fromJson(jsonString, Metadata.class); 30 | } 31 | 32 | public String getURL() { 33 | return url; 34 | } 35 | 36 | public void setURL(String u) { 37 | url = u; 38 | } 39 | 40 | public String getReferer() { 41 | return referer; 42 | } 43 | 44 | public void setReferer(String r) { 45 | referer = r; 46 | } 47 | 48 | public String getHash() { 49 | return hash; 50 | } 51 | 52 | public void setHash(String h) { 53 | hash = h; 54 | } 55 | 56 | // Returns the metadata object as a json string. 57 | public String toString() { 58 | return new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(this); 59 | } 60 | 61 | // toString() but wrapped in /* */ 62 | public String toCommentString() { 63 | return "/*\n" + toString() + "\n*/\n\n"; 64 | } 65 | 66 | // non-indented toString(). 67 | public String toUglyString() { 68 | return new GsonBuilder().disableHtmlEscaping().create().toJson(this); 69 | } 70 | 71 | // Returns the filename calculated from the metadata object minus the 72 | // extension. 73 | 74 | // Filename will be 75 | // "filename_from_URL[minus extension and limited to 50 chars]-[hash]". 76 | public String getFileNameWithoutExtension() throws MalformedURLException { 77 | 78 | String jsFileName = ""; 79 | 80 | jsFileName = StringUtils.getURLBaseName(getURL()); 81 | // Limit the file name to Config.urlFileNameLimit. 82 | if (jsFileName.length() > Config.urlFileNameLimit) { 83 | jsFileName = jsFileName.substring(0, Config.urlFileNameLimit); 84 | } 85 | // Replace illegal characters in the filename. 86 | // https://stackoverflow.com/a/15075907 87 | jsFileName = jsFileName.replaceAll("[^a-zA-Z0-9\\.\\-]", "_"); 88 | if (!StringUtils.isEmpty(jsFileName)) { 89 | // If the URL does not end in a file jsFileName will be empty. 90 | // If it's not empty, we add the "-" to it. 91 | jsFileName = jsFileName.concat("-"); 92 | } 93 | // If jsFileName was empty do nothing. 94 | // Attach the hash and the extension. 95 | jsFileName = jsFileName.concat(getHash()); 96 | return jsFileName; 97 | } 98 | 99 | public String getHost() throws MalformedURLException { 100 | return new URL(getURL()).getHost(); 101 | } 102 | } -------------------------------------------------------------------------------- /src/lint/ProcessRequestTask.java: -------------------------------------------------------------------------------- 1 | package lint; 2 | 3 | import static burp.BurpExtender.callbacks; 4 | import static burp.BurpExtender.log; 5 | import java.net.URL; 6 | import burp.Config; 7 | import burp.IHttpRequestResponse; 8 | import utils.ReqResp; 9 | import utils.StringUtils; 10 | 11 | /** 12 | * ProcessRequestTask processes each incoming request. 13 | */ 14 | public class ProcessRequestTask implements Runnable { 15 | 16 | private int toolFlag; 17 | private IHttpRequestResponse requestResponse; 18 | private Config extensionConfig; 19 | 20 | // Constructor. 21 | public ProcessRequestTask( 22 | int toolFlag, 23 | IHttpRequestResponse requestResponse, 24 | Config config 25 | ) { 26 | 27 | this.toolFlag = toolFlag; 28 | this.requestResponse = requestResponse; 29 | this.extensionConfig = config; 30 | } 31 | 32 | // This is how we process each incoming request. 33 | @Override 34 | public void run() { 35 | 36 | // This will be the thread identifier for logs. 37 | String threadURL = requestResponse.getHttpService().toString(); 38 | log.debug("----------"); 39 | final String toolName = callbacks.getToolName(toolFlag); 40 | // Some toolFlags have the same toolName. See table in `Config.java`. 41 | log.debug( 42 | "Inside the request thread for %s. Got a request. Tool: %s - Tool Flag: %d", 43 | threadURL, 44 | toolName, 45 | toolFlag 46 | ); 47 | 48 | // Only process if the callbacks.getToolName(toolFlag) is in 49 | // processTools, otherwise return. 50 | if (!StringUtils.arrayContains(toolName, extensionConfig.processToolList)) { 51 | log.debug( 52 | "Inside the request thread for %s. %s is not in the process-tool-list, returning from ProcessRequestTask.", 53 | threadURL, 54 | toolName 55 | ); 56 | return; 57 | } 58 | 59 | // Check if the request is in scope. 60 | if (extensionConfig.processInScope) { 61 | // Get the request URL. 62 | URL reqURL = ReqResp.getURL(requestResponse); 63 | if (!callbacks.isInScope(reqURL)) { 64 | // Request is not in scope, return. 65 | log.debug( 66 | "Inside the request thread for %s. Request is not in scope, returning from ProcessRequestTask.", 67 | threadURL 68 | ); 69 | return; 70 | } 71 | } 72 | 73 | // Remove the specified headers (in Config's "removable-headers") from 74 | // the request. 75 | for (final String hdr : extensionConfig.headersToRemove) { 76 | requestResponse = ReqResp.removeHeader(true, requestResponse, hdr); 77 | } 78 | log.debug( 79 | "Inside the request thread for %s. Removed headers from the request, returning from ProcessRequestTask.", 80 | threadURL 81 | ); 82 | 83 | // We are done here. 84 | log.debug("Inside the request thread for %s. Finished", threadURL); 85 | return; 86 | } 87 | } -------------------------------------------------------------------------------- /src/lint/ProcessResponseTask.java: -------------------------------------------------------------------------------- 1 | package lint; 2 | 3 | import static burp.BurpExtender.db; 4 | import static burp.BurpExtender.log; 5 | import java.io.IOException; 6 | import java.sql.SQLException; 7 | import com.google.gson.GsonBuilder; 8 | import linttable.LintResult; 9 | import utils.StringUtils; 10 | 11 | /** 12 | * ProcessResponseTask processes one intercepted response. 13 | */ 14 | public class ProcessResponseTask implements Runnable { 15 | 16 | private String data; 17 | private Metadata metadata; 18 | 19 | public ProcessResponseTask( 20 | String data, Metadata metadata) { 21 | 22 | this.data = data; 23 | this.metadata = metadata; 24 | log.debug( 25 | "Created a new ProcessResponseTask.\nmetadata: %s", 26 | metadata.toUglyString() 27 | ); 28 | } 29 | 30 | @Override 31 | public void run() { 32 | 33 | try { 34 | 35 | // Create a LintResult to store in the table. 36 | LintResult lr = new LintResult( 37 | metadata, // metadata 38 | metadata.getHost(), // host 39 | metadata.getURL(), // url 40 | metadata.getHash(), // hash 41 | data, // javascript -- not beautified yet 42 | "Not Beautified", // status 43 | "", // eslint results 44 | 0, // is_processed 45 | 0 // number of results 46 | ); 47 | 48 | // Add the data to the table. 49 | db.addRow(lr); 50 | 51 | log.debug("Added new request to the table: %s", metadata.toUglyString()); 52 | 53 | // If the row already exists in the table, we will not reach here. 54 | 55 | } catch (SQLException | IOException e) { 56 | if (e.getMessage().contains("UNIQUE constraint failed")) { 57 | log.debug( 58 | "Row %s already exists. Skipping.", 59 | metadata.toUglyString() 60 | ); 61 | } else { 62 | log.error(StringUtils.getStackTrace(e)); 63 | } 64 | } 65 | } 66 | 67 | public String toString() { 68 | return new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(this); 69 | } 70 | } -------------------------------------------------------------------------------- /src/lint/UpdateTableThread.java: -------------------------------------------------------------------------------- 1 | package lint; 2 | 3 | import static burp.BurpExtender.db; 4 | import static burp.BurpExtender.log; 5 | import static burp.BurpExtender.mainTab; 6 | import java.io.IOException; 7 | import java.sql.SQLException; 8 | import java.util.ArrayList; 9 | import javax.swing.SwingUtilities; 10 | import linttable.LintResult; 11 | import utils.StringUtils; 12 | 13 | /** 14 | * UpdateTableThread updates the table with the results from the database. 15 | */ 16 | public class UpdateTableThread implements Runnable { 17 | 18 | private int delay; 19 | private volatile boolean running; 20 | 21 | public UpdateTableThread(int delay) { 22 | this.delay = delay; 23 | this.running = true; 24 | } 25 | 26 | // 1. Read all rows from the database. 27 | // 2. Delete all rows in the table model. 28 | // 3. Add all new rows to the JTable. 29 | public void process() throws IOException, SQLException, InterruptedException { 30 | 31 | while (running) { 32 | if (db != null) { 33 | // 1. Read every row from the table eslint in the database. 34 | final ArrayList results = db.getAllRows(); 35 | log.debug("Inside UpdateTableThread - Reading all rows."); 36 | 37 | // 2. Delete all rows in the table model. 38 | // 3. Add all rows to the table. 39 | // populate() does both. 40 | // Do everything inside the Swing Event Dispatch Thread. 41 | SwingUtilities.invokeLater(new Runnable() { 42 | public void run() { 43 | if (mainTab != null) { 44 | mainTab.lintTable.populate(results); 45 | } 46 | log.debug("Inside UpdateTableThread - Updated the table from the database."); 47 | } 48 | }); 49 | 50 | log.debug("Inside UpdateTableThread - Sleeping for %d seconds.", delay); 51 | // 4. Sleep for X seconds. 52 | Thread.sleep(delay * 1000); 53 | 54 | /// 5. Go to 1. 55 | } 56 | } 57 | db.close(); 58 | log.debug("Inside UpdateTableThread - Done with the thread."); 59 | } 60 | 61 | @Override 62 | public void run() { 63 | try { 64 | process(); 65 | } catch (Exception e) { 66 | log.error("Inside UpdateTableThread - %s.", StringUtils.getStackTrace(e)); 67 | } 68 | } 69 | 70 | // Stop the thread. 71 | public void stop() { 72 | running = false; 73 | } 74 | 75 | // Wrap it in a thread. 76 | public void start() { 77 | new Thread(this).start(); 78 | } 79 | } -------------------------------------------------------------------------------- /src/linttable/LintResult.java: -------------------------------------------------------------------------------- 1 | package linttable; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import lint.Metadata; 6 | 7 | /** 8 | * LintRow 9 | */ 10 | // Displayed in the LintTable. 11 | public class LintResult { 12 | 13 | public Metadata metadata; 14 | public String host; 15 | public String url; 16 | public String hash; 17 | public transient String beautifiedJavaScript; // transient == does not appear in toString() 18 | public String status; 19 | public transient String results; // transient 20 | public int isProcessed; 21 | public int numResults; 22 | 23 | public LintResult() {} 24 | 25 | public LintResult( 26 | Metadata metadata, String host, String url, String hash, 27 | String beautifiedJavaScript, String status, String results, 28 | int isProcessed, int numResults) { 29 | 30 | this.metadata = metadata; 31 | this.host = host; 32 | this.url = url; 33 | this.hash = hash; 34 | this.beautifiedJavaScript = beautifiedJavaScript; 35 | this.status = status; 36 | this.results = results; 37 | this.isProcessed = isProcessed; 38 | this.numResults = numResults; 39 | } 40 | 41 | public String toString() { 42 | return new Gson().toJson(this); 43 | } 44 | } -------------------------------------------------------------------------------- /src/linttable/LintTable.java: -------------------------------------------------------------------------------- 1 | package linttable; 2 | 3 | import static burp.BurpExtender.log; 4 | 5 | import java.awt.event.MouseEvent; 6 | import java.awt.event.MouseListener; 7 | import java.io.File; 8 | import java.util.ArrayList; 9 | 10 | import javax.swing.JPopupMenu; 11 | import javax.swing.JTable; 12 | import javax.swing.RowFilter; 13 | import javax.swing.table.TableColumnModel; 14 | import javax.swing.table.TableRowSorter; 15 | 16 | import org.apache.commons.io.FileUtils; 17 | import org.apache.commons.io.FilenameUtils; 18 | 19 | import utils.FileChooser; 20 | import utils.StringUtils; 21 | 22 | /** 23 | * LintTable 24 | */ 25 | public class LintTable extends JTable implements MouseListener { 26 | 27 | private LintTableModel model; 28 | private TableRowSorter sorter; 29 | 30 | public LintTable() { 31 | model = new LintTableModel(); 32 | initTable(); 33 | } 34 | 35 | private void initTable() { 36 | 37 | addMouseListener(this); 38 | 39 | // setAutoCreateRowSorter(true); 40 | setModel(model); 41 | sorter = new TableRowSorter(model); 42 | setRowSorter(sorter); 43 | 44 | // Reduce the size of the last two columns. 45 | // TODO Change this if we change the table columns. 46 | // http://glazedlists.1045722.n5.nabble.com/Setting-JTable-column-widths-to-different-percentages-of-the-total-table-td3417756.html 47 | setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); 48 | TableColumnModel colModel = getColumnModel(); 49 | // This works, not sure why but it works. 50 | colModel.getColumn(0).setPreferredWidth(2000); 51 | colModel.getColumn(1).setPreferredWidth(6000); 52 | colModel.getColumn(2).setPreferredWidth(1500); 53 | colModel.getColumn(3).setPreferredWidth(900); 54 | } 55 | 56 | public int getTableSelectedRow() { 57 | return convertRowIndexToModel(getSelectedRow()); 58 | } 59 | 60 | public LintResult get(int index) { 61 | return model.get(index); 62 | } 63 | 64 | public LintResult getSelectedResult() { 65 | return get(getTableSelectedRow()); 66 | } 67 | 68 | public void add(LintResult lr) { 69 | model.add(lr); 70 | } 71 | 72 | public void delete(int index) { 73 | model.delete(index); 74 | } 75 | 76 | public void clear() { 77 | model.clear(); 78 | } 79 | 80 | public ArrayList getAll() { 81 | return model.getAll(); 82 | } 83 | 84 | public void populate(ArrayList res) { 85 | model.populate(res); 86 | } 87 | 88 | // Filters column with columnIndex by text. 89 | // https://docs.oracle.com/javase/tutorial/uiswing/examples/components/TableFilterDemoProject/src/components/TableFilterDemo.java 90 | public void filter(String text, int columnIndex) throws IndexOutOfBoundsException { 91 | 92 | // Check if column is invalid. 93 | if (model.invalidColumnIndex(columnIndex)) { 94 | // If column is invalid throw an exception. 95 | String errorMessage = 96 | String.format("Requested column index: %s - max column index: %s", columnIndex, model.getColumnCount()); 97 | throw new IndexOutOfBoundsException(errorMessage); 98 | } 99 | 100 | RowFilter rf = null; 101 | // If the current expression doesn't parse, don't update. 102 | try { 103 | rf = RowFilter.regexFilter(text, columnIndex); 104 | 105 | } catch (java.util.regex.PatternSyntaxException e) { 106 | return; 107 | } 108 | sorter.setRowFilter(rf); 109 | } 110 | 111 | 112 | // Implementing MouseListener. 113 | 114 | @Override 115 | public void mouseClicked(MouseEvent e) { 116 | // TODO implement this. 117 | // Double-click should open the save results menu. 118 | 119 | if (e.getClickCount() == 2) { 120 | saveRecord(e); 121 | } 122 | 123 | } 124 | 125 | @Override 126 | public void mousePressed(MouseEvent e) {} 127 | 128 | @Override 129 | public void mouseReleased(MouseEvent e) {} 130 | 131 | @Override 132 | public void mouseEntered(MouseEvent e) {} 133 | 134 | @Override 135 | public void mouseExited(MouseEvent e) {} 136 | 137 | // End of MouseListener implementation. 138 | 139 | // save should let user select a directory and then save two files 140 | // there, one is beautified JavaScript and the other is the ESLint 141 | // results. Both of these files should belong to the record that was 142 | // right-clicked. 143 | private void saveRecord(MouseEvent evt) { 144 | 145 | LintTable table = (LintTable) evt.getSource(); 146 | // Get the selected result. Because we are updating the table and losing 147 | // our selection every second, we need to get the results first. 148 | LintResult selected = table.getSelectedResult(); 149 | // Open the directory selection dialog. 150 | String lastDir = FileChooser.getLastWorkingDirectory(); 151 | File selectedDir = FileChooser.saveDirectory(table.getParent(), lastDir, "Save JavaScript and Results"); 152 | 153 | if (selectedDir != null) { 154 | try { 155 | // Get the selected directory as a string. 156 | String selectedPath = selectedDir.getPath(); 157 | // Create the beautified JavaScript file name. 158 | String beautifiedFileName = selected.metadata.getFileNameWithoutExtension().concat(".js"); 159 | String beautifiedFilePath = FilenameUtils.concat(selectedPath, beautifiedFileName); 160 | 161 | String resultsFileName = selected.metadata.getFileNameWithoutExtension().concat("-linted.js"); 162 | String resultsFilePath = FilenameUtils.concat(selectedPath, resultsFileName); 163 | 164 | // Save both files. 165 | 166 | // Issue 37. 167 | // Add the metadata strings to file before saving them. 168 | StringBuilder sbJS = new StringBuilder(selected.metadata.toCommentString()); 169 | sbJS.append(selected.beautifiedJavaScript); 170 | FileUtils.writeStringToFile(new File(beautifiedFilePath), sbJS.toString(), StringUtils.UTF8); 171 | log.debug("Stored beautified JavaScipt in: %s.", beautifiedFilePath); 172 | 173 | StringBuilder sbRes = new StringBuilder(selected.metadata.toCommentString()); 174 | sbRes.append(selected.results); 175 | FileUtils.writeStringToFile(new File(resultsFilePath), sbRes.toString(), StringUtils.UTF8); 176 | log.debug("Stored results in: %s.", resultsFilePath); 177 | 178 | // Save the directory as the last working directory. 179 | FileChooser.setLastWorkingDirectory(selectedPath); 180 | } catch (Exception e) { 181 | log.error("Could not save results %s.", StringUtils.getStackTrace(e)); 182 | } 183 | return; 184 | } 185 | log.debug("Cancelled the save results dialog."); 186 | } 187 | 188 | } -------------------------------------------------------------------------------- /src/linttable/LintTableModel.java: -------------------------------------------------------------------------------- 1 | package linttable; 2 | 3 | import java.util.ArrayList; 4 | import javax.swing.table.AbstractTableModel; 5 | 6 | import burp.Config; 7 | 8 | /** 9 | * LintTableModel 10 | */ 11 | public class LintTableModel extends AbstractTableModel { 12 | 13 | private String[] columnNames; 14 | private Class[] columnClasses; 15 | private ArrayList lintResults; 16 | 17 | public LintTableModel() { 18 | initTableModel(); 19 | } 20 | 21 | private void initTableModel() { 22 | // Set columns. 23 | columnNames = Config.lintTableColumnNames; 24 | columnClasses = Config.lintTableColumnClasses; 25 | // Create the underlying LintResults. 26 | lintResults = new ArrayList(); 27 | } 28 | 29 | // Implementing AbstractTableModel. 30 | 31 | @Override 32 | public int getRowCount() { 33 | return lintResults.size(); 34 | } 35 | 36 | @Override 37 | public int getColumnCount() { 38 | return columnNames.length; 39 | } 40 | 41 | @Override 42 | public Object getValueAt(int rowIndex, int columnIndex) { 43 | LintResult lr = lintResults.get(rowIndex); 44 | switch (columnIndex) { 45 | case 0: 46 | return lr.host; 47 | case 1: 48 | return lr.url; 49 | case 2: 50 | return lr.status; 51 | case 3: 52 | return lr.numResults; 53 | } 54 | return lr.host; 55 | } 56 | 57 | @Override 58 | public String getColumnName(int column) { 59 | // Returns the column name. 60 | return columnNames[column]; 61 | } 62 | 63 | @Override 64 | public Class getColumnClass(int columnIndex) { 65 | // Returns the column class. 66 | return columnClasses[columnIndex]; 67 | } 68 | 69 | // AbstractTableModel implemented. 70 | 71 | private boolean invalidRowIndex(int index) { 72 | return ((index < 0) || (index >= getRowCount())); 73 | } 74 | 75 | public LintResult get(int index) throws IndexOutOfBoundsException { 76 | if (invalidRowIndex(index)) { 77 | String errorMessage = String.format("Requested index: %s - max index: %s", index, getRowCount()); 78 | throw new IndexOutOfBoundsException(errorMessage); 79 | } 80 | return lintResults.get(index); 81 | } 82 | 83 | public void add(LintResult lr) { 84 | lintResults.add(lr); 85 | fireTableDataChanged(); 86 | } 87 | 88 | public void delete(int index) { 89 | if (invalidRowIndex(index)) { 90 | String errorMessage = String.format("Requested index: %s - max index: %s", index, getRowCount()); 91 | throw new IndexOutOfBoundsException(errorMessage); 92 | } 93 | lintResults.remove(index); 94 | fireTableDataChanged(); 95 | } 96 | 97 | public void edit(int index, LintResult lr) throws IndexOutOfBoundsException { 98 | if (invalidRowIndex(index)) { 99 | String errorMessage = String.format("Requested index: %s - max index: %s", index, getRowCount()); 100 | throw new IndexOutOfBoundsException(errorMessage); 101 | } 102 | lintResults.set(index, lr); 103 | fireTableDataChanged(); 104 | } 105 | 106 | public void clear() { 107 | lintResults.clear(); 108 | fireTableDataChanged(); 109 | } 110 | 111 | public void populate(ArrayList newResults) { 112 | clear(); 113 | lintResults.addAll(newResults); 114 | fireTableDataChanged(); 115 | } 116 | 117 | public ArrayList getAll() { 118 | return lintResults; 119 | } 120 | 121 | // Returns true if a column index is invalid. 122 | public boolean invalidColumnIndex(int columnIndex) { 123 | return ((columnIndex < 0) || (columnIndex >= getColumnCount())); 124 | } 125 | } -------------------------------------------------------------------------------- /src/resources/db/add_row.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO eslint 2 | (metadata, url, hash, beautified_javascript, status, results, is_processed, number_of_results) 3 | VALUES (?,?,?,?,?,?,?,?); -------------------------------------------------------------------------------- /src/resources/db/check_already_processed-trigger.sql: -------------------------------------------------------------------------------- 1 | CREATE TRIGGER IF NOT EXISTS check_if_already_processed_after_insert 2 | AFTER INSERT ON eslint 3 | WHEN 4 | new.is_processed == 0 AND 5 | EXISTS (SELECT 1 FROM eslint WHERE is_processed == 1 AND hash = new.hash) 6 | BEGIN 7 | /* We want to check if the hash is already in the table. If new.hash is 8 | already in the table with is_processed = 1 then we want to update the 9 | status, eslint, is_processed and number_of_results from the first existing row. 10 | This might be problematic later but in theory (at least) all the tables 11 | with is_processed=1 and the same hash should have the same results. */ 12 | 13 | UPDATE eslint 14 | SET 15 | (beautified_javascript, status, is_processed, results, number_of_results) = 16 | ( 17 | SELECT e.beautified_javascript, e.status, e.is_processed, e.results, e.number_of_results 18 | FROM eslint e 19 | WHERE 20 | new.hash = e.hash AND e.is_processed == 1 21 | ) 22 | WHERE 23 | /* This should only target the last inserted row. */ 24 | metadata == new.metadata; 25 | END; 26 | -------------------------------------------------------------------------------- /src/resources/db/create_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS eslint ( 2 | metadata TEXT NOT NULL, 3 | url TEXT, 4 | hash TEXT, 5 | beautified_javascript TEXT, 6 | status TEXT, 7 | results TEXT, 8 | is_processed INTEGER, 9 | number_of_results INTEGER, 10 | PRIMARY KEY (metadata) 11 | ) 12 | -------------------------------------------------------------------------------- /src/resources/db/get-all_rows.sql: -------------------------------------------------------------------------------- 1 | SELECT * FROM eslint 2 | ORDER BY 3 | rowid -------------------------------------------------------------------------------- /src/resources/db/get_new_rows.sql: -------------------------------------------------------------------------------- 1 | SELECT rowid, * FROM eslint 2 | WHERE 3 | rowid > (?) AND is_processed != 1 4 | ORDER BY 5 | rowid -------------------------------------------------------------------------------- /src/resources/db/update_hash-trigger.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Every time a new row is updated with results, it also updates all the rows 3 | that have the same hash and their processed column is 0. 4 | */ 5 | 6 | CREATE TRIGGER IF NOT EXISTS update_for_all_hashes 7 | AFTER UPDATE OF is_processed ON eslint 8 | WHEN 9 | -- Only execute if we are updating the results. 10 | new.is_processed == 1 11 | BEGIN 12 | UPDATE eslint 13 | SET 14 | status = new.status, 15 | results = new.results, 16 | is_processed = new.is_processed, 17 | number_of_results = new.number_of_results 18 | WHERE 19 | hash == new.hash AND is_processed == 0; 20 | END; 21 | -------------------------------------------------------------------------------- /src/resources/db/update_row.sql: -------------------------------------------------------------------------------- 1 | UPDATE eslint 2 | SET 3 | beautified_javascript = ?, 4 | status = ?, 5 | results = ?, 6 | is_processed = ?, 7 | number_of_results = ? 8 | WHERE 9 | metadata = ? -------------------------------------------------------------------------------- /src/utils/BurpLog.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import static burp.BurpExtender.callbacks; 4 | 5 | /** 6 | * BurpLog does some simple logging to the extension's standard and error outputs 7 | */ 8 | public class BurpLog { 9 | 10 | private boolean debugMode = false; 11 | 12 | public BurpLog(boolean debugMode) { 13 | this.debugMode = debugMode; 14 | } 15 | 16 | // debug prints to standard output. 17 | public void debug(String format, Object ...args) { 18 | if (debugMode) StringUtils.printFormat(format, args); 19 | } 20 | 21 | // error prints to error stream. 22 | public void error(String format, Object ...args) { 23 | StringUtils.errorFormat(format, args); 24 | } 25 | 26 | // Create a Burp alert. 27 | public void alert(String format, Object ...args) { 28 | String msg = String.format(format, args); 29 | callbacks.issueAlert(msg); 30 | } 31 | 32 | public boolean isDebugMode() { 33 | return debugMode; 34 | } 35 | 36 | public void setDebugMode(boolean debugMode) { 37 | this.debugMode = debugMode; 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/utils/Constants.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | /** 4 | * Constants 5 | */ 6 | public class Constants { 7 | 8 | public static final String EMPTY_STRING = ""; 9 | } -------------------------------------------------------------------------------- /src/utils/CustomException.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | // Custom exception to return Beautify errors. 4 | public class CustomException extends Exception { 5 | 6 | public CustomException(String errorMessage) { 7 | super(errorMessage); 8 | } 9 | } -------------------------------------------------------------------------------- /src/utils/Exec.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | 9 | import org.apache.commons.exec.CommandLine; 10 | import org.apache.commons.exec.DefaultExecutor; 11 | import org.apache.commons.exec.ExecuteException; 12 | import org.apache.commons.exec.PumpStreamHandler; 13 | import org.apache.commons.io.IOUtils; 14 | 15 | /** 16 | * Exec 17 | */ 18 | public class Exec { 19 | 20 | private String workingDirectory; 21 | private String stdOut = ""; 22 | private String stdErr = ""; 23 | private CommandLine cmdLine; 24 | private int[] exitValues; 25 | 26 | // exitValues are valid exit values. By default, any exit value other than 0 27 | // is treated like an error by Commons-Exec. We can add others here. If you 28 | // set anything other than 0, be sure to include 0 because 0 will then be 29 | // treated like an error. 30 | public Exec(String cmd, String[] args, String workDir, int ...exitValues) { 31 | workingDirectory = workDir; 32 | this.exitValues = exitValues; 33 | 34 | // On Windows Commons-Exec needs the first item to be "cmd.exe" and then 35 | // "/c" and the rest of arguments. 36 | if (SystemUtils.IS_OS_WINDOWS) { 37 | cmdLine = new CommandLine("cmd.exe"); 38 | cmdLine.addArgument("/c"); 39 | // Add the original command. 40 | cmdLine.addArgument(cmd); 41 | } else { 42 | cmdLine = new CommandLine(cmd); 43 | } 44 | // Add the rest of the arguments. 45 | cmdLine.addArguments(args); 46 | } 47 | 48 | public int exec() throws ExecuteException, IOException { 49 | 50 | DefaultExecutor executor = new DefaultExecutor(); 51 | 52 | // How to get both stdout and stderr. 53 | // https://stackoverflow.com/a/34571800 54 | ByteArrayOutputStream stdout = new ByteArrayOutputStream(); 55 | ByteArrayOutputStream stderr = new ByteArrayOutputStream(); 56 | PumpStreamHandler psh = new PumpStreamHandler(stdout, stderr); 57 | executor.setStreamHandler(psh); 58 | if (workingDirectory != null) 59 | executor.setWorkingDirectory(new File(workingDirectory)); 60 | executor.setExitValues(exitValues); 61 | int exitValue = executor.execute(cmdLine); 62 | stdOut = stdout.toString().trim(); 63 | stdErr = stderr.toString().trim(); 64 | return exitValue; 65 | } 66 | 67 | public String getCommandLine() { 68 | return cmdLine.toString(); 69 | } 70 | 71 | public String getStdOut() { 72 | return stdOut; 73 | } 74 | 75 | public String getStdErr() { 76 | return stdErr; 77 | } 78 | 79 | // Executes the Exec object but does not redirect stdout and stderr. 80 | public int execCmd() throws ExecuteException, IOException { 81 | DefaultExecutor executor = new DefaultExecutor(); 82 | if (workingDirectory != null) 83 | executor.setWorkingDirectory(new File(workingDirectory)); 84 | executor.setExitValues(exitValues); 85 | int exitValue = executor.execute(cmdLine); 86 | return exitValue; 87 | } 88 | } -------------------------------------------------------------------------------- /src/utils/FileChooser.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import static burp.BurpExtender.callbacks; 4 | import static utils.StringUtils.isNotEmpty; 5 | 6 | import java.awt.Component; 7 | import java.io.File; 8 | 9 | import javax.swing.JFileChooser; 10 | import javax.swing.JOptionPane; 11 | import javax.swing.filechooser.FileNameExtensionFilter; 12 | 13 | 14 | /** 15 | * FileChooser extends JFileChooser and adds an overwrite warning. 16 | */ 17 | public class FileChooser extends JFileChooser { 18 | 19 | // Show an overwrite warning if the selected file exists. 20 | @Override 21 | public void approveSelection() { 22 | 23 | File selected = getSelectedFile(); 24 | if (selected.exists()) { 25 | int ret = JOptionPane.showConfirmDialog( 26 | this.getParent(), 27 | "Overwrite existing file " + selected + "?", 28 | "File exists", 29 | JOptionPane.OK_CANCEL_OPTION, 30 | JOptionPane.WARNING_MESSAGE 31 | ); 32 | if (ret == JOptionPane.OK_OPTION) 33 | super.approveSelection(); 34 | } else { 35 | super.approveSelection(); 36 | } 37 | } 38 | 39 | // Parent = parent swing component. 40 | // startingPath = where to start. 41 | // title = dialog title. 42 | // extension (e.g., json) = the extension to look for. 43 | // returns null if the dialog is cancelled, treat the result accordingly. 44 | public static File saveFile(Component parent, String startingPath, String title, 45 | String extension) { 46 | 47 | // Issue27, overwrite dialog prompt. 48 | // JFileChooser fc = new JFileChooser(); 49 | FileChooser fc = new FileChooser(); 50 | 51 | // If starting path is set, use it. 52 | if (isNotEmpty(startingPath)) 53 | fc.setCurrentDirectory(new File(startingPath)); 54 | 55 | // If title is set, use it. 56 | if (isNotEmpty(title)) 57 | fc.setDialogTitle(title); 58 | 59 | // If extension is set, create the file filter. 60 | if (isNotEmpty(extension)) { 61 | // "JSON Files (*.json)" 62 | String extFilterString = String.format("%s Files (*.%s)", extension.toUpperCase(), 63 | extension.toLowerCase()); 64 | String[] extFilterList = new String[] {extFilterString}; 65 | FileNameExtensionFilter ff = 66 | new FileNameExtensionFilter(extFilterString, extFilterList); 67 | fc.addChoosableFileFilter(ff); 68 | } 69 | // Only choose files. 70 | fc.setFileSelectionMode(JFileChooser.FILES_ONLY); 71 | // Show the dialog and store the return value. 72 | int retVal = fc.showSaveDialog(parent); 73 | // If the dialog was cancelled, return null. 74 | if (retVal != JFileChooser.APPROVE_OPTION) 75 | return null; 76 | 77 | return fc.getSelectedFile(); 78 | } 79 | 80 | // Parent = parent swing component. 81 | // startingPath = where to start. 82 | // title = dialog title. 83 | // extension (e.g., json) = the extension to look for. 84 | // returns null if the dialog is cancelled, treat the result accordingly. 85 | public static File openFile(Component parent, String startingPath, 86 | String title, String extension) { 87 | 88 | JFileChooser fc = new JFileChooser(); 89 | // If starting path is set, use it. 90 | if (isNotEmpty(startingPath)) 91 | fc.setCurrentDirectory(new File(startingPath)); 92 | 93 | // If title is set, use it. 94 | if (isNotEmpty(title)) 95 | fc.setDialogTitle(title); 96 | 97 | // If extension is set, create the file filter. 98 | if (isNotEmpty(extension)) { 99 | // "JSON Files (*.json)" 100 | String extFilterString = String.format( 101 | "%s Files (*.%s)", 102 | extension.toUpperCase(),extension.toLowerCase() 103 | ); 104 | String[] extFilterList = new String[] {extFilterString}; 105 | FileNameExtensionFilter ff = 106 | new FileNameExtensionFilter(extFilterString, extFilterList); 107 | fc.addChoosableFileFilter(ff); 108 | } 109 | // Only choose files. 110 | fc.setFileSelectionMode(JFileChooser.FILES_ONLY); 111 | // Show the dialog and store the return value. 112 | int retVal = fc.showOpenDialog(parent); // The only difference with saveFile. 113 | // If the dialog was cancelled, return null. 114 | if (retVal != JFileChooser.APPROVE_OPTION) 115 | return null; 116 | 117 | return fc.getSelectedFile(); 118 | } 119 | 120 | // Get last working directory, should be "lastdir" in extension settings. 121 | public static String getLastWorkingDirectory() { 122 | String lastdir = callbacks.loadExtensionSetting("lastdir"); 123 | if (lastdir == null) 124 | return Constants.EMPTY_STRING; 125 | 126 | return lastdir; 127 | } 128 | 129 | public static void setLastWorkingDirectory(String lastdir) { 130 | callbacks.saveExtensionSetting("lastdir", lastdir); 131 | } 132 | 133 | // Opens a JFileChooser dialog to select a directory to do stuff. 134 | // Parent = parent swing component. 135 | // startingPath = where to start. 136 | // title = dialog title. 137 | // returns null if the dialog is cancelled, treat the result accordingly. 138 | public static File saveDirectory(Component parent, String startingPath, String title) { 139 | 140 | JFileChooser fc = new JFileChooser(); 141 | // If starting path is set, use it. 142 | if (isNotEmpty(startingPath)) 143 | fc.setCurrentDirectory(new File(startingPath)); 144 | 145 | // If title is set, use it. 146 | if (isNotEmpty(title)) 147 | fc.setDialogTitle(title); 148 | 149 | // Only choose files. 150 | fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); 151 | // Show the dialog and store the return value. 152 | int retVal = fc.showSaveDialog(parent); // The only difference with openFile. 153 | // If the dialog was cancelled, return null. 154 | if (retVal != JFileChooser.APPROVE_OPTION) 155 | return null; 156 | 157 | // Also check if getSelectedFile() works. 158 | // return fc.getCurrentDirectory(); 159 | return fc.getSelectedFile(); 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /src/utils/Header.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.TreeMap; 7 | 8 | import burp.IHttpRequestResponse; 9 | import static burp.BurpExtender.helpers; 10 | 11 | /** 12 | * Header class. 13 | */ 14 | public class Header { 15 | 16 | private Map> hdr; 17 | private String first; 18 | 19 | public Header() { 20 | // Using a TreeMap like this will make the keys be case-insensitive. 21 | // https://stackoverflow.com/a/22336599 22 | hdr = new TreeMap>(String.CASE_INSENSITIVE_ORDER); 23 | first = ""; 24 | } 25 | 26 | public Header(boolean isRequest, IHttpRequestResponse requestResponse) { 27 | hdr = getBurpHeaders(isRequest, requestResponse); 28 | List tmpHeaders = importFromBurp(isRequest, requestResponse); 29 | if (tmpHeaders != null) { 30 | first = tmpHeaders.get(0); 31 | } else { 32 | first = null; 33 | } 34 | } 35 | 36 | public void add(String header, String value) { 37 | ArrayList vals; 38 | if(hdr.get(header) == null) { 39 | // If not, create the ArrayList. 40 | vals = new ArrayList(); 41 | } else { 42 | // Get the existing ArrayList. 43 | vals = hdr.get(header); 44 | } 45 | vals.add(value); 46 | hdr.put(header, vals); 47 | } 48 | 49 | public ArrayList get(String header) { 50 | return hdr.get(header); 51 | } 52 | 53 | public void overwrite(String header, String value) { 54 | ArrayList vals = new ArrayList(); 55 | vals.add(value); 56 | hdr.put(header, vals); 57 | } 58 | 59 | public void remove(String header) { 60 | hdr.remove(header); 61 | } 62 | 63 | public static List importFromBurp(boolean isRequest, IHttpRequestResponse requestResponse) { 64 | // Get the headers from Burp. 65 | List burpHeaders = null; 66 | if (isRequest) { 67 | burpHeaders = helpers.analyzeRequest(requestResponse).getHeaders(); 68 | } else { 69 | byte[] respBytes = requestResponse.getResponse(); 70 | burpHeaders = helpers.analyzeResponse(respBytes).getHeaders(); 71 | } 72 | return burpHeaders; 73 | } 74 | 75 | public List exportToBurp() { 76 | List headers = new ArrayList(); 77 | // Add the first line. 78 | headers.add(first); 79 | // Add the rest of the headers. 80 | for (Map.Entry> h : hdr.entrySet()) { 81 | // If a header has multiple values, repeat the header. 82 | for (String val : h.getValue()) { 83 | headers.add(String.format("%s: %s", h.getKey(), val)); 84 | } 85 | } 86 | return headers; 87 | } 88 | 89 | // Static methods 90 | 91 | public static Map> getBurpHeaders(boolean isRequest, 92 | IHttpRequestResponse requestResponse) { 93 | 94 | Map> headers = new TreeMap>(String.CASE_INSENSITIVE_ORDER); 95 | 96 | // Get the headers from Burp. 97 | List burpHeaders = importFromBurp(isRequest, requestResponse); 98 | 99 | // If Burp does not have any headers then return null. 100 | if (burpHeaders == null) { 101 | return null; 102 | } 103 | 104 | // First line is "GET /whatever HTTP/1.1", we will skip it. 105 | for (String header : burpHeaders.subList(1, burpHeaders.size())) { 106 | // Split only once. 107 | String[] halves = header.split(":", 2); 108 | // Remove whitespace from both parts. 109 | halves[0] = halves[0].trim(); 110 | halves[1] = halves[1].trim(); 111 | 112 | // Check if the header already exists in the map. 113 | ArrayList vals; 114 | if (headers.get(halves[0]) == null) { 115 | // If not, create the ArrayList. 116 | vals = new ArrayList(); 117 | } else { 118 | // Get the existing ArrayList. 119 | vals = headers.get(halves[0]); 120 | } 121 | vals.add(halves[1]); 122 | headers.put(halves[0], vals); 123 | } 124 | return headers; 125 | } 126 | } -------------------------------------------------------------------------------- /src/utils/PausableExecutor.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import java.util.concurrent.LinkedBlockingQueue; 4 | import java.util.concurrent.ThreadPoolExecutor; 5 | import java.util.concurrent.TimeUnit; 6 | import java.util.concurrent.locks.Condition; 7 | import java.util.concurrent.locks.Lock; 8 | import java.util.concurrent.locks.ReentrantLock; 9 | import burp.Config; 10 | 11 | public class PausableExecutor extends ThreadPoolExecutor { 12 | private boolean isPaused = false; 13 | private Lock pauseLock = new ReentrantLock(); 14 | private Condition unpaused = pauseLock.newCondition(); 15 | 16 | public PausableExecutor(Config extensionConfig) { 17 | super( 18 | extensionConfig.numLintThreads, 19 | extensionConfig.numLintThreads, 20 | extensionConfig.lintTimeout, 21 | TimeUnit.SECONDS, 22 | new LinkedBlockingQueue<>() 23 | ); 24 | } 25 | @Override 26 | protected void beforeExecute(Thread t, Runnable r) { 27 | super.beforeExecute(t, r); 28 | pauseLock.lock(); 29 | try { 30 | while (isPaused) { 31 | unpaused.await(); 32 | } 33 | } catch (InterruptedException e) { 34 | t.interrupt(); 35 | } finally { 36 | pauseLock.unlock(); 37 | } 38 | } 39 | public void pause() { 40 | pauseLock.lock(); 41 | try { 42 | isPaused = true; 43 | } finally { 44 | pauseLock.unlock(); 45 | } 46 | } 47 | public void resume() { 48 | pauseLock.lock(); 49 | try { 50 | isPaused = false; 51 | unpaused.signal(); 52 | } finally { 53 | pauseLock.unlock(); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/utils/ReqResp.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import static burp.BurpExtender.helpers; 4 | 5 | import java.security.MessageDigest; 6 | import java.security.NoSuchAlgorithmException; 7 | import java.util.ArrayList; 8 | 9 | import org.apache.commons.io.FilenameUtils; 10 | 11 | import burp.Detective; 12 | import burp.IHttpRequestResponse; 13 | import burp.IRequestInfo; 14 | import burp.IResponseInfo; 15 | import lint.Metadata; 16 | 17 | 18 | /** 19 | * ReqResp 20 | */ 21 | public class ReqResp { 22 | 23 | public static byte[] getRequestBody(IHttpRequestResponse requestResponse) { 24 | byte[] requestBytes = requestResponse.getRequest(); 25 | IRequestInfo reqInfo = helpers.analyzeRequest(requestResponse); 26 | int bodyOffset = reqInfo.getBodyOffset(); 27 | int requestSize = requestBytes.length; 28 | int bodySize = requestSize - bodyOffset; 29 | byte[] bodyBytes = new byte[bodySize]; 30 | System.arraycopy(requestBytes, bodyOffset, bodyBytes, 0, bodySize); 31 | return bodyBytes; 32 | } 33 | 34 | public static byte[] getResponseBody(IHttpRequestResponse requestResponse) { 35 | byte[] responseBytes = requestResponse.getResponse(); 36 | IResponseInfo respInfo = helpers.analyzeResponse(responseBytes); 37 | int bodyOffset = respInfo.getBodyOffset(); 38 | int responseSize = responseBytes.length; 39 | int bodySize = responseSize - bodyOffset; 40 | byte[] bodyBytes = new byte[bodySize]; 41 | System.arraycopy(responseBytes, bodyOffset, bodyBytes, 0, bodySize); 42 | return bodyBytes; 43 | } 44 | 45 | public static byte[] getBody(boolean isRequest, 46 | IHttpRequestResponse requestResponse) { 47 | 48 | if (isRequest) { 49 | return getRequestBody(requestResponse); 50 | } 51 | return getResponseBody(requestResponse); 52 | } 53 | 54 | public static IHttpRequestResponse addHeader(boolean isRequest, 55 | IHttpRequestResponse requestResponse, String header, String value) { 56 | 57 | // Add a header to the request or response. 58 | Header hdr = new Header(isRequest, requestResponse); 59 | hdr.add(header, value); 60 | // Get the body. 61 | byte[] body = getBody(isRequest, requestResponse); 62 | // Build the HTTP message. 63 | byte[] modifiedMsg = helpers.buildHttpMessage(hdr.exportToBurp(), body); 64 | if (isRequest) { 65 | requestResponse.setRequest(modifiedMsg); 66 | } else { 67 | requestResponse.setResponse(modifiedMsg); 68 | } 69 | return requestResponse; 70 | } 71 | 72 | public static IHttpRequestResponse removeHeader(boolean isRequest, 73 | IHttpRequestResponse requestResponse, String headerName) { 74 | 75 | Header hdr = new Header(isRequest, requestResponse); 76 | hdr.remove(headerName); 77 | // Get the body. 78 | byte[] body = getBody(isRequest, requestResponse); 79 | // Build the HTTP message. 80 | byte[] modifiedMsg = helpers.buildHttpMessage(hdr.exportToBurp(), body); 81 | if (isRequest) { 82 | requestResponse.setRequest(modifiedMsg); 83 | } else { 84 | requestResponse.setResponse(modifiedMsg); 85 | } 86 | return requestResponse; 87 | } 88 | 89 | public static ArrayList getHeader(String header, boolean isRequest, 90 | IHttpRequestResponse requestResponse) { 91 | 92 | Header hdr = new Header(isRequest, requestResponse); 93 | return hdr.get(header); 94 | } 95 | 96 | // Returns the java.net.URL for the IHttpRequestResponse's request. 97 | public static java.net.URL getURL(IHttpRequestResponse requestResponse) { 98 | IRequestInfo reqInfo = helpers.analyzeRequest(requestResponse); 99 | return reqInfo.getUrl(); 100 | } 101 | 102 | // Creates and returns the metadat for the IHttpRequestResponse. 103 | public static Metadata getMetadata(IHttpRequestResponse requestResponse) throws NoSuchAlgorithmException { 104 | 105 | String url = ReqResp.getURL(requestResponse).toString(); 106 | // If there is no Referer header, getHeader will be null and we cannot 107 | // call .get(0) on it. 108 | ArrayList refererHeaders = ReqResp.getHeader("Referer", true, requestResponse); 109 | String referer = ""; 110 | if (refererHeaders != null) { 111 | referer = refererHeaders.get(0); 112 | } 113 | 114 | // If requestResponse only has a request we will get errors here. 115 | byte[] bodyBytes = new byte[0]; 116 | if (requestResponse.getResponse() != null) { 117 | bodyBytes = ReqResp.getResponseBody(requestResponse); 118 | } 119 | 120 | byte[] hashBytes = MessageDigest.getInstance("SHA-1").digest(bodyBytes); 121 | String hashString = StringUtils.encodeHexString(hashBytes); 122 | return new Metadata(url, referer, hashString); 123 | } 124 | 125 | public static String getRequestExtension(IHttpRequestResponse requestResponse) { 126 | // Get the request URL. 127 | // URL requestURL = getRequestURL(requestResponse); 128 | return FilenameUtils.getExtension(Detective.getRequestURL(requestResponse).getPath()); 129 | 130 | /** 131 | * URL u = new 132 | * URL("https://example.net/path/to/whatever.js?param1=val1¶m2=val2"); 133 | * System.out.printf("getFile(): %s\n", u.getFile()); 134 | * System.out.printf("getPath(): %s\n", u.getPath()); 135 | * 136 | * URL.getPath() returns the path including the initial /. getPath(): 137 | * /path/to/whatever.js URL.getFile() return getPath along with GET query 138 | * string. getFile(): /path/to/whatever.js?param1=val1¶m2=val2 139 | */ 140 | } 141 | 142 | public static String getHost(IHttpRequestResponse requestResponse) { 143 | return getURL(requestResponse).getHost(); 144 | } 145 | 146 | } -------------------------------------------------------------------------------- /src/utils/Resources.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | 6 | import org.apache.commons.io.IOUtils; 7 | 8 | 9 | /** 10 | * Resources 11 | */ 12 | public class Resources { 13 | 14 | // Reads a resource and returns it as a string. 15 | // Remember to designate files with a /. 16 | // E.g., to get "resources/whatever.txt", call getResourceFile("/whatever.txt"). 17 | // E.g., "resources/path/whatever.txt" -> getResourceFile("/path/whatever.txt"). 18 | public static String getResourceFile(Class cls, String name) throws IOException { 19 | InputStream in = cls.getResourceAsStream(name); 20 | String content = IOUtils.toString(in, StringUtils.UTF8); 21 | in.close(); 22 | return content; 23 | } 24 | } -------------------------------------------------------------------------------- /src/utils/StringUtils.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import static burp.BurpExtender.callbacks; 4 | import java.io.PrintWriter; 5 | import java.io.StringWriter; 6 | import java.net.MalformedURLException; 7 | import java.net.URL; 8 | import java.security.MessageDigest; 9 | import java.security.NoSuchAlgorithmException; 10 | import java.util.Base64; 11 | import org.apache.commons.io.FilenameUtils; 12 | 13 | /** 14 | * StringUtils 15 | */ 16 | public class StringUtils { 17 | 18 | public static String UTF8 = "UTF-8"; 19 | 20 | // Print to extension output. 21 | public static void print(String data) { 22 | callbacks.printOutput(data); 23 | } 24 | 25 | // Print with format string. 26 | public static void printFormat(String format, Object... args) { 27 | print(String.format(format, args)); 28 | } 29 | 30 | // Print to extension error. 31 | public static void error(String data) { 32 | callbacks.printError(data); 33 | } 34 | 35 | // Print errors with format string. 36 | public static void errorFormat(String format, Object... args) { 37 | error(String.format(format, args)); 38 | } 39 | 40 | public static String bytesToString(byte[] data) { 41 | return new String(data); 42 | } 43 | 44 | public static byte[] stringToBytes(String data) { 45 | return data.getBytes(); 46 | } 47 | 48 | public static String getStackTrace(Exception e) { 49 | StringWriter sw = new StringWriter(); 50 | e.printStackTrace(new PrintWriter(sw)); 51 | return sw.toString(); 52 | } 53 | 54 | public static void printStackTrace(Exception e) { 55 | error(getStackTrace(e)); 56 | } 57 | 58 | // Returns the filename of a string URL without the extension. 59 | // https://stackoverflow.com/a/17167743 60 | public static String getURLBaseName(String url) throws MalformedURLException { 61 | return FilenameUtils.getBaseName(new URL(url).getPath()); 62 | } 63 | 64 | // Base64 encode and decode methods that return a String instead of byte[]. 65 | public static String base64Encode(String plaintext) { 66 | return Base64.getEncoder().encodeToString(stringToBytes(plaintext)); 67 | } 68 | 69 | public static String base64Decode(String encoded) { 70 | byte[] decodedBytes = Base64.getDecoder().decode(encoded); 71 | return bytesToString(decodedBytes); 72 | } 73 | 74 | // Returns true if item is in arr. Does case-insensitive comparison. 75 | /** 76 | * For case-sensitive contains do: List lst = 77 | * java.util.Arrays.asList(arr); return lst.contains(item); 78 | */ 79 | public static boolean arrayContains(String item, String[] arr) { 80 | for (String arrayItem : arr) { 81 | if (item.equalsIgnoreCase(arrayItem)) 82 | return true; 83 | } 84 | return false; 85 | } 86 | 87 | // Returns the parent directory of a full path. 88 | public static String getParentDirectory(String fullpath) { 89 | return FilenameUtils.getFullPath(fullpath); 90 | // File f = new File(fullpath); 91 | // return f.getParent(); 92 | } 93 | 94 | // TODO Remove this if not needed. 95 | // Returns the SHA-1 hash of a String as a String. 96 | public static String sha1(String data) throws NoSuchAlgorithmException { 97 | 98 | byte[] hashBytes = MessageDigest.getInstance("SHA-1").digest(data.getBytes()); 99 | return StringUtils.encodeHexString(hashBytes); 100 | 101 | } 102 | 103 | // Returns the opposite of isEmpty. 104 | public static boolean isNotEmpty(final CharSequence cs) { 105 | return !isEmpty(cs); 106 | } 107 | 108 | /** 109 | * isEmpty was copied from Apache commons-lang.StringUtils. 110 | * It's used in the capilize methods. 111 | * https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/StringUtils.java 112 | * 113 | * See the license below: 114 | * Licensed to the Apache Software Foundation (ASF) under one or more 115 | * contributor license agreements. See the NOTICE file distributed with 116 | * this work for additional information regarding copyright ownership. 117 | * The ASF licenses this file to You under the Apache License, Version 2.0 118 | * (the "License"); you may not use this file except in compliance with 119 | * the License. You may obtain a copy of the License at 120 | * 121 | * http://www.apache.org/licenses/LICENSE-2.0 122 | * 123 | * Unless required by applicable law or agreed to in writing, software 124 | * distributed under the License is distributed on an "AS IS" BASIS, 125 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 126 | * See the License for the specific language governing permissions and 127 | * limitations under the License. 128 | */ 129 | 130 | /** 131 | *

Checks if a CharSequence is empty ("") or null.

132 | * 133 | *
134 |      * StringUtils.isEmpty(null)      = true
135 |      * StringUtils.isEmpty("")        = true
136 |      * StringUtils.isEmpty(" ")       = false
137 |      * StringUtils.isEmpty("bob")     = false
138 |      * StringUtils.isEmpty("  bob  ") = false
139 |      * 
140 | * 141 | *

NOTE: This method changed in Lang version 2.0. 142 | * It no longer trims the CharSequence. 143 | * That functionality is available in isBlank().

144 | * 145 | * @param cs the CharSequence to check, may be null 146 | * @return {@code true} if the CharSequence is empty or null 147 | * @since 3.0 Changed signature from isEmpty(String) to isEmpty(CharSequence) 148 | */ 149 | public static boolean isEmpty(final CharSequence cs) { 150 | return cs == null || cs.length() == 0; 151 | } 152 | 153 | /** 154 | * encodeHex and encodeHexString were copied from the Apache commons-codec 155 | * library. 156 | * https://github.com/apache/commons-codec/blob/master/src/main/java/org/apache/commons/codec/binary/Hex.java 157 | * 158 | */ 159 | /** 160 | * Used to build output as Hex 161 | */ 162 | private static final char[] DIGITS_LOWER = { 163 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 164 | 'e', 'f' 165 | }; 166 | 167 | /** 168 | * Used to build output as Hex 169 | */ 170 | private static final char[] DIGITS_UPPER = { 171 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 172 | 'E', 'F' 173 | }; 174 | 175 | /** 176 | * Converts an array of bytes into a String representing the hexadecimal 177 | * values of each byte in order. The returned String will be double the 178 | * length of the passed array, as it takes two characters to represent any 179 | * given byte. 180 | * 181 | * @param data a byte[] to convert to Hex characters 182 | * @return A String containing lower-case hexadecimal characters 183 | * @since 1.4 184 | */ 185 | public static String encodeHexString(final byte[] data) { 186 | return new String(encodeHex(data)); 187 | } 188 | 189 | /** 190 | * Converts an array of bytes into a String representing the hexadecimal 191 | * values of each byte in order. The returned String will be double the 192 | * length of the passed array, as it takes two characters to represent any 193 | * given byte. 194 | * 195 | * @param data a byte[] to convert to Hex characters 196 | * @param toLowerCase {@code true} converts to lowercase, {@code false} to 197 | * uppercase 198 | * @return A String containing lower-case hexadecimal characters 199 | * @since 1.11 200 | */ 201 | public static String encodeHexString(final byte[] data, final boolean toLowerCase) { 202 | return new String(encodeHex(data, toLowerCase)); 203 | } 204 | 205 | /** 206 | * Converts an array of bytes into an array of characters representing the 207 | * hexadecimal values of each byte in order. The returned array will be 208 | * double the length of the passed array, as it takes two characters to 209 | * represent any given byte. 210 | * 211 | * @param data a byte[] to convert to Hex characters 212 | * @return A char[] containing lower-case hexadecimal characters 213 | */ 214 | public static char[] encodeHex(final byte[] data) { 215 | return encodeHex(data, true); 216 | } 217 | 218 | /** 219 | * Converts an array of bytes into an array of characters representing the 220 | * hexadecimal values of each byte in order. The returned array will be 221 | * double the length of the passed array, as it takes two characters to 222 | * represent any given byte. 223 | * 224 | * @param data a byte[] to convert to Hex characters 225 | * @param toLowerCase {@code true} converts to lowercase, {@code false} to 226 | * uppercase 227 | * @return A char[] containing hexadecimal characters in the selected case 228 | * @since 1.4 229 | */ 230 | public static char[] encodeHex(final byte[] data, final boolean toLowerCase) { 231 | return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER); 232 | } 233 | 234 | /** 235 | * Converts an array of bytes into an array of characters representing the 236 | * hexadecimal values of each byte in order. The returned array will be 237 | * double the length of the passed array, as it takes two characters to 238 | * represent any given byte. 239 | * 240 | * @param data a byte[] to convert to Hex characters 241 | * @param toDigits the output alphabet (must contain at least 16 chars) 242 | * @return A char[] containing the appropriate characters from the alphabet 243 | * For best results, this should be either upper- or lower-case hex. 244 | * @since 1.4 245 | */ 246 | private static char[] encodeHex(final byte[] data, final char[] toDigits) { 247 | final int l = data.length; 248 | final char[] out = new char[l << 1]; 249 | // two characters form the hex value. 250 | for (int i = 0, j = 0; i < l; i++) { 251 | out[j++] = toDigits[(0xF0 & data[i]) >>> 4]; 252 | out[j++] = toDigits[0x0F & data[i]]; 253 | } 254 | return out; 255 | } 256 | } --------------------------------------------------------------------------------