├── .github └── workflows │ ├── contrast-scan.yml │ ├── contrast_security_app.yaml │ └── maven-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── openolly │ │ ├── Expression.java │ │ ├── Instrumenter.java │ │ ├── JOTAgent.java │ │ ├── Matcher.java │ │ ├── MethodIndexValue.java │ │ ├── MethodLocator.java │ │ ├── SafeString.java │ │ ├── Sensor.java │ │ ├── SensorIndexValue.java │ │ ├── advice │ │ ├── CEAdvice.java │ │ ├── CNEAdvice.java │ │ ├── MRAdvice.java │ │ ├── MVEAdvice.java │ │ ├── MVNEAdvice.java │ │ ├── ScopeAdvice.java │ │ ├── SensorException.java │ │ ├── TraceAdvice.java │ │ └── scope │ │ │ └── BinaryScope.java │ │ ├── config │ │ ├── ConfigMonitor.java │ │ ├── ConfigReader.java │ │ └── GraphSerializer.java │ │ └── reporting │ │ ├── Event.java │ │ ├── Report.java │ │ ├── Reporter.java │ │ ├── RouteGraph.java │ │ └── Trace.java └── resources │ ├── access.jot │ ├── ciphers.jot │ ├── cmdi.jot │ ├── core.jot │ ├── eli.jot │ ├── jee.jot │ ├── reports.jot │ ├── sqli.jot │ └── ticketbook.jot └── test ├── java └── org │ └── openolly │ ├── AllTests.java │ ├── ConfigReaderTest.java │ ├── ExpressionTest.java │ ├── GraphSerializerTest.java │ ├── SafeStringTest.java │ ├── ScopeTest.java │ ├── SensorTest.java │ ├── TableTest.java │ └── TraceTest.java └── resources ├── junit.jot ├── rules ├── core.jot ├── jee.jot └── reports.jot └── testrule.jot /.github/workflows/contrast-scan.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow will initiate a Contrast Scan on your built artifact, and subsequently upload the results SARIF to Github. 7 | # Because Contrast Scan is designed to run against your deployable artifact, you need to build an artifact that will be passed to the Contrast Scan Action. 8 | # Contrast Scan currently supports Java, JavaScript and .NET artifacts. 9 | # For more information about the Contrast Scan GitHub Action see here: https://github.com/Contrast-Security-OSS/contrastscan-action 10 | 11 | # Pre-requisites: 12 | # All Contrast related account secrets should be configured as GitHub secrets to be passed as inputs to the Contrast Scan Action. 13 | # The required secrets are CONTRAST_API_KEY, CONTRAST_ORGANIZATION_ID and CONTRAST_AUTH_HEADER. 14 | 15 | on: 16 | push: 17 | branches: [ "master" ] 18 | pull_request: 19 | # The branches below must be a subset of the branches above 20 | branches: [ "master" ] 21 | schedule: 22 | - cron: '31 15 * * 3' 23 | 24 | permissions: 25 | contents: read 26 | 27 | name: Scan analyze workflow 28 | jobs: 29 | build-and-scan: 30 | permissions: 31 | contents: read # for actions/checkout 32 | security-events: write # for github/codeql-action/upload-sarif 33 | runs-on: ubuntu-latest 34 | # check out project 35 | steps: 36 | - uses: actions/checkout@v3 37 | 38 | # build project 39 | - name: Set up JDK 11 40 | uses: actions/setup-java@v3 41 | with: 42 | java-version: '11' 43 | distribution: 'adopt' 44 | - name: Build with Maven 45 | run: mvn --batch-mode --update-snapshots package -DskipTests 46 | 47 | # Scan Artifact 48 | - name: Contrast Scan Analyze 49 | uses: Contrast-Security-OSS/contrastscan-action@v2 50 | with: 51 | artifact: target/jot-0.9.2.jar # replace this path with the path to your built artifact 52 | apiKey: ${{ secrets.CONTRAST_API_KEY }} 53 | orgId: ${{ secrets.CONTRAST_ORGANIZATION_ID }} 54 | authHeader: ${{ secrets.CONTRAST_AUTH_HEADER }} 55 | #Upload the results to GitHub 56 | - name: Upload SARIF file 57 | uses: github/codeql-action/upload-sarif@v2 58 | with: 59 | sarif_file: results.sarif # The file name must be 'results.sarif', as this is what the Github Action will output 60 | -------------------------------------------------------------------------------- /.github/workflows/contrast_security_app.yaml: -------------------------------------------------------------------------------- 1 | # DISCLAIMER: This workflow file has been auto-generated and committed to the repo by the GitHub App from Contrast Security. 2 | # Manual edits to this file could cause the integration to produce unexpected behavior or break. 3 | # Version: 1.0.0 4 | # Last updated: 2023-06-29T22:48:21.118832366Z 5 | name: Contrast Security App Workflow 6 | on: 7 | workflow_dispatch: 8 | push: 9 | branches: 10 | - master 11 | pull_request: 12 | types: [opened, synchronize, reopened] 13 | branches: 14 | - master 15 | jobs: 16 | fingerprint_repo: 17 | if: ${{ github.actor != 'dependabot[bot]' }} 18 | runs-on: ubuntu-22.04 19 | steps: 20 | - name: Clone repository 21 | uses: actions/checkout@v3 22 | - name: Run Contrast SCA Fingerprint 23 | id: fingerprint 24 | uses: 'Contrast-Security-OSS/contrast-sca-action@v2' 25 | with: 26 | apiKey: ${{ secrets.CONTRAST_GITHUB_APP_API_KEY }} 27 | authHeader: ${{ secrets.CONTRAST_GITHUB_APP_AUTH_HEADER }} 28 | orgId: ${{ vars.CONTRAST_GITHUB_APP_ORG_ID }} 29 | apiUrl: ${{ vars.CONTRAST_GITHUB_APP_TS_URL }} 30 | repoUrl: ${{ github.server_url }}/${{ github.repository }} 31 | repoName: ${{ github.repository }} 32 | externalId: ${{ vars.CONTRAST_GITHUB_APP_ID }} 33 | command: fingerprint 34 | outputs: 35 | fingerprint: ${{ steps.fingerprint.outputs.fingerprint }} 36 | analyze_dependencies: 37 | if: ${{ needs.fingerprint_repo.outputs.fingerprint != '' }} 38 | needs: fingerprint_repo 39 | runs-on: ubuntu-22.04 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | manifest: 44 | - ${{ fromJson(needs.fingerprint_repo.outputs.fingerprint) }} 45 | steps: 46 | - name: Clone repository 47 | uses: actions/checkout@v3 48 | - name: Run Contrast SCA Audit 49 | uses: 'Contrast-Security-OSS/contrast-sca-action@v2' 50 | with: 51 | apiKey: ${{ secrets.CONTRAST_GITHUB_APP_API_KEY }} 52 | authHeader: ${{ secrets.CONTRAST_GITHUB_APP_AUTH_HEADER }} 53 | orgId: ${{ vars.CONTRAST_GITHUB_APP_ORG_ID }} 54 | apiUrl: ${{ vars.CONTRAST_GITHUB_APP_TS_URL }} 55 | filePath: ${{ matrix.manifest.filePath }} 56 | repositoryId: ${{ matrix.manifest.repositoryId }} 57 | projectGroupId: ${{ matrix.manifest.projectGroupId }} 58 | -------------------------------------------------------------------------------- /.github/workflows/maven-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a package using Maven and then publish it to GitHub packages when a release is created 2 | # For more information see: https://github.com/actions/setup-java#apache-maven-with-a-settings-path 3 | 4 | name: Maven Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up JDK 1.8 18 | uses: actions/setup-java@v1 19 | with: 20 | java-version: 1.8 21 | server-id: github # Value of the distributionManagement/repository/id field of the pom.xml 22 | settings-path: ${{ github.workspace }} # location for the settings.xml file 23 | 24 | - name: Build with Maven 25 | run: mvn -B package --file pom.xml 26 | 27 | - name: Publish to GitHub Packages Apache Maven 28 | run: mvn deploy -s $GITHUB_WORKSPACE/settings.xml 29 | env: 30 | GITHUB_TOKEN: ${{ github.token }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project specific 2 | target 3 | graphviz 4 | dependency-reduced-pom.xml 5 | .classpath 6 | .project 7 | .DS_Store 8 | .vscode 9 | 10 | # Compiled class file 11 | *.class 12 | 13 | # Log file 14 | *.log 15 | 16 | # BlueJ files 17 | *.ctxt 18 | 19 | # Mobile Tools for Java (J2ME) 20 | .mtj.tmp/ 21 | 22 | # Package Files # 23 | *.jar 24 | *.war 25 | *.nar 26 | *.ear 27 | *.zip 28 | *.tar.gz 29 | *.rar 30 | 31 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 32 | hs_err_pid* 33 | .settings/org.eclipse.core.resources.prefs 34 | *.prefs 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java Observability Toolkit (JOT) 2 | 3 | The Java Observability Toolkit (JOT) is a platform for making any Java application observable 4 | without writing any code or even recompiling. The platform consists of... 5 | 6 | - a library that you can add to your applications 7 | - a set of JOTs that you can enable to make different aspects of your application observable. 8 | 9 | You can create JOTs quickly and easily without any coding. JOTs can make 10 | just about anything in a running Java application observable. Here are 11 | a few examples of what you might use it for... but the only limit is your 12 | creativity. 13 | 14 | - You're stuck troubleshooting a production issue and you don't have enough 15 | logs or the ability to debug. With a simple JOT rule you can get the data 16 | you need quickly and easily. 17 | 18 | - You want to monitor something that's only accessible deep inside an app 19 | and you don't have a way to expose it to your tools. 20 | 21 | - You want to prevent abuse of powerful methods by attackers, but they're in libraries 22 | that you don't control. 23 | 24 | JOTs goal is to provide maximum instrumentation power without compromising simplicity. 25 | 26 | Let us know the cool things you do with JOT! And consider contributing them back 27 | to the project so that others can share in your awesomeness. The easiest way to contribute 28 | is to create a pull request. If you're sharing a JOT, add it to the projects' "contrib" 29 | directory... and please include a comment detailing what it's intended to be used for. 30 | 31 | 32 | ## A Simple JOT 33 | 34 | Define your sensors and reports in yaml and save it in a .jot file. Sensors define what data 35 | to capture and "reports" tell JOT how to track the data over time. 36 | 37 | sensors: 38 | - name: "get-ciphers" 39 | description: "Identifies encryption ciphers" 40 | methods: 41 | - "javax.crypto.Cipher.getInstance" 42 | captures: 43 | - "#P0" 44 | 45 | reports: 46 | - name: "Encryption Usage" 47 | type: "list" 48 | cols: "get-ciphers" 49 | 50 | ## Getting Started with JOT 51 | 52 | Basically all you have to do is add JOT to the JVM 53 | - Download the latest jot.jar from https://github.com/planetlevel/jot/tags 54 | - Grab the ciphers.jot file from https://github.com/planetlevel/jot/blob/master/src/main/resources/ciphers.jot 55 | - Launch your app with "java -javaagent:jot.jar=ciphers.jot" 56 | 57 | Then you just use your application normally and let JOT gather data for you. 58 | JOT will make a nice table capturing exactly where encryption is used and 59 | what algorithm is specified. 60 | 61 | Encryption Algorithms get-ciphers 62 | ------------------------------------------------------------ ---------------------- 63 | com.acme.ticketbook.Ticket.encrypt(Ticket.java:125) DES 64 | org.apache.jsp.accessA_jsp._jspService(accessA_jsp.java:212) AES 65 | org.apache.jsp.accessA_jsp._jspService(accessA_jsp.java:213) PBEWithMD5AndTripleDES 66 | org.apache.jsp.accessB_jsp._jspService(accessB_jsp.java:212) DES 67 | org.apache.jsp.accessC_jsp._jspService(accessC_jsp.java:212) DES/CBC/PKCS5Padding 68 | 69 | ## Advanced Options 70 | You might find using the JAVA_TOOL_OPTIONS environment variable useful. 71 | Like "export JAVA_TOOL_OPTIONS="-javaagent:jot.jar=ciphers.jot". Then 72 | no matter how Java eventually gets launched, it will use JOT. 73 | 74 | If you want to use multiple JOTs at the same time, you can either put them all into one 75 | big .jot file, or you can put multiple .jot files in a directory and use 76 | "-javaagent:jot.jar=directory" 77 | 78 | Lastly, every JOT can have multiple "captures." A capture is a way to specify 79 | what data you want to collect from the methods you specified in the JOT. The simple 80 | captures are things like: 81 | #P0 - the first parameter 82 | #P1 - the second parameter 83 | #ARGS - all the parameters concatenated in a String 84 | #OBJ - the object the method is being called on 85 | #RET - the return from the method 86 | 87 | Captures are actually Spring Expression Language (SpEL) expressions, so you can call methods 88 | on those basic objects, compare stuff, and do operations. This helps 89 | you observe exactly what you want. See below for all the details of writing your own sensors. 90 | 91 | ## Logging with JOT 92 | 93 | JOT uses FluentLogger which takes configuration from `java.util.logging` and can be configured by the JVM with `-Djava.util.logging.config.file` 94 | 95 | This sample config will log JOT to stdout as well as to /tmp/jot.log 96 | 97 | handlers = java.util.logging.ConsoleHandler, java.util.logging.FileHandler 98 | 99 | java.util.logging.ConsoleHandler.level = ALL 100 | java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter 101 | java.util.logging.SimpleFormatter.format = %1$tF %1$tT %4$-7s [%2$s] - %5$s %n 102 | 103 | java.util.logging.FileHandler.level=ALL 104 | java.util.logging.FileHandler.pattern=/tmp/jot.log 105 | java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter 106 | java.util.logging.FileHandler.append=true 107 | 108 | using: 109 | 110 | java -javaagent:jot.jar=ciphers.jot -Djava.util.logging.config.file=/root/jul.properties 111 | 112 | 113 | ## Creating Your Own JOT Sensors 114 | 115 | Sensors are how you define the data you want to gather. You can also use 116 | sensors to perform limited actions within an application. 117 | 118 | # The name of this sensor, which can be referenced by reports 119 | - name: "get-ciphers" 120 | 121 | # Use this to describe what this sensor does. Try to provide enough 122 | # detail so that anyone would understand what it's about. 123 | description: "What does sensor do?" 124 | 125 | # A list of methods to gather data from. To avoid potential performance issues, 126 | # avoid putting sensors in methods that are extremely frequently used, 127 | # like StringBuffer.append() or File.. 128 | methods: 129 | - "a.b.c.Class.method" 130 | - "d.e.f.Class.method" 131 | 132 | # Scopes allow you to limit when a sensor will fire. 133 | # A sensor will fire if it is invoked while "inside" one of the scopes. That is, 134 | # when any of these methods is on the stack. 135 | # You can define negative scopes using !scope, so that this sensor will only fire outside the scope 136 | # For static methods you need to prefix the method with "static" 137 | scopes: 138 | - "a.b.c.Class.method" # matches only if inside method 139 | - "!a.b.c.Class.method" # matches only if NOT inside method 140 | - "static a.b.c.Class.method" # matches if inside static method 141 | 142 | # Excludes allow you to prevent data from being gathered from any 143 | # classes that starts with any of these patterns or are "isAssignableFrom" these classes 144 | # FIXME: currently you must put .all after each classname 145 | excludes: 146 | - "javax.el.MapELResolver.all" 147 | - "org.foo.package.all" 148 | 149 | # Captures are the workhorse of JOT and are written in Spring Expression Language (SpEL) 150 | # You may reference data from the method currently running. 151 | # Options are OBJ, P1..P10, ARGS, RET, STACK. These objects are whatever type they happen 152 | # to be, and you can invoke any existing methods on those objects. Note that STACK is a StackFrame[] 153 | # See https://blog.abelotech.com/posts/useful-things-spring-expression-language-spel/ 154 | captures: 155 | - "#RET?.toUpperCase()" # call toUpperCase if #RET is not null (note ?. operator) 156 | - "\"\"+#P0+\":\"+#RET" # for methods that take a name and return a value 157 | - "#OBJ.getCanonicalPath() + \" \" + #OBJ.exists() ? [EXISTS] : [DOES NOT EXIST]" # use ternary operator 158 | - "\"\"+#P0+\":\"+(#RET ? \"Y\" : \"N\")" # for methods that return boolean 159 | 160 | # Matchers allow you to filter the data that was captured with a set of regular expressions. 161 | # If there are no matchers then all captures will report data. 162 | # Positive matchers only fire if the data matches the pattern. You'll get a result if any positive matchers match. 163 | # Negative matchers (starting with !) only fire if the data does not match the pattern. You'll get a result if no negative matchers match. 164 | # If you mix positive and negative, you'll get a result if any positive captures match and no negative matchers match. 165 | matchers: 166 | - "^\\w*$" # matches anything with word characters start to finish 167 | - "!^\\[foo" # matches anything that doesn't start with foo 168 | - "!null" # hide empty results from output 169 | 170 | # Exceptions are a way to change the behavior of an application. 171 | # If the sensor fires (capture occurs and matchers fire) then JOT will 172 | # throw a SensorException. 173 | # The message should be appropriate for end user, JOT will log all the relevant details. 174 | # Note that generally you don't want to use #RET in your capture if you're throwing an exception, 175 | # because it will throw at the end of the method, which is probably too late. 176 | exception: "Message" 177 | 178 | # Debug mode will generate extra logging for this rule only 179 | debug: "false" 180 | 181 | 182 | 183 | ## Creating Your Own JOT Reports 184 | 185 | Reports let you define how JOT will collect data over time and 186 | how it will format it for you. 187 | 188 | # Title of this report that will be displayed above the results 189 | - name: "example" 190 | 191 | # Type of reports include 192 | # 1. list 193 | # ROWS: caller method 194 | # COLS: one column named after the sensor defined in "cols" 195 | # DATA: values from the sensor named in "cols" 196 | # 2. compare 197 | # ROWS: 198 | # COLS: uses the "cols" sensor values as column headers 199 | # DATA: values from the sensor named in "cols" 200 | # 3. table 201 | # ROWS: 202 | # COLS: uses rule names for cols[0-n] as column headers - parses data values 203 | # DATA: values from the sensor named in "cols" 204 | # 4. series - table but each row starts with a timestamp (currently includes callers col too) 205 | # ROWS: 206 | # COLS: uses rule names for cols[0-n] as column headers - parses data values 207 | # DATA: values from the sensor named in "cols" 208 | type: "table" 209 | 210 | # Rows indicates a sensor to be used to populate row headers 211 | rows: "get-routes" 212 | 213 | # Cols indicates a sensor (or list of sensors) to be used to populate column headers 214 | cols: "get-users" 215 | 216 | # Data indicates how the content of data cells should be populated. Can be a fixed string or a rule name. 217 | data: "X" 218 | 219 | 220 | 221 | ## Building from Source 222 | 223 | Should be as simple as cloning this repo and building with maven 224 | 225 | $ git clone https://github.com/planetlevel/jot.git 226 | $ cd jot 227 | $ mvn install 228 | 229 | Then you can use the jot-x.x.jar in the target directory. 230 | 231 | Contributions are welcome. See the bugtracker to find issues to work on if you want to make JOT better. 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | ## TO DO LIST 240 | 241 | - Solve problem of sensors inside JOT scope 242 | 1) don't instrument JOT classes -- anything using shading 243 | 2) use global scope to check anywhere you're inside a sensor call 244 | 245 | - Create separate JOT log file instead of using java.util.logging 246 | 247 | - New rules 248 | 1) which routes are non-idempotent? 249 | 250 | - Sensors 251 | # future features - maybe think about reporting? 252 | # enabled: "false" 253 | # sample: "1000" # report every 1000 times? time frequency? 254 | # counter: "?" # report 10 mins? 255 | # scope: include a capture and regex to say whether to start scope (if service.P0.getParameter=foobar) 256 | # exec: run this code. before? after? during? 257 | 258 | - Reports 259 | # possible additions to cols -- caller, stack[n], trace# 260 | 261 | - Query Language 262 | # investigate using query language instead of yaml jots. 263 | # Perhaps two types of "queries" -- one to create sensors, another to pull reports. 264 | 265 | # SELECT #P0.toUpperCase() 266 | FROM java.lang.Runtime.getRuntime.exec 267 | WITHIN javax.servlet.Servlet.service 268 | WHERE pattern 269 | 270 | # SELECT [capture, capture] 271 | FROM [method, method] 272 | EXCLUDE [class, class] 273 | WITHIN [scope, scope] 274 | WHERE [pattern, pattern] 275 | THROWS [msg] 276 | 277 | # SELECT #P0 FROM javax.crypto.Cipher.getInstance 278 | 279 | # SELECT #P0 FROM javax.crypto.Cipher.getInstance 280 | WHERE "DES|DESEDE" 281 | THROWS "Weak encryption algorithm detected" 282 | 283 | 284 | 285 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | com.contrastsecurity 7 | jot 8 | 0.9.2 9 | 10 | jot 11 | 12 | 13 | 1.8 14 | UTF-8 15 | UTF-8 16 | 1.8 17 | ${maven.compiler.source} 18 | 5.5.2 19 | 1.5.2 20 | 21 | 22 | 23 | 24 | org.apache.commons 25 | commons-lang3 26 | 3.11 27 | 28 | 29 | org.apache.commons 30 | commons-text 31 | 1.10.0 32 | 33 | 34 | commons-io 35 | commons-io 36 | 2.8.0 37 | 38 | 39 | com.google.guava 40 | guava 41 | 30.1-jre 42 | 43 | 44 | net.bytebuddy 45 | byte-buddy 46 | 1.10.18 47 | 48 | 49 | jakarta.xml.bind 50 | jakarta.xml.bind-api 51 | 3.0.0 52 | 53 | 54 | com.fasterxml.jackson.core 55 | jackson-core 56 | 2.13.4 57 | 58 | 59 | com.fasterxml.jackson.core 60 | jackson-databind 61 | 2.13.4.1 62 | 63 | 64 | com.fasterxml.jackson.dataformat 65 | jackson-dataformat-yaml 66 | 2.13.4 67 | 68 | 69 | com.google.code.gson 70 | gson 71 | 2.9.1 72 | 73 | 74 | org.springframework 75 | spring-expression 76 | 5.3.22 77 | 78 | 79 | org.junit.jupiter 80 | junit-jupiter-engine 81 | ${junit.jupiter.version} 82 | test 83 | 84 | 85 | org.junit.platform 86 | junit-platform-runner 87 | ${junit.platform.version} 88 | test 89 | 90 | 91 | com.google.flogger 92 | flogger 93 | 0.7.4 94 | 95 | 96 | com.google.flogger 97 | flogger-system-backend 98 | 0.7.4 99 | runtime 100 | 101 | 102 | 103 | 104 | 105 | 106 | org.apache.maven.plugins 107 | maven-surefire-plugin 108 | 3.0.0-M5 109 | 110 | 114 | 115 | 116 | 117 | 118 | org.apache.maven.plugins 119 | maven-compiler-plugin 120 | 3.8.1 121 | 122 | 1.8 123 | 1.8 124 | 125 | 126 | 127 | 128 | org.apache.maven.plugins 129 | maven-shade-plugin 130 | 3.2.4 131 | 132 | 133 | package 134 | 135 | shade 136 | 137 | 138 | 139 | 140 | org 141 | org.shaded 142 | 143 | org.openolly.**.* 144 | 145 | 146 | 147 | net 148 | net.shaded 149 | 150 | 151 | com 152 | com.shaded 153 | 154 | com.fasterxml.**.* 155 | 156 | 157 | 158 | 159 | 160 | *:* 161 | 162 | META-INF/license/** 163 | META-INF/* 164 | META-INF/maven/** 165 | LICENSE 166 | NOTICE 167 | /*.txt 168 | build.properties 169 | 170 | 171 | 172 | 173 | 174 | 176 | 177 | org.openolly.JOTAgent 178 | org.openolly.JOTAgent 179 | true 180 | true 181 | false 182 | jot-${project.version}.jar 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/Expression.java: -------------------------------------------------------------------------------- 1 | package org.openolly; 2 | 3 | import java.util.Collections; 4 | import java.util.Enumeration; 5 | import java.util.List; 6 | 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.springframework.expression.ExpressionParser; 9 | import org.springframework.expression.ParseException; 10 | import org.springframework.expression.spel.standard.SpelExpressionParser; 11 | import org.springframework.expression.spel.support.StandardEvaluationContext; 12 | 13 | import com.google.common.flogger.FluentLogger; 14 | 15 | public class Expression { 16 | 17 | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 18 | 19 | private static ExpressionParser parser = new SpelExpressionParser(); 20 | private org.springframework.expression.Expression exp = null; 21 | 22 | private StandardEvaluationContext context = new StandardEvaluationContext(); 23 | 24 | public Expression(String expression) throws ParseException, NoSuchMethodException, SecurityException { 25 | exp = parser.parseExpression(expression); 26 | context.registerFunction("toUpper", StringUtils.class.getMethod("upperCase", String.class)); 27 | context.registerFunction("toLower", StringUtils.class.getMethod("upperCase", String.class)); 28 | context.registerFunction("sort", Collections.class.getMethod("sort", List.class)); 29 | } 30 | 31 | public Object execute(Object obj, Object[] params, Object ret, String caller, StackTraceElement[] stack) { 32 | 33 | // convert input if necessary 34 | obj = convert( obj ); 35 | params = convert( params ); 36 | ret = convert( ret ); 37 | 38 | context.setVariable("OBJ", obj); 39 | context.setVariable("ARGS", params); 40 | context.setVariable("RET", ret); 41 | if (params != null) { 42 | for (int i = 0; i < params.length; i++) { 43 | context.setVariable("P" + i, params[i]); 44 | } 45 | } 46 | context.setVariable("CALLER", caller); 47 | context.setVariable("STACK", stack); 48 | 49 | // convert output if necessary 50 | Object result = null; 51 | try { 52 | result = convert( exp.getValue(context) ); 53 | } catch( Exception e ) { 54 | logger.atWarning().log( "[JOT] Couldn't evaluate %s: %s", this, e.getMessage() ); 55 | result = null; 56 | } 57 | return result; 58 | } 59 | 60 | public Object[] convert( Object[] o ) { 61 | if ( o != null ) { 62 | for ( int i = 0; i < o.length; i++ ) { 63 | o[i] = convert( o[i] ); 64 | } 65 | } 66 | return o; 67 | } 68 | 69 | // FIXME: this is where any conversions on expression output should happen 70 | public Object convert(Object o) { 71 | if ( o != null ) { 72 | // note: this burns the enumeration, use only if no side-effect 73 | if ( Enumeration.class.isAssignableFrom( o.getClass() ) ) { 74 | o = Collections.list((Enumeration)o); 75 | } 76 | } 77 | return o; 78 | } 79 | 80 | public String toString() { 81 | return exp.getExpressionString(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/Instrumenter.java: -------------------------------------------------------------------------------- 1 | package org.openolly; 2 | 3 | import static net.bytebuddy.matcher.ElementMatchers.hasGenericSuperType; 4 | import static net.bytebuddy.matcher.ElementMatchers.isMethod; 5 | import static net.bytebuddy.matcher.ElementMatchers.named; 6 | import static net.bytebuddy.matcher.ElementMatchers.none; 7 | 8 | import java.lang.instrument.Instrumentation; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | import org.apache.commons.lang3.exception.ExceptionUtils; 13 | import org.openolly.advice.TraceAdvice; 14 | 15 | import net.bytebuddy.agent.builder.AgentBuilder; 16 | import net.bytebuddy.agent.builder.AgentBuilder.InitializationStrategy; 17 | import net.bytebuddy.agent.builder.AgentBuilder.InstallationListener; 18 | import net.bytebuddy.agent.builder.AgentBuilder.LocationStrategy; 19 | import net.bytebuddy.agent.builder.AgentBuilder.RedefinitionStrategy; 20 | import net.bytebuddy.agent.builder.AgentBuilder.TypeStrategy; 21 | import net.bytebuddy.asm.Advice; 22 | import net.bytebuddy.dynamic.ClassFileLocator; 23 | 24 | import com.google.common.flogger.FluentLogger; 25 | 26 | public class Instrumenter { 27 | 28 | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 29 | 30 | private static Instrumentation inst = null; 31 | 32 | public static void instrument(Instrumentation inst, Sensor sensor ) { 33 | List list = new ArrayList(); 34 | list.add( sensor ); 35 | instrument( inst, list ); 36 | } 37 | 38 | public static void instrument(Instrumentation inst, List sensors) { 39 | Instrumenter.inst = inst; 40 | 41 | try { 42 | AgentBuilder builder = new AgentBuilder.Default() 43 | // .with(AgentBuilder.Listener.StreamWriting.toSystemError()) 44 | // .with(AgentBuilder.Listener.StreamWriting.toSystemError().withErrorsOnly()) 45 | // .with(AgentBuilder.Listener.StreamWriting.toSystemError().withTransformationsOnly()) 46 | .with(InstallationListener.ErrorSuppressing.INSTANCE) // prevent errors from breaking app 47 | .with(LocationStrategy.ForClassLoader.STRONG.withFallbackTo(ClassFileLocator.ForClassLoader.ofBootLoader())) 48 | // .with(RedefinitionStrategy.RETRANSFORMATION) // for redefine, for java. classes too? 49 | // .with(InitializationStrategy.NoOp.INSTANCE) // for redefine 50 | // .with(TypeStrategy.Default.REDEFINE) // for redefine 51 | .disableClassFormatChanges().ignore(none()); 52 | 53 | builder = builder 54 | // add a sensor for every service method to start and stop a trace 55 | .type(hasGenericSuperType(named("javax.servlet.Servlet"))) 56 | .transform((b, td, cl, m) -> b.visit(Advice.to(TraceAdvice.class).on(named("service").and(isMethod())))); 57 | 58 | for (Sensor sensor : sensors ) { 59 | //System.err.println( "[SENSOR] processing " + sensor ); 60 | logger.atWarning().log("[SENSOR] processing " + sensor); 61 | builder = sensor.instrument( builder ); 62 | } 63 | 64 | builder.installOn(inst); 65 | } catch (Exception e) { 66 | //e.printStackTrace(); 67 | logger.atWarning().log( ExceptionUtils.getStackTrace(e) ); 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/JOTAgent.java: -------------------------------------------------------------------------------- 1 | package org.openolly; 2 | 3 | import java.lang.instrument.Instrumentation; 4 | 5 | import org.apache.commons.lang3.exception.ExceptionUtils; 6 | import org.openolly.config.ConfigReader; 7 | import org.openolly.reporting.Reporter; 8 | 9 | import com.google.common.flogger.FluentLogger; 10 | 11 | public class JOTAgent { 12 | 13 | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 14 | 15 | public static void premain(String args, Instrumentation inst) { 16 | //System.out.println("[SENSOR] In JOTAgent premain method"); 17 | transform( args, inst ); 18 | } 19 | 20 | public static void agentmain(String args, Instrumentation inst) { 21 | //System.out.println("[SENSOR] In JOTAgent agentmain method"); 22 | transform( args, inst ); 23 | } 24 | 25 | public static void transform(String yaml, Instrumentation inst) { 26 | try { 27 | ConfigReader.init(yaml); 28 | Instrumenter.instrument(inst, Sensor.getSensors()); 29 | new Reporter(); 30 | } catch (Exception e) { 31 | //e.printStackTrace(); 32 | logger.atWarning().log( ExceptionUtils.getStackTrace(e) ); 33 | } 34 | //System.err.println("[SENSOR] Java Observability Toolkit Installed"); 35 | logger.atInfo().log("[SENSOR] Java Observability Toolkit Installed"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/Matcher.java: -------------------------------------------------------------------------------- 1 | package org.openolly; 2 | 3 | import java.util.List; 4 | import java.util.regex.Pattern; 5 | 6 | public class Matcher { 7 | private Pattern pattern = null; 8 | private boolean negative = false; 9 | 10 | public Matcher( String pattern ) { 11 | this.pattern = Pattern.compile( pattern ); 12 | } 13 | 14 | public Matcher( String pattern, boolean negative ) { 15 | this.pattern = Pattern.compile( pattern ); 16 | this.negative = negative; 17 | } 18 | 19 | public boolean find( String value ) { 20 | return pattern.matcher(value).find(); } 21 | 22 | public boolean isNegative() { 23 | return negative; 24 | } 25 | 26 | // everything matches unless: 27 | // it does not match a positive (no positives means it's a match) 28 | // it does match a negative (no negatives means it's a match) 29 | public static boolean eval(String value, List positiveMatchers, List negativeMatchers) { 30 | boolean pmatch = positiveMatchers.isEmpty(); 31 | for ( Matcher m : positiveMatchers ) { 32 | if ( m.find( value ) ) { 33 | pmatch = true; 34 | break; 35 | } 36 | } 37 | boolean nmatch = false; 38 | for ( Matcher m : negativeMatchers ) { 39 | nmatch |= m.find( value ); 40 | } 41 | 42 | return pmatch && !nmatch; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/MethodIndexValue.java: -------------------------------------------------------------------------------- 1 | package org.openolly; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | @Retention(RetentionPolicy.RUNTIME) 7 | public @interface MethodIndexValue { 8 | int value(); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/MethodLocator.java: -------------------------------------------------------------------------------- 1 | package org.openolly; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | import org.openolly.advice.scope.BinaryScope; 6 | 7 | public class MethodLocator { 8 | public String clazz = null; 9 | public String method = null; 10 | public String params = null; 11 | public Method javaMethod = null; 12 | public boolean isVoid = false; 13 | private boolean negative = false; 14 | private int index = 0; 15 | 16 | private BinaryScope scope = new BinaryScope(); 17 | 18 | public MethodLocator(String sig) { 19 | int idx = sig.lastIndexOf('.'); 20 | if ( idx > -1 ) { 21 | clazz = sig.substring(0, idx); 22 | method = sig.substring(idx + 1); 23 | } else { 24 | clazz = sig; 25 | } 26 | } 27 | 28 | public MethodLocator( String clazz, String method) { 29 | this.clazz = clazz; 30 | this.method = method; 31 | } 32 | 33 | public MethodLocator(String sig, boolean negative ) { 34 | this( sig ); 35 | this.negative = negative; 36 | } 37 | 38 | public void enterScope() { 39 | scope.enterScope(); 40 | } 41 | 42 | public void leaveScope() { 43 | scope.leaveScope(); 44 | } 45 | 46 | public boolean inScope() { 47 | if ( negative ) { 48 | return !scope.inScope(); 49 | } 50 | return scope.inScope(); 51 | } 52 | 53 | public boolean inOutermostScope() { 54 | if ( negative ) { 55 | return !scope.inOutermostScope(); 56 | } 57 | return scope.inOutermostScope(); 58 | } 59 | 60 | public int scopeValue() { 61 | return scope.value(); 62 | } 63 | 64 | public Method getJavaMethod(Object o) throws NoSuchMethodException, SecurityException, ClassNotFoundException { 65 | if (javaMethod != null) 66 | return javaMethod; 67 | else { 68 | javaMethod = o.getClass().getMethod(method, null); 69 | } 70 | return javaMethod; 71 | } 72 | 73 | public String className() { 74 | return clazz; 75 | } 76 | 77 | public String methodName() { 78 | return method; 79 | } 80 | 81 | public boolean isConstructor() { 82 | return method.equals( "" ); 83 | } 84 | 85 | public boolean isNegative() { 86 | return negative; 87 | } 88 | 89 | public String toString() { 90 | StringBuilder sb = new StringBuilder(); 91 | if ( clazz != null ) { 92 | sb.append( clazz + "." ); 93 | } 94 | sb.append( method + "(" ); 95 | if ( params != null ) { 96 | sb.append( params ); 97 | } 98 | sb.append( ")" ); 99 | return sb.toString(); 100 | } 101 | 102 | public void setIndex(int index) { 103 | this.index = index; 104 | } 105 | public int getIndex() { 106 | return index; 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/SafeString.java: -------------------------------------------------------------------------------- 1 | package org.openolly; 2 | 3 | import java.lang.reflect.Method; 4 | import java.util.Arrays; 5 | import java.util.Collection; 6 | import java.util.Collections; 7 | import java.util.Enumeration; 8 | import java.util.HashSet; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.Map.Entry; 12 | import java.util.Set; 13 | import java.util.Vector; 14 | 15 | import com.google.common.flogger.FluentLogger; 16 | 17 | public class SafeString { 18 | 19 | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 20 | 21 | public static void main( String[] args ) { 22 | Object[] arr = {"foo","bar",new String[]{"blue","red"}}; 23 | //System.out.println( "CT: " + arr.getClass().getComponentType() ); 24 | logger.atInfo().log( "CT: " + arr.getClass().getComponentType() ); 25 | //System.out.println( "ASS: " + arr.getClass().getComponentType().isPrimitive()); 26 | logger.atInfo().log("ASS: " + arr.getClass().getComponentType().isPrimitive()); 27 | //System.out.println( "A: " + SafeString.format(arr, false)); 28 | logger.atInfo().log("A: " + SafeString.format(arr, false)); 29 | int[] arr1 = {1,2}; 30 | //System.out.println( "CT: " + arr1.getClass().getComponentType() ); 31 | logger.atInfo().log("CT: " + arr1.getClass().getComponentType() ); 32 | //System.out.println( "ASS: " + arr1.getClass().getComponentType().isPrimitive()); 33 | logger.atInfo().log("ASS: " + arr1.getClass().getComponentType().isPrimitive()); 34 | //System.out.println( "A: " + SafeString.format(arr1, false)); 35 | logger.atInfo().log("A: " + SafeString.format(arr1, false)); 36 | Vector v = new Vector(); 37 | v.add( 1 ); 38 | v.add( 2); 39 | //System.out.println( "V: " + SafeString.format(v.elements(), false)); 40 | logger.atInfo().log( "V: " + SafeString.format(v.elements(), false)); 41 | Object[] arr3 = {"foo","bar",null}; 42 | //System.out.println( "A3: " + SafeString.format(arr3, false)); 43 | logger.atInfo().log( "A3: " + SafeString.format(arr3, false)); 44 | Object[] arr4 = null; 45 | //System.out.println( "A4: " + SafeString.format(arr4, false)); 46 | logger.atInfo().log( "A4: " + SafeString.format(arr4, false)); 47 | } 48 | public static String format( Object o, boolean debug ) { 49 | return format( o, false, debug ); 50 | } 51 | 52 | // convert enumeration to list before sending 53 | public static String format( Object o, boolean readOnly, boolean debug ) { 54 | if ( o == null ) { 55 | return "null"; 56 | } 57 | if ( o instanceof String ) { 58 | return (String)o; 59 | } 60 | if ( o instanceof Entry ) { 61 | return formatMapEntry( (Entry)o ); 62 | } 63 | if ( !readOnly && o instanceof Enumeration ) { 64 | return formatEnumeration( (Enumeration)o ); 65 | } 66 | if ( o.getClass().isArray() ) { 67 | return( formatArray( o ) ); 68 | } 69 | if ( o instanceof Collection ) { 70 | return Arrays.deepToString(((Collection)o).toArray()); 71 | } 72 | if ( o instanceof Map ) { 73 | return formatMap( (Map)o ); 74 | } 75 | String ret = "{" + o.getClass() + ":" + o.toString() + "}"; 76 | if ( debug ) { 77 | //System.out.println( "[SENSOR] Defaulting to toString() - " + ret ); 78 | logger.atFinest().log( "[SENSOR] Defaulting to toString() - " + ret ); 79 | dumpMethods(o); 80 | } 81 | return ret; 82 | } 83 | 84 | private static void dumpMethods(Object o) { 85 | //System.err.println( "[SENSOR] Possible methods to invoke on: " + o.getClass()); 86 | logger.atWarning().log("[SENSOR] Possible methods to invoke on: " + o.getClass()); 87 | for ( Method m : getAllMethodsInHierarchy(o.getClass())) { 88 | if ( m.getParameterCount() == 0 ) { 89 | //System.err.println( " " + m.getName() + "()" ); 90 | logger.atWarning().log(" " + m.getName() + "()" ); 91 | } 92 | } 93 | } 94 | 95 | public static Method[] getAllMethodsInHierarchy(Class objectClass) { 96 | Set allMethods = new HashSet(); 97 | Method[] declaredMethods = objectClass.getDeclaredMethods(); 98 | Method[] methods = objectClass.getMethods(); 99 | if (objectClass.getSuperclass() != null) { 100 | Class superClass = objectClass.getSuperclass(); 101 | Method[] superClassMethods = getAllMethodsInHierarchy(superClass); 102 | allMethods.addAll(Arrays.asList(superClassMethods)); 103 | } 104 | allMethods.addAll(Arrays.asList(declaredMethods)); 105 | allMethods.addAll(Arrays.asList(methods)); 106 | return allMethods.toArray(new Method[allMethods.size()]); 107 | } 108 | 109 | private static String formatArray( Object o ) { 110 | if ( !o.getClass().isArray() ) return "{not an array}"; 111 | Class type = o.getClass().getComponentType(); 112 | if ( !type.isPrimitive() ) return Arrays.deepToString( (Object[])o ); 113 | if ( type == byte.class ) return Arrays.toString( (int[])o ); 114 | if ( type == short.class ) return Arrays.toString( (int[])o ); 115 | if ( type == int.class ) return Arrays.toString( (int[])o ); 116 | if ( type == long.class ) return Arrays.toString( (int[])o ); 117 | if ( type == float.class ) return Arrays.toString( (int[])o ); 118 | if ( type == double.class ) return Arrays.toString( (int[])o ); 119 | if ( type == boolean.class ) return Arrays.toString( (int[])o ); 120 | if ( type == char.class ) return Arrays.toString( (int[])o ); 121 | return "{unknown}"; 122 | } 123 | 124 | private static String formatMapEntry(Entry o) { 125 | StringBuilder sb = new StringBuilder("["); 126 | sb.append( o.getKey() ); 127 | sb.append( "=" ); 128 | sb.append( o.getValue() ); 129 | sb.append( "]"); 130 | return sb.toString(); 131 | } 132 | 133 | // FIXME: danger, this burns the enumeration, don't modify an enumeration being returned! 134 | private static String formatEnumeration(Enumeration o) { 135 | List list = Collections.list(o); 136 | return list.toString(); 137 | } 138 | 139 | private static String formatMap(Map o) { 140 | StringBuilder sb = new StringBuilder("["); 141 | for ( Entry e : (Set)o.entrySet() ) { 142 | sb.append( format( e, false ) ); 143 | sb.append( "," ); 144 | } 145 | sb.setLength(sb.length()-1); 146 | return sb.toString() + "]"; 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/Sensor.java: -------------------------------------------------------------------------------- 1 | package org.openolly; 2 | 3 | import static net.bytebuddy.matcher.ElementMatchers.failSafe; 4 | import static net.bytebuddy.matcher.ElementMatchers.hasGenericSuperType; 5 | import static net.bytebuddy.matcher.ElementMatchers.isConstructor; 6 | import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; 7 | import static net.bytebuddy.matcher.ElementMatchers.nameContains; 8 | import static net.bytebuddy.matcher.ElementMatchers.named; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Collection; 12 | import java.util.Collections; 13 | import java.util.List; 14 | import java.util.regex.Pattern; 15 | 16 | import org.apache.commons.lang3.StringUtils; 17 | import org.apache.commons.lang3.exception.ExceptionUtils; 18 | import org.openolly.advice.CEAdvice; 19 | import org.openolly.advice.CNEAdvice; 20 | import org.openolly.advice.MRAdvice; 21 | import org.openolly.advice.MVEAdvice; 22 | import org.openolly.advice.MVNEAdvice; 23 | import org.openolly.advice.ScopeAdvice; 24 | import org.openolly.advice.SensorException; 25 | import org.openolly.advice.scope.BinaryScope; 26 | import org.openolly.reporting.Event; 27 | import org.openolly.reporting.Trace; 28 | 29 | import com.google.common.flogger.FluentLogger; 30 | 31 | import net.bytebuddy.agent.builder.AgentBuilder; 32 | import net.bytebuddy.asm.Advice; 33 | 34 | public class Sensor { 35 | 36 | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 37 | private static List sensors = new ArrayList(); 38 | 39 | // protect against ANY sensor firing inside ANY other sensor 40 | // a threadlocal scope for *this* sensor for *this* thread 41 | private BinaryScope globalScope = new BinaryScope(); 42 | 43 | // an index to use to look up the right Sensor from within an instrumented method 44 | private int index; 45 | 46 | private String name = null; 47 | private String description = null; 48 | private List methods = new ArrayList(); 49 | private List scopes = new ArrayList(); 50 | private List excludes = new ArrayList(); 51 | private List captures = new ArrayList(); 52 | private List negativeMatchers = new ArrayList(); 53 | private List positiveMatchers = new ArrayList(); 54 | private String exception = null; 55 | private boolean debug = false; 56 | private boolean hasReturn = false; 57 | 58 | private Pattern namePattern = Pattern.compile( "([a-z]+\\-)*[a-z]+"); 59 | private Pattern descriptionPattern = Pattern.compile( "^.*$"); 60 | private Pattern methodPattern = Pattern.compile( "^(!)?([\\p{L}_$][\\p{L}\\p{N}_$]*\\.)*(|[\\p{L}_$][\\p{L}\\p{N}_$]*)$"); 61 | private Pattern capturePattern = Pattern.compile( "^.*$"); 62 | private Pattern exceptionPattern = Pattern.compile( "^.*$"); 63 | 64 | public Sensor() { 65 | sensors.add( this ); 66 | this.index = sensors.indexOf(this); 67 | } 68 | 69 | public static boolean exists(String name) { 70 | for ( Sensor s : sensors ) { 71 | if ( s.getName().equals( name ) ) { 72 | return true; 73 | } 74 | } 75 | return false; 76 | } 77 | 78 | public static Sensor getSensor(int sensorIndex) { 79 | return sensors.get( sensorIndex ); 80 | } 81 | 82 | public static List getSensors() { 83 | return Collections.unmodifiableList(sensors); 84 | } 85 | 86 | public static void clearSensors() { 87 | sensors.clear(); 88 | } 89 | 90 | public List execute(String advice, String caller, Object obj, Object[] params, Object ret, StackTraceElement[] stack) { 91 | 92 | // protect against calling execute from itself 93 | if ( globalScope.inScope() ) { 94 | 95 | // FIXME: add if ( debug ) before all logger calls? Or figure out how to do it right 96 | logger.atWarning().log( "[JOT %s] Skipping sensor - already in scope %s", this, globalScope.value() ); 97 | return new ArrayList(); 98 | } 99 | 100 | // return results for making tests easier 101 | List results = new ArrayList(); 102 | try { 103 | globalScope.enterScope(); 104 | 105 | // if there are scopes, and none are in scope, quit 106 | if ( !scopes.isEmpty() ) { 107 | boolean inScope = false; 108 | for ( MethodLocator scope : scopes ) { 109 | inScope |= ( scope.inScope() ); 110 | } 111 | if ( !inScope ) return results; 112 | } 113 | 114 | for (Expression capture : captures) { 115 | try { 116 | Object o = capture.execute(obj, params, ret, caller, stack); 117 | 118 | if ( o instanceof Collection ) { 119 | for ( Object item : (Collection)o ) { 120 | String safe = SafeString.format( item, debug ); 121 | boolean matched = Matcher.eval( safe, getPositiveMatchers(), getNegativeMatchers() ); 122 | if ( matched ) { 123 | String cleaned = StringUtils.normalizeSpace( safe ); 124 | Event event = new Event(getName(), cleaned, stack); 125 | Trace.getCurrentTrace().addEvent( event ); 126 | // logger.atWarning().log( "[JOT %s] %s", this, event ); 127 | results.add(safe); 128 | } 129 | } 130 | } else { 131 | String safe = SafeString.format( o, debug ); 132 | boolean matched = Matcher.eval( safe, getPositiveMatchers(), getNegativeMatchers() ); 133 | if ( matched ) { 134 | String cleaned = StringUtils.normalizeSpace( safe ); 135 | Event event = new Event(getName(), cleaned, stack); 136 | Trace.getCurrentTrace().addEvent( event ); 137 | // logger.atWarning().log( "[JOT %s] %s", this, event ); 138 | results.add(safe); 139 | } 140 | } 141 | 142 | } catch( Exception e ) { 143 | logger.atWarning().log( "[JOT %s] Error running capture (%s) %s", this, capture, e.getMessage() ); 144 | //e.printStackTrace(); 145 | logger.atWarning().log( ExceptionUtils.getStackTrace(e) ); 146 | } 147 | } 148 | 149 | // if there are either results or zero captures, generate exceptions if any 150 | if ( getException() != null ) { 151 | if ( captures.isEmpty() || !results.isEmpty() ) { 152 | String details = SafeString.format( results, false ); 153 | SensorException e = new SensorException( getException() ); 154 | logger.atInfo().log( "[JOT %s] %s: %s", this, getException(), details ); 155 | throw e; 156 | } 157 | } 158 | } finally { 159 | globalScope.leaveScope(); 160 | } 161 | return results; 162 | } 163 | 164 | public String toString() { 165 | return getName(); 166 | } 167 | 168 | //================= parsing 169 | 170 | public Sensor name(String value) { 171 | if ( value == null ) { 172 | logger.atWarning().log( "[JOT %s] YAML error: name was missing. Continuing.", this ); 173 | return this; 174 | } 175 | if (!namePattern.matcher(value).matches()) { 176 | logger.atWarning().log( "[JOT %s] YAML error: malformed name (" + value + ") must match " + namePattern.pattern() + ". Continuing", this ); 177 | } 178 | name = value; 179 | return this; 180 | } 181 | 182 | public Sensor description(String value) { 183 | if ( value == null ) { 184 | return this; 185 | } 186 | if (!descriptionPattern.matcher(value).matches()) { 187 | logger.atWarning().log( "[JOT %s] YAML error: malformed description (" + value + ") must match " + descriptionPattern.pattern() + ". Continuing", this ); 188 | } 189 | description = value; 190 | return this; 191 | } 192 | 193 | 194 | public Sensor method(String value) { 195 | if ( value == null ) { 196 | logger.atWarning().log( "[JOT %s] YAML error: at least one method is required. Continuing", this ); 197 | return this; 198 | } 199 | if (!methodPattern.matcher(value).matches()) { 200 | logger.atWarning().log( "[JOT %s] YAML error: malformed method (" + value + ") must match " + methodPattern.pattern() + ". Continuing", this ); 201 | } 202 | MethodLocator loc = new MethodLocator( value ); 203 | methods.add( loc ); 204 | loc.setIndex( methods.indexOf( loc ) ); 205 | return this; 206 | } 207 | 208 | public Sensor methods( List list ) { 209 | if ( list == null || list.isEmpty() ) { 210 | return this; 211 | } 212 | for ( String value : list ) { 213 | method( value ); 214 | } 215 | return this; 216 | } 217 | 218 | public Sensor scope(String value) { 219 | if ( value == null ) { 220 | return this; 221 | } 222 | boolean negative = false; 223 | if ( value.startsWith( "!" ) ) { 224 | negative = true; 225 | value = value.substring(1); 226 | } 227 | if (!methodPattern.matcher(value).matches()) { 228 | logger.atWarning().log( "[JOT %s] YAML error: malformed scope (" + value + ") must match " + methodPattern.pattern(), this ); 229 | } 230 | scopes.add(new MethodLocator(value, negative)); 231 | return this; 232 | } 233 | 234 | public Sensor scopes( List list ) { 235 | if ( list == null || list.isEmpty() ) { 236 | return this; 237 | } 238 | for ( String s : list ) { 239 | scope( s ); 240 | } 241 | return this; 242 | } 243 | 244 | public Sensor exclude(String value) { 245 | if ( value == null ) { 246 | return this; 247 | } 248 | if (!methodPattern.matcher(value).matches()) { 249 | logger.atWarning().log( "[JOT %s] YAML error: malformed exclude (" + value + ") must match " + methodPattern.pattern(), this ); 250 | } 251 | excludes.add(new MethodLocator(value, null)); 252 | return this; 253 | } 254 | 255 | public Sensor excludes( List list ) { 256 | if ( list == null || list.isEmpty() ) { 257 | return this; 258 | } 259 | for ( String s : list ) { 260 | exclude( s ); 261 | } 262 | return this; 263 | } 264 | 265 | public Sensor capture(String value) { 266 | if ( value == null ) { 267 | return this; 268 | } 269 | if (!capturePattern.matcher(value).matches()) { 270 | logger.atWarning().log( "[JOT %s] YAML error: malformed capture (" + value + ") must match " + capturePattern.pattern(), this ); 271 | } 272 | try { 273 | Expression exp = new Expression(value); 274 | if ( value.contains( "#RET" ) ) { 275 | hasReturn = true; 276 | } 277 | captures.add( exp ); 278 | } catch( Exception e ) { 279 | logger.atWarning().log( "[JOT %s] YAML error: malformed capture (" + value + "): " + e.getMessage(), this ); 280 | } 281 | return this; 282 | } 283 | 284 | public Sensor captures( List list ) { 285 | if ( list == null || list.isEmpty() ) { 286 | return this; 287 | } 288 | for ( String s : list ) { 289 | capture( s ); 290 | } 291 | return this; 292 | } 293 | 294 | public Sensor matcher(String value) { 295 | try { 296 | if ( value == null ) { 297 | return this; 298 | } 299 | if ( value.startsWith( "!" ) ) { 300 | value = value.substring(1); 301 | Matcher matcher = new Matcher( value, true ); 302 | negativeMatchers.add( matcher ); 303 | } else { 304 | Matcher matcher = new Matcher( value, false ); 305 | positiveMatchers.add( matcher ); 306 | } 307 | } catch( Exception e ) { 308 | logger.atWarning().log( "[JOT %s] YAML error: malformed matcher (" + value + "): " + e.getMessage(), this ); 309 | } 310 | return this; 311 | } 312 | 313 | public Sensor matchers( List list ) { 314 | if ( list == null || list.isEmpty() ) { 315 | return this; 316 | } 317 | for ( String s : list ) { 318 | matcher( s ); 319 | } 320 | return this; 321 | } 322 | 323 | public Sensor exception(String value) { 324 | if ( value == null ) { 325 | return this; 326 | } 327 | if (!exceptionPattern.matcher(value).matches()) { 328 | logger.atWarning().log( "[JOT %s] YAML error: malformed exception (" + value + ") must match " + exceptionPattern.pattern(), this ); 329 | } 330 | exception = value; 331 | return this; 332 | } 333 | 334 | public Sensor debug( boolean value ) { 335 | debug = value; 336 | return this; 337 | } 338 | 339 | 340 | 341 | // =========== 342 | 343 | public String getName() { 344 | return name; 345 | } 346 | 347 | public String getDescription() { 348 | return description; 349 | } 350 | 351 | public List getMethods() { 352 | return methods; 353 | } 354 | 355 | public List getScopes() { 356 | return scopes; 357 | } 358 | 359 | public List getExcludes() { 360 | return excludes; 361 | } 362 | 363 | public List getCaptures() { 364 | return captures; 365 | } 366 | 367 | public List getPositiveMatchers() { 368 | return positiveMatchers; 369 | } 370 | 371 | public List getNegativeMatchers() { 372 | return negativeMatchers; 373 | } 374 | 375 | public String getException() { 376 | return exception; 377 | } 378 | 379 | public boolean hasException() { 380 | return exception != null; 381 | } 382 | 383 | public int getIndex() { 384 | return index; 385 | } 386 | 387 | public MethodLocator getMethodScope( int index ) { 388 | return methods.get( index ); 389 | } 390 | 391 | public boolean getDebug() { 392 | return debug; 393 | } 394 | 395 | public boolean hasReturn() { 396 | return hasReturn; 397 | } 398 | 399 | //============== 400 | 401 | public static void enterMethodScope( int sensorIndex, int methodIndex ) { 402 | getSensor( sensorIndex ).enterMethodScope( methodIndex ); 403 | } 404 | 405 | public static void leaveMethodScope( int sensorIndex, int methodIndex ) { 406 | getSensor( sensorIndex ).leaveMethodScope( methodIndex ); 407 | } 408 | 409 | public int methodScopeValue(int methodIndex) { 410 | MethodLocator loc = methods.get(methodIndex); 411 | return loc.scopeValue(); 412 | } 413 | 414 | public void enterMethodScope(int methodIndex) { 415 | MethodLocator loc = methods.get(methodIndex); 416 | if ( debug ) logger.atFine().log( "[JOT %s] METHOD SCOPE ["+methodIndex+"] + " + loc.scopeValue() +" >> "+ getName() + " " + (loc.scopeValue() + 1), this ); 417 | loc.enterScope(); 418 | } 419 | 420 | public void leaveMethodScope(int methodIndex) { 421 | MethodLocator loc = methods.get(methodIndex); 422 | loc.leaveScope(); 423 | if ( debug ) logger.atFine().log( "[JOT %s] METHOD SCOPE ["+methodIndex+"] - " + (loc.scopeValue() + 1) + " >> "+ getName() + " " + loc.scopeValue(), this ); 424 | } 425 | 426 | public boolean inOutermostMethodScope(int methodIndex) { 427 | MethodLocator loc = methods.get(methodIndex); 428 | return loc.inOutermostScope(); 429 | } 430 | 431 | //=========== stop reentry into sensors 432 | 433 | public void enterGlobalScope() { 434 | globalScope.enterScope(); 435 | } 436 | 437 | //=========== FIXME: ScopeScope is a stupid name 438 | 439 | public static void enterScopeScope( int sensorIndex, int scopeIndex ) { 440 | getSensor( sensorIndex ).enterScopeScope( scopeIndex ); 441 | } 442 | 443 | public static void leaveScopeScope( int sensorIndex, int scopeIndex ) { 444 | getSensor( sensorIndex ).leaveScopeScope( scopeIndex ); 445 | } 446 | 447 | public int scopeScopeValue(int scopeIndex) { 448 | MethodLocator loc = scopes.get(scopeIndex); 449 | return loc.scopeValue(); 450 | } 451 | 452 | public void enterScopeScope(int scopeIndex) { 453 | MethodLocator loc = scopes.get(scopeIndex); 454 | if ( debug ) logger.atFine().log( "[JOT %s] SCOPE SCOPE" + loc.scopeValue() +" >> "+ getName(), this); 455 | loc.enterScope(); 456 | } 457 | 458 | public void leaveScopeScope(int scopeIndex) { 459 | MethodLocator loc = scopes.get(scopeIndex); 460 | loc.leaveScope(); 461 | if ( debug ) logger.atFine().log( "[JOT %s] SCOPE SCOPE" + loc.scopeValue() +" << "+ getName(), this); 462 | } 463 | 464 | public boolean inOutermostScopeScope(int scopeIndex) { 465 | MethodLocator loc = scopes.get(scopeIndex); 466 | return loc.inOutermostScope(); 467 | } 468 | 469 | 470 | public AgentBuilder instrument(AgentBuilder builder ) { 471 | 472 | // global class ignores 473 | builder = builder 474 | .ignore(nameContains("maven")) 475 | .ignore(nameContains("codehaus")) 476 | .ignore(nameStartsWith("shaded")) 477 | .ignore(nameStartsWith("org.eclipse.osgi")); 478 | 479 | // add advice to "methods" 480 | for ( MethodLocator method : this.getMethods() ) { 481 | //System.err.println( "INSTRUMENTING " + method.className() + " -> " + method.methodName() ); 482 | logger.atWarning().log("INSTRUMENTING " + method.className() + " -> " + method.methodName() ); 483 | try { 484 | 485 | // ignore methods listed as excludes in JOT rule 486 | for ( MethodLocator eloc : this.getExcludes() ) { 487 | //System.out.println ( "EXCLUDING: " + eloc.className() + " from " + method ); 488 | logger.atInfo().log( "EXCLUDING: " + eloc.className() + " from " + method ); 489 | builder.ignore(nameStartsWith(eloc.className()) ); 490 | builder.ignore(hasGenericSuperType(failSafe(named(eloc.className())))); 491 | } 492 | 493 | // transform constructors with JOT exception rule (CE) 494 | if ( method.isConstructor() && hasException() ) { 495 | builder = builder 496 | .type(hasGenericSuperType(failSafe(named(method.className())))) 497 | .transform((b, td, cl, m) -> b.visit( Advice 498 | .withCustomMapping() 499 | .bind(SensorIndexValue.class, this.getIndex() ) 500 | .bind(MethodIndexValue.class, method.getIndex() ) 501 | .to(CEAdvice.class) 502 | .on( isConstructor())) ); 503 | } 504 | 505 | // transform constructors with no JOT exception rule (CNE) 506 | else if ( method.isConstructor() ) { 507 | builder = builder 508 | .type(hasGenericSuperType(failSafe(named(method.className())))) 509 | .transform((b, td, cl, m) -> b.visit( Advice 510 | .withCustomMapping() 511 | .bind(SensorIndexValue.class, this.getIndex() ) 512 | .bind(MethodIndexValue.class, method.getIndex() ) 513 | .to(CNEAdvice.class) 514 | .on( isConstructor())) ); 515 | } 516 | 517 | // transform methods with a return but no JOT exception (MR) 518 | else if ( hasReturn() ) { 519 | builder = builder 520 | .type(hasGenericSuperType(failSafe(named(method.className())))) 521 | .transform((b, td, cl, m) -> b.visit( Advice 522 | .withCustomMapping() 523 | .bind(SensorIndexValue.class, this.getIndex() ) 524 | .bind(MethodIndexValue.class, method.getIndex() ) 525 | .to(MRAdvice.class) 526 | .on(named(method.methodName())) ) ); 527 | } 528 | 529 | // transform methods with no return and JOT exception rule (MVE) 530 | else if ( hasException() ) { 531 | builder = builder 532 | .type(hasGenericSuperType(failSafe(named(method.className())))) 533 | .transform((b, td, cl, m) -> b.visit( Advice 534 | .withCustomMapping() 535 | .bind(SensorIndexValue.class, this.getIndex() ) 536 | .bind(MethodIndexValue.class, method.getIndex() ) 537 | .to(MVEAdvice.class) 538 | .on(named(method.methodName())) ) ); 539 | } 540 | 541 | // transform methods with no return and no JOT exception (MVNE) 542 | else { 543 | builder = builder 544 | .type(hasGenericSuperType(failSafe(named(method.className())))) 545 | .transform((b, td, cl, m) -> b.visit( Advice 546 | .withCustomMapping() 547 | .bind(SensorIndexValue.class, this.getIndex() ) 548 | .bind(MethodIndexValue.class, method.getIndex() ) 549 | .to(MVNEAdvice.class) 550 | .on(named(method.methodName())) ) ); 551 | } 552 | 553 | } catch (Exception e) { 554 | //System.err.println("WARNING: Error instrumenting: " + e.getMessage()); 555 | logger.atWarning().log("WARNING: Error instrumenting: " + e.getMessage()); 556 | //e.printStackTrace(); 557 | logger.atWarning().log( ExceptionUtils.getStackTrace(e) ); 558 | } 559 | } 560 | 561 | // add advice to "scopes" 562 | for ( MethodLocator scope : this.getScopes() ) { 563 | //System.err.println( "SCOPERIZING " + scope.className() + " -> " + scope.methodName() ); 564 | logger.atWarning().log("SCOPERIZING " + scope.className() + " -> " + scope.methodName() ); 565 | try { 566 | builder = builder 567 | .type(hasGenericSuperType(failSafe(named(scope.className())))) 568 | .transform((b, td, cl, m) -> b.visit( Advice 569 | .withCustomMapping() 570 | .bind(SensorIndexValue.class, this.getIndex() ) 571 | .bind(MethodIndexValue.class, scope.getIndex() ) 572 | .to(ScopeAdvice.class) 573 | .on(named(scope.methodName()) 574 | .or(named(scope.methodName()) 575 | .or(isConstructor()))) ) ); 576 | 577 | } catch (Exception e) { 578 | //System.err.println("WARNING: Error instrumenting: " + e.getMessage()); 579 | logger.atWarning().log("WARNING: Error instrumenting: " + e.getMessage()); 580 | //e.printStackTrace(); 581 | logger.atWarning().log( ExceptionUtils.getStackTrace(e) ); 582 | } 583 | } 584 | 585 | return builder; 586 | } 587 | 588 | } 589 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/SensorIndexValue.java: -------------------------------------------------------------------------------- 1 | package org.openolly; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | @Retention(RetentionPolicy.RUNTIME) 7 | public @interface SensorIndexValue { 8 | int value(); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/advice/CEAdvice.java: -------------------------------------------------------------------------------- 1 | package org.openolly.advice; 2 | 3 | import org.apache.commons.lang3.exception.ExceptionUtils; 4 | import org.openolly.MethodIndexValue; 5 | import org.openolly.Sensor; 6 | import org.openolly.SensorIndexValue; 7 | 8 | import com.google.common.flogger.FluentLogger; 9 | 10 | import net.bytebuddy.asm.Advice; 11 | 12 | public class CEAdvice { 13 | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 14 | @Advice.OnMethodEnter() 15 | public static void enter(@SensorIndexValue(value = -1) int sensorIndex, 16 | @MethodIndexValue(value = -1) int methodIndex, @Advice.Origin("#t") String className, 17 | @Advice.Origin("#m") String methodName, @Advice.This(optional = true) Object obj, 18 | @Advice.AllArguments Object[] args) throws SensorException { 19 | Sensor sensor = Sensor.getSensor(sensorIndex); 20 | try { 21 | sensor.enterMethodScope(methodIndex); 22 | if (sensor.inOutermostMethodScope(methodIndex)) { 23 | String sig = className + "." + methodName + "()"; 24 | StackTraceElement[] trace = Thread.currentThread().getStackTrace(); 25 | sensor.execute("CEAdvice", sig, null, args, null, trace); 26 | } 27 | } catch (Throwable t) { 28 | if ( t instanceof SensorException ) { 29 | throw t; 30 | } else { 31 | //t.printStackTrace(); 32 | logger.atWarning().log( ExceptionUtils.getStackTrace(t) ); 33 | } 34 | } 35 | } 36 | 37 | @Advice.OnMethodExit() 38 | public static void exit(@SensorIndexValue(value = -1) int sensorIndex, 39 | @MethodIndexValue(value = -1) int methodIndex) { 40 | Sensor sensor = Sensor.getSensor(sensorIndex); 41 | sensor.leaveMethodScope(methodIndex); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/advice/CNEAdvice.java: -------------------------------------------------------------------------------- 1 | package org.openolly.advice; 2 | 3 | import org.openolly.MethodIndexValue; 4 | import org.openolly.Sensor; 5 | import org.openolly.SensorIndexValue; 6 | 7 | import net.bytebuddy.asm.Advice; 8 | 9 | public class CNEAdvice { 10 | 11 | @Advice.OnMethodEnter() 12 | public static void enter(@SensorIndexValue(value = -1) int sensorIndex, 13 | @MethodIndexValue(value = -1) int methodIndex) { 14 | Sensor sensor = Sensor.getSensor(sensorIndex); 15 | sensor.enterMethodScope(methodIndex); 16 | } 17 | 18 | @Advice.OnMethodExit() 19 | public static void exit(@SensorIndexValue(value = -1) int sensorIndex, 20 | @MethodIndexValue(value = -1) int methodIndex, @Advice.Origin("#t") String className, 21 | @Advice.Origin("#m") String methodName, @Advice.This(optional = true) Object obj, 22 | @Advice.AllArguments Object[] args) { 23 | Sensor sensor = Sensor.getSensor(sensorIndex); 24 | try { 25 | if (sensor.inOutermostMethodScope(methodIndex)) { 26 | String sig = className + "." + methodName + "()"; 27 | StackTraceElement[] trace = Thread.currentThread().getStackTrace(); 28 | sensor.execute("CNEAdvice", sig, obj, args, null, trace); 29 | } 30 | } finally { 31 | sensor.leaveMethodScope(methodIndex); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/advice/MRAdvice.java: -------------------------------------------------------------------------------- 1 | package org.openolly.advice; 2 | 3 | import org.apache.commons.lang3.exception.ExceptionUtils; 4 | import org.openolly.MethodIndexValue; 5 | import org.openolly.Sensor; 6 | import org.openolly.SensorIndexValue; 7 | 8 | import com.google.common.flogger.FluentLogger; 9 | 10 | import net.bytebuddy.asm.Advice; 11 | 12 | public class MRAdvice { 13 | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 14 | @Advice.OnMethodEnter() 15 | public static void enter(@SensorIndexValue(value = -1) int sensorIndex, 16 | @MethodIndexValue(value = -1) int methodIndex) { 17 | Sensor sensor = Sensor.getSensor(sensorIndex); 18 | sensor.enterMethodScope(methodIndex); 19 | } 20 | 21 | @Advice.OnMethodExit(onThrowable = Throwable.class) 22 | public static void exit(@SensorIndexValue(value = -1) int sensorIndex, 23 | @MethodIndexValue(value = -1) int methodIndex, @Advice.Origin("#s") String className, 24 | @Advice.Origin("#m") String methodName, @Advice.This(optional=true) Object obj, 25 | @Advice.AllArguments Object[] args, @Advice.Return Object ret) throws SensorException { 26 | Sensor sensor = Sensor.getSensor(sensorIndex); 27 | try { 28 | if (sensor.inOutermostMethodScope(methodIndex)) { 29 | String sig = className + "." + methodName + "()"; 30 | StackTraceElement[] trace = Thread.currentThread().getStackTrace(); 31 | sensor.execute("MRAdvice", sig, obj, args, ret, trace); 32 | } 33 | } catch( Throwable t ) { 34 | if ( !(t instanceof SensorException) ) { 35 | throw t; 36 | } else { 37 | //t.printStackTrace(); 38 | logger.atWarning().log( ExceptionUtils.getStackTrace(t) ); 39 | } 40 | } finally { 41 | sensor.leaveMethodScope(methodIndex); 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/advice/MVEAdvice.java: -------------------------------------------------------------------------------- 1 | package org.openolly.advice; 2 | 3 | import org.apache.commons.lang3.exception.ExceptionUtils; 4 | import org.openolly.MethodIndexValue; 5 | import org.openolly.Sensor; 6 | import org.openolly.SensorIndexValue; 7 | 8 | import com.google.common.flogger.FluentLogger; 9 | 10 | import net.bytebuddy.asm.Advice; 11 | 12 | public class MVEAdvice { 13 | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 14 | @Advice.OnMethodEnter() 15 | public static void enter(@SensorIndexValue(value = -1) int sensorIndex, 16 | @MethodIndexValue(value = -1) int methodIndex, @Advice.Origin("#t") String className, 17 | @Advice.Origin("#m") String methodName, @Advice.This(optional = true) Object obj, 18 | @Advice.AllArguments Object[] args) throws SensorException { 19 | Sensor sensor = Sensor.getSensor(sensorIndex); 20 | try { 21 | sensor.enterMethodScope(methodIndex); 22 | if (sensor.inOutermostMethodScope(methodIndex)) { 23 | String sig = className + "." + methodName + "()"; 24 | StackTraceElement[] trace = Thread.currentThread().getStackTrace(); 25 | sensor.execute("MVEAdvice", sig, obj, args, null, trace); 26 | } 27 | } catch (Throwable t) { 28 | if ( t instanceof SensorException ) { 29 | throw t; 30 | } else { 31 | //t.printStackTrace(); 32 | logger.atWarning().log( ExceptionUtils.getStackTrace(t) ); 33 | } 34 | } 35 | } 36 | 37 | @Advice.OnMethodExit(onThrowable = Throwable.class) 38 | public static void exit(@SensorIndexValue(value = -1) int sensorIndex, 39 | @MethodIndexValue(value = -1) int methodIndex) { 40 | Sensor sensor = Sensor.getSensor(sensorIndex); 41 | sensor.leaveMethodScope(methodIndex); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/advice/MVNEAdvice.java: -------------------------------------------------------------------------------- 1 | package org.openolly.advice; 2 | 3 | import org.apache.commons.lang3.exception.ExceptionUtils; 4 | import org.openolly.MethodIndexValue; 5 | import org.openolly.Sensor; 6 | import org.openolly.SensorIndexValue; 7 | 8 | import com.google.common.flogger.FluentLogger; 9 | 10 | import net.bytebuddy.asm.Advice; 11 | 12 | public class MVNEAdvice { 13 | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 14 | @Advice.OnMethodEnter() 15 | public static void enter(@SensorIndexValue(value = -1) int sensorIndex, 16 | @MethodIndexValue(value = -1) int methodIndex, @Advice.Origin("#t") String className, 17 | @Advice.Origin("#m") String methodName, @Advice.This(optional = true) Object obj, 18 | @Advice.AllArguments Object[] args) { 19 | Sensor sensor = Sensor.getSensor(sensorIndex); 20 | try { 21 | sensor.enterMethodScope(methodIndex); 22 | if (sensor.inOutermostMethodScope(methodIndex)) { 23 | String sig = className + "." + methodName + "()"; 24 | StackTraceElement[] trace = Thread.currentThread().getStackTrace(); 25 | sensor.execute("MVNEAdvice", sig, obj, args, null, trace); 26 | } 27 | } catch (Throwable t) { 28 | //t.printStackTrace(); 29 | logger.atWarning().log( ExceptionUtils.getStackTrace(t) ); 30 | } 31 | } 32 | 33 | @Advice.OnMethodExit(onThrowable = Throwable.class) 34 | public static void exit(@SensorIndexValue(value = -1) int sensorIndex, 35 | @MethodIndexValue(value = -1) int methodIndex) { 36 | Sensor sensor = Sensor.getSensor(sensorIndex); 37 | sensor.leaveMethodScope(methodIndex); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/advice/ScopeAdvice.java: -------------------------------------------------------------------------------- 1 | package org.openolly.advice; 2 | 3 | import org.openolly.MethodIndexValue; 4 | import org.openolly.Sensor; 5 | import org.openolly.SensorIndexValue; 6 | 7 | import net.bytebuddy.asm.Advice; 8 | 9 | public class ScopeAdvice { 10 | 11 | @Advice.OnMethodEnter() 12 | public static void enter(@SensorIndexValue(value = -1) int sensorIndex, @MethodIndexValue(value = -1) int scopeIndex) { 13 | Sensor.enterScopeScope(sensorIndex, scopeIndex); 14 | } 15 | 16 | // FIXME: Must use onThrowable here to ensure we leave scope in exceptional cases 17 | // BUT IT'S BREAKING IN CONSTRUCTORS 18 | @Advice.OnMethodExit() 19 | public static void exit(@SensorIndexValue(value = -1) int sensorIndex, @MethodIndexValue(value = -1) int scopeIndex) { 20 | Sensor.leaveScopeScope(sensorIndex, scopeIndex); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/advice/SensorException.java: -------------------------------------------------------------------------------- 1 | package org.openolly.advice; 2 | 3 | public class SensorException extends RuntimeException { 4 | 5 | public SensorException(String msg) { 6 | super( msg ); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/advice/TraceAdvice.java: -------------------------------------------------------------------------------- 1 | package org.openolly.advice; 2 | 3 | import org.openolly.advice.scope.BinaryScope; 4 | import org.openolly.reporting.Trace; 5 | 6 | import net.bytebuddy.asm.Advice; 7 | 8 | public class TraceAdvice { 9 | 10 | public static BinaryScope scope = new BinaryScope(); 11 | 12 | @Advice.OnMethodEnter() 13 | public static void enter() { 14 | scope.enterScope(); 15 | if ( scope.inOutermostScope() ) { 16 | Trace.getCurrentTrace().start(); 17 | } 18 | } 19 | 20 | // Must use onThrowable here to ensure we close the trace in exceptional cases 21 | @Advice.OnMethodExit(onThrowable = Throwable.class) 22 | public static void exit() { 23 | try { 24 | if ( scope.inOutermostScope() ) { 25 | Trace.getCurrentTrace().stop(); 26 | } 27 | } finally { 28 | scope.leaveScope(); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/advice/scope/BinaryScope.java: -------------------------------------------------------------------------------- 1 | package org.openolly.advice.scope; 2 | 3 | public final class BinaryScope { 4 | 5 | private final ThreadLocal counters = new ThreadLocal() { 6 | @Override 7 | protected Counter initialValue() { 8 | return new Counter(); 9 | } 10 | }; 11 | 12 | public boolean inScope() { 13 | return counters.get().value != 0; 14 | } 15 | 16 | public boolean inOutermostScope() { 17 | return counters.get().value == 1; 18 | } 19 | 20 | public boolean inNestedSensor() { 21 | return counters.get().value > 1; 22 | } 23 | 24 | public int value() { 25 | return counters.get().value; 26 | } 27 | 28 | public void enterScope() { 29 | counters.get().value++; 30 | } 31 | 32 | public void leaveScope() { 33 | counters.get().value--; 34 | } 35 | 36 | public void reset() { 37 | counters.get().value = 0; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return String.valueOf(value()); 43 | } 44 | 45 | private static final class Counter { 46 | private int value; 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/java/org/openolly/config/ConfigMonitor.java: -------------------------------------------------------------------------------- 1 | package org.openolly.config; 2 | 3 | import java.util.Timer; 4 | import java.util.TimerTask; 5 | 6 | import com.google.common.flogger.FluentLogger; 7 | 8 | public class ConfigMonitor extends TimerTask { 9 | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 10 | public void start() { 11 | // FIXME: look into WatchService 12 | new Timer().scheduleAtFixedRate( this, 10*1000, 10*1000); 13 | //System.out.println( "[SENSOR] Monitoring yaml configuration for changes" ); 14 | logger.atInfo().log("[SENSOR] Monitoring yaml configuration for changes" ); 15 | } 16 | 17 | public void run() { 18 | // try { 19 | // // FIXME - this won't work until retransformation is enabled 20 | // if ( ConfigReader.isChanged() ) { 21 | // ConfigReader.load(); 22 | // } 23 | // } catch( Exception e ) { 24 | // System.out.println( "Error checking yaml... continuing with old config" ); 25 | // e.printStackTrace(); 26 | // } 27 | 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/config/ConfigReader.java: -------------------------------------------------------------------------------- 1 | package org.openolly.config; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import org.openolly.Sensor; 9 | import org.openolly.reporting.Report; 10 | 11 | import com.fasterxml.jackson.databind.DeserializationFeature; 12 | import com.fasterxml.jackson.databind.JsonMappingException; 13 | import com.fasterxml.jackson.databind.ObjectMapper; 14 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; 15 | import com.google.common.flogger.FluentLogger; 16 | 17 | 18 | public class ConfigReader { 19 | 20 | private static File yaml = null; 21 | private static long lastModified = 0; 22 | 23 | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 24 | 25 | public static boolean isChanged() { 26 | return lastModified != yaml.lastModified(); 27 | } 28 | 29 | public static void init(String filename) throws IOException { 30 | if ( filename == null ) { 31 | //System.err.println( "[JOT] Error: no JOT file or directory provided"); 32 | logger.atSevere().log( "[JOT] Error: no JOT file or directory provided"); 33 | //System.err.println( "[JOT] Try using -javaagent:jot-x.x.jar=JOTFILE" ); 34 | logger.atSevere().log( "[JOT] Try using -javaagent:jot-x.x.jar=JOTFILE" ); 35 | return; 36 | } 37 | //System.err.println( "[JOT] Loading JOTs from " + filename ); 38 | logger.atWarning().log("[JOT] Loading JOTs from " + filename ); 39 | yaml = new File( filename ); 40 | if ( yaml.isDirectory() ) { 41 | for ( File f : yaml.listFiles() ) { 42 | //System.out.println( "Loading JOTs from " + f ); 43 | logger.atInfo().log("Loading JOTs from " + f ); 44 | load( f ); 45 | } 46 | } 47 | else { 48 | load( yaml ); 49 | } 50 | } 51 | 52 | 53 | // FIXME - sensors are added to the Sensor class, Reports are tracked in Reporter 54 | public static void load( File yaml ) throws IOException { 55 | ConfigReader.yaml = yaml; 56 | lastModified = yaml.lastModified(); 57 | 58 | // allow fields to be missing 59 | ObjectMapper mapper = new ObjectMapper(new YAMLFactory()) 60 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 61 | 62 | YAML config = mapper.readValue(yaml, YAML.class); 63 | 64 | if ( config.sensors != null ) { 65 | for ( YAMLSensor entry : config.sensors ) { 66 | if ( Sensor.exists( entry.name ) ) { 67 | //System.err.println( "[JOT] WARNING: " + entry.name + " from " + yaml + " already loaded. Skipping." ); 68 | logger.atWarning().log("[JOT] WARNING: " + entry.name + " from " + yaml + " already loaded. Skipping." ); 69 | } 70 | 71 | else if ( entry.name.equals("example")) { 72 | //System.err.println( "[JOT] WARNING: Found 'example' JOT in " + yaml + ". Skipping." ); 73 | logger.atWarning().log("[JOT] WARNING: Found 'example' JOT in " + yaml + ". Skipping." ); 74 | } 75 | 76 | else { 77 | new Sensor() 78 | .name( entry.name ) 79 | .description( entry.description ) 80 | .methods( entry.methods ) 81 | .scopes( entry.scopes ) 82 | .excludes( entry.excludes ) 83 | .captures( entry.captures ) 84 | .matchers( entry.matchers ) 85 | .exception( entry.exception ) 86 | .debug( entry.debug ); 87 | } 88 | } 89 | } 90 | 91 | if ( config.reports != null ) { 92 | for ( YAMLReport entry : config.reports ) { 93 | if ( !entry.name.contentEquals( "example" )) { 94 | new Report() 95 | .name( entry.name ) 96 | .type( entry.type ) 97 | .rows( entry.rows ) 98 | .cols( entry.cols ) 99 | .data( entry.data ); 100 | } 101 | } 102 | } 103 | } 104 | 105 | private static class YAML { 106 | public YAML() {} 107 | public List sensors = null; 108 | public List reports = null; 109 | } 110 | 111 | private static class YAMLSensor { 112 | public String name = null; 113 | public String description = null; 114 | public List methods = new ArrayList(); 115 | public List scopes = new ArrayList(); 116 | public List excludes = new ArrayList(); 117 | public List captures = new ArrayList(); 118 | public List matchers = new ArrayList(); 119 | public String exception = null; 120 | public boolean debug = false; 121 | } 122 | 123 | private static class YAMLReport { 124 | public String name = null; 125 | public String type = null; 126 | public String rows = null; 127 | public String cols = null; 128 | public String data = null; 129 | } 130 | 131 | 132 | public static void write( List list) throws Exception, JsonMappingException, IOException { 133 | ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); 134 | mapper.writeValue(System.out, list); 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/config/GraphSerializer.java: -------------------------------------------------------------------------------- 1 | package org.openolly.config; 2 | 3 | import java.io.IOException; 4 | import java.util.Set; 5 | 6 | import com.google.gson.Gson; 7 | import com.google.gson.GsonBuilder; 8 | 9 | import org.openolly.reporting.Event; 10 | 11 | public class GraphSerializer { 12 | 13 | public static String serialize( Set events ) throws IOException { 14 | Gson gson = new GsonBuilder().setPrettyPrinting().create(); 15 | String jsonString = gson.toJson(events); 16 | return jsonString; 17 | } 18 | 19 | public static Object deserialize( String jsonString ) throws IOException { 20 | return null; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/reporting/Event.java: -------------------------------------------------------------------------------- 1 | package org.openolly.reporting; 2 | 3 | public class Event implements Comparable { 4 | 5 | private String rule = null; 6 | private String capture = null; 7 | private StackTraceElement[] stack = null; 8 | private int id = 0; 9 | private static int counter = 0; 10 | 11 | public Event(String rule, String capture, StackTraceElement[] stack ) { 12 | this.rule = rule; 13 | this.capture = capture; 14 | this.stack = stack; 15 | this.id = counter++; 16 | } 17 | 18 | public String getRule() { 19 | return rule; 20 | } 21 | 22 | public String getCapture() { 23 | return capture; 24 | } 25 | 26 | public String getCaller( int depth ) { 27 | return stack[depth].toString(); 28 | } 29 | 30 | public StackTraceElement[] getStack() { 31 | return stack; 32 | } 33 | 34 | // FIXME: return the "name" of a name-value pair captured. Needs update. 35 | public String getNameFromCapture() { 36 | if ( capture.indexOf( ':' ) != -1 ) { 37 | return capture.substring( 0, capture.indexOf(':')); 38 | } else { 39 | return capture; 40 | } 41 | } 42 | 43 | // FIXME: return the "name" of a name-value pair captured. Needs update. 44 | public String getValueFromCapture() { 45 | if ( capture.indexOf( ':' ) != -1 ) { 46 | return capture.substring( capture.indexOf(':') + 1); 47 | } else { 48 | return "X"; 49 | } 50 | } 51 | 52 | public String toString() { 53 | return "[JOT " + rule + "] " + getCaller(2) + " " + getCapture(); 54 | } 55 | 56 | public String getHash() { 57 | return ""+id; 58 | } 59 | 60 | @Override 61 | public int hashCode() { 62 | final int prime = 31; 63 | int result = 1; 64 | result = prime * result + rule.hashCode() ; 65 | result = prime * result + capture.hashCode() ; 66 | return result; 67 | } 68 | 69 | @Override 70 | public boolean equals(Object obj) { 71 | if (obj == null) 72 | return false; 73 | if (this == obj) 74 | return true; 75 | 76 | if (getClass() != obj.getClass()) 77 | return false; 78 | Event other = (Event) obj; 79 | return (rule == other.rule && capture == other.capture ); 80 | } 81 | 82 | @Override 83 | public int compareTo(Object obj) { 84 | if (obj == null) 85 | return 0; 86 | if (this == obj) 87 | return 0; 88 | if (getClass() != obj.getClass()) 89 | return 0; 90 | Event other = (Event) obj; 91 | return (rule + capture).compareTo( other.rule + other.capture ); 92 | } 93 | 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/reporting/Report.java: -------------------------------------------------------------------------------- 1 | package org.openolly.reporting; 2 | 3 | import java.text.SimpleDateFormat; 4 | import java.util.Collection; 5 | import java.util.Date; 6 | import java.util.Set; 7 | import java.util.TreeSet; 8 | 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.apache.commons.lang3.exception.ExceptionUtils; 11 | 12 | import com.google.common.collect.TreeBasedTable; 13 | import com.google.common.flogger.FluentLogger; 14 | 15 | public class Report { 16 | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 17 | 18 | private int rowNameColWidth = 10; 19 | 20 | private TreeBasedTable> table = TreeBasedTable.create(); 21 | private ReportStyle mode = null; 22 | private String name = null; 23 | private String rows = null; 24 | private String[] cols = null; 25 | private String data = null; 26 | 27 | public enum ReportStyle { 28 | LIST, COMPARE, TABLE, SERIES 29 | } 30 | 31 | public Report() { 32 | Reporter.add( this ); 33 | } 34 | 35 | public void update( Trace t ) { 36 | try { 37 | switch( mode ) { 38 | case LIST: updateList( t ); break; 39 | case COMPARE: updateCompare( t ); break; 40 | case TABLE: updateTable( t ); break; 41 | case SERIES: updateSeries( t ); break; 42 | } 43 | }catch( Exception e ) { 44 | //e.printStackTrace(); 45 | logger.atWarning().log( ExceptionUtils.getStackTrace(e) ); 46 | } 47 | } 48 | 49 | private void updateList(Trace t) { 50 | for( Event colEvent : t.getEventsForRule( cols[0] ) ) { 51 | String rowName = colEvent.getCaller(2); 52 | String colName = cols[0]; 53 | String data = colEvent.getCapture(); 54 | String shortened = StringUtils.abbreviateMiddle(data, "...", 50 ); 55 | set( rowName, colName, shortened ); 56 | } 57 | } 58 | 59 | private void updateCompare(Trace t) { 60 | for (Event rowEvent : t.getEventsForRule( rows ) ) { 61 | String rowName = rowEvent.getNameFromCapture(); 62 | for( Event colEvent : t.getEventsForRule( cols[0] ) ) { 63 | String colName = colEvent.getNameFromCapture(); 64 | String data = colEvent.getValueFromCapture(); // FIXME: get rid of getValue() use getCapture() 65 | String shortened = StringUtils.abbreviateMiddle(data, "...", 50 ); 66 | set( rowName, colName, shortened ); 67 | } 68 | } 69 | } 70 | 71 | private void updateTable(Trace t) { 72 | for (Event rowEvent : t.getEventsForRule( rows ) ) { 73 | String r = rowEvent.getNameFromCapture(); 74 | for ( String col : cols ) { 75 | for (Event event : t.getEventsForRule( col ) ) { 76 | String data = event.getCapture(); 77 | String shortened = StringUtils.abbreviateMiddle(data, "...", 50 ); 78 | set( r, col, shortened ); 79 | } 80 | } 81 | } 82 | } 83 | 84 | 85 | private final static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 86 | private static int counter = 10000; 87 | 88 | // FIXME: Should date be in Events, or just added here in Table? 89 | private void updateSeries(Trace t) { 90 | String time = dateFormat.format(new Date()); 91 | for (Event rowEvent : t.getEventsForRule( rows ) ) { 92 | String r = rowEvent.getNameFromCapture(); 93 | for ( String col : cols ) { 94 | String[] parts = col.split(":"); 95 | int frame = -1; 96 | if ( parts.length > 1 ) { 97 | col = parts[0]; 98 | frame = Integer.parseInt( parts[1] ); 99 | } 100 | for ( Event event : t.getEventsForRule( col ) ) { 101 | StackTraceElement[] stack = event.getStack(); 102 | String caller = ""; 103 | if ( frame != -1 ) { 104 | caller = stack[frame].toString(); 105 | } 106 | String row = "" + counter++; 107 | String item = event.getCapture(); 108 | // set( row, " Time", time ); // extra space so this will sort first 109 | set( row, rows, r ); 110 | set( row, "Method Call", caller ); 111 | set( row, col, item ); 112 | } 113 | } 114 | } 115 | } 116 | 117 | 118 | private void dumpStack(StackTraceElement[] stack) { 119 | String out = ""; 120 | for ( StackTraceElement frame : stack ) { 121 | //System.out.println( " " + frame ); 122 | out += " " + frame +"\n"; 123 | } 124 | logger.atInfo().log(out); 125 | } 126 | 127 | //==================================== 128 | 129 | public Report name( String value ) { 130 | this.name = value; 131 | rowNameColWidth = Math.max( rowNameColWidth, name.length() + 4 ); 132 | return this; 133 | } 134 | 135 | public Report type( String value ) { 136 | this.mode = ReportStyle.valueOf( value.toUpperCase() ); 137 | return this; 138 | } 139 | 140 | 141 | // FIXME: check to be sure value is a legit rule name 142 | 143 | public Report rows( String value ) { 144 | this.rows = value; 145 | return this; 146 | } 147 | 148 | // FIXME: check to be sure value is a legit rule name 149 | 150 | public Report cols( String value ) { 151 | this.cols = value.split(", "); 152 | return this; 153 | } 154 | 155 | // FIXME: check to be sure value is a legit rule name 156 | 157 | public Report data( String value ) { 158 | this.data = value; 159 | return this; 160 | } 161 | 162 | //==================================== 163 | 164 | public void set( String r, String c, Collection values ) { 165 | for ( String v : values ) { 166 | set( r, c, v ); 167 | } 168 | } 169 | 170 | public Set get( String r, String c ) { 171 | return table.get(r, c); 172 | } 173 | 174 | public void set( String r, String c, String v ) { 175 | if ( r != null && !r.isEmpty() && 176 | c != null && !c.isEmpty() && 177 | v != null && !v.isEmpty() ) { 178 | Set current = table.get( r, c ); 179 | if ( current == null ) { 180 | current = new TreeSet(); 181 | table.put( r, c, current); 182 | } 183 | current.add( v ); 184 | rowNameColWidth = Math.max( rowNameColWidth, r.length() ); 185 | } else { 186 | //System.err.println( "Failed attempt to set " + r + ", " + c + ", " + v ); 187 | logger.atWarning().log("Failed attempt to set " + r + ", " + c + ", " + v ); 188 | } 189 | } 190 | 191 | public void clear( String r, String c ) { 192 | table.remove( r, c ); 193 | } 194 | 195 | public void addRow( String r ) { 196 | if ( r != null && !r.isEmpty() ) { 197 | set( r, "|||", "X" ); 198 | } 199 | } 200 | 201 | 202 | public void addCol( String c ) { 203 | if ( c != null && !c.isEmpty() ) { 204 | set( "|||", c, "X" ); 205 | } 206 | } 207 | 208 | public void clearRow( String r ) { 209 | for( String c : table.columnKeySet() ) { 210 | table.remove( r, c ); 211 | } 212 | } 213 | 214 | public void clearCol( String c ) { 215 | for ( String r : table.rowKeySet() ) { 216 | table.remove( r, c ); 217 | } 218 | } 219 | 220 | public synchronized void dump() { 221 | 222 | String out = "\n"; 223 | // print headers 224 | //System.err.print( StringUtils.rightPad(name, rowNameColWidth) ); 225 | out += StringUtils.rightPad(name, rowNameColWidth) ; 226 | //System.err.print( " " ); 227 | out += " "; 228 | for ( String col : table.columnKeySet() ) { 229 | if ( !col.contentEquals( "|||" ) ) { 230 | int colWidth = getMaxWidth( col ); 231 | //System.err.print( StringUtils.rightPad(col, colWidth, " ") ); 232 | out += StringUtils.rightPad(col, colWidth, " "); 233 | //System.err.print( " " ); 234 | out += " "; 235 | } 236 | } 237 | 238 | // print ---- 239 | //System.err.println(); 240 | out += "\n"; 241 | String under1 = StringUtils.repeat( "-", rowNameColWidth ); 242 | //System.err.print( under1 ); 243 | out += under1; 244 | //System.err.print( " " ); 245 | out += " "; 246 | for ( String col : table.columnKeySet() ) { 247 | if ( !col.equals("|||" ) ) { 248 | int colWidth = getMaxWidth( col ); 249 | String under2 = StringUtils.repeat( "-", colWidth ); 250 | //System.err.print( under2 ); 251 | out += under2; 252 | //System.err.print( " " ); 253 | out += " "; 254 | } 255 | } 256 | //System.err.println(); 257 | out += "\n"; 258 | 259 | // print data 260 | for ( String row : table.rowKeySet() ) { 261 | if ( !row.equals( "|||" ) ) { 262 | //System.err.print( StringUtils.rightPad(row,rowNameColWidth," " ).substring(0,rowNameColWidth)); 263 | out += StringUtils.rightPad(row,rowNameColWidth," " ).substring(0,rowNameColWidth); 264 | //System.err.print( " " ); 265 | out += " "; 266 | for ( String col : table.columnKeySet() ) { 267 | if ( !col.contentEquals( "|||" ) ) { 268 | int colWidth = getMaxWidth( col ); 269 | Set values = table.get(row,col); 270 | String value = ""; 271 | if ( values != null ) { 272 | value = String.join(",", values ); 273 | } 274 | //System.err.print( StringUtils.rightPad(value,colWidth," " ).substring(0,colWidth)); 275 | out += StringUtils.rightPad(value,colWidth," " ).substring(0,colWidth); 276 | //System.err.print( " " ); 277 | out += " "; 278 | } 279 | } 280 | //System.err.println(); 281 | out += "\n"; 282 | } 283 | } 284 | //System.err.println(); 285 | out+= "\n"; 286 | logger.atWarning().log(out); 287 | } 288 | 289 | private int getMaxWidth( String col ) { 290 | int width = 4; 291 | if ( col.length() > width ) { 292 | width = col.length(); 293 | } 294 | for ( String row : table.rowKeySet() ) { 295 | Set values = table.get(row, col); 296 | if ( values != null ) { 297 | String val = String.join(",", values ); 298 | if ( val != null && val.length() > width ) { 299 | width = val.length(); 300 | } 301 | } 302 | } 303 | return width; 304 | } 305 | 306 | public String toString() { 307 | return name + " (" + rows + "," + cols + "," +data + ")"; 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/reporting/Reporter.java: -------------------------------------------------------------------------------- 1 | package org.openolly.reporting; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.apache.commons.lang3.exception.ExceptionUtils; 7 | 8 | import com.google.common.flogger.FluentLogger; 9 | 10 | public class Reporter { 11 | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 12 | 13 | private static List reports = new ArrayList(); 14 | 15 | public Reporter() { 16 | // new Timer().scheduleAtFixedRate( this, 10*1000, 5*1000); 17 | // System.out.println( "Reporting thread started" ); 18 | } 19 | 20 | public static void update(Trace t) { 21 | for ( Report table : reports ) { 22 | try { 23 | table.update( t ); 24 | table.dump(); 25 | } catch( Exception e ) { 26 | //e.printStackTrace(); 27 | logger.atWarning().log( ExceptionUtils.getStackTrace(e) ); 28 | } 29 | } 30 | } 31 | 32 | public static void add(Report table) { 33 | reports.add( table ); 34 | } 35 | 36 | public static List getReports() { 37 | return reports; 38 | } 39 | 40 | 41 | } -------------------------------------------------------------------------------- /src/main/java/org/openolly/reporting/RouteGraph.java: -------------------------------------------------------------------------------- 1 | package org.openolly.reporting; 2 | 3 | import java.util.List; 4 | import java.util.Set; 5 | import java.util.TreeSet; 6 | 7 | import com.google.common.flogger.FluentLogger; 8 | import com.google.common.graph.EndpointPair; 9 | import com.google.common.graph.Graph; 10 | import com.google.common.graph.GraphBuilder; 11 | import com.google.common.graph.Graphs; 12 | import com.google.common.graph.MutableGraph; 13 | 14 | import org.apache.commons.lang3.exception.ExceptionUtils; 15 | import org.apache.commons.text.StringEscapeUtils; 16 | 17 | // ... 18 | 19 | public class RouteGraph { 20 | 21 | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 22 | 23 | private static MutableGraph graph = GraphBuilder.directed().allowsSelfLoops(true).build(); 24 | 25 | public static void update(Trace t) { 26 | try { 27 | List events = t.getEventsForRule( "route" ); 28 | if ( events.isEmpty() ) { 29 | return; 30 | } 31 | Event route = events.get(0); 32 | if ( route.getCapture().endsWith(".js") || route.getCapture().endsWith(".css") 33 | || route.getCapture().endsWith( ".jpg") || route.getCapture().endsWith( ".png" ) 34 | || route.getCapture().endsWith( ".gif" ) ) { 35 | return; 36 | } 37 | route = findExistingRoute( route ); 38 | graph.addNode( route ); 39 | 40 | Event tail = route; 41 | for ( Event event : t.getEvents() ) { 42 | if ( !event.getRule().equals( "route" ) ) { 43 | event = findExistingEventInRoute( route, event ); 44 | graph.putEdge( tail, event ); 45 | tail = event; 46 | if ( event.getRule().equals( "forward" ) ) { 47 | route = event; 48 | } 49 | } 50 | } 51 | dump( graph ); 52 | } catch( Exception e ) { 53 | //e.printStackTrace(); 54 | logger.atWarning().log( ExceptionUtils.getStackTrace(e) ); 55 | } 56 | } 57 | 58 | // Return existing node for this route event if it's already in graph 59 | public static Event findExistingRoute( Event add ) { 60 | for ( Event e : graph.nodes() ) { 61 | if ( e.getRule().equals( add.getRule() ) && e.getCapture().equals( add.getCapture() ) ) { 62 | return e; 63 | } 64 | } 65 | return add; 66 | } 67 | 68 | // Return existing event if it's already in the subgraph for this route 69 | public static Event findExistingEventInRoute( Event route, Event add ) { 70 | Set t = Graphs.reachableNodes(graph, route); 71 | for ( Event e : t ) { 72 | if ( e.getRule().equals( add.getRule() ) && e.getCapture().equals( add.getCapture() ) ) { 73 | return e; 74 | } 75 | } 76 | return add; 77 | } 78 | 79 | // FIXME: HTML Encode specials 80 | private static String style = "shape=box, style=\"rounded,filled\",fillcolor=blue,fontcolor=white,fontname=Helvetica,fontsize=10"; 81 | public static void dump( Graph g ) { 82 | String out = ""; 83 | //System.err.println( "====" ); 84 | out += "===="+"\n"; 85 | //System.err.println( "digraph {" ); 86 | out += "digraph {" +"\n"; 87 | // System.err.println( "\trankdir=LR" ); 88 | if ( !graph.nodes().isEmpty() ) { 89 | //System.err.println( "\t{ node ["+style+"]"); 90 | out += "\t{ node ["+style+"]"+"\n"; 91 | for ( Event e : graph.nodes() ) { 92 | /*System.err.println( "\t\t" + e.getHash() + " [label=<"+ 93 | ""+ 94 | ""+ 95 | ""+ 96 | "
"+StringEscapeUtils.escapeHtml4( e.getRule() )+"
"+StringEscapeUtils.escapeHtml4( e.getCapture() )+"
"+StringEscapeUtils.escapeHtml4( e.getCaller(2) )+"
>];" );*/ 97 | out += "\t\t" + e.getHash() + " [label=<"+ 98 | ""+ 99 | ""+ 100 | ""+ 101 | "
"+StringEscapeUtils.escapeHtml4( e.getRule() )+"
"+StringEscapeUtils.escapeHtml4( e.getCapture() )+"
"+StringEscapeUtils.escapeHtml4( e.getCaller(2) )+"
>];" +"\n"; 102 | } 103 | //System.err.println( "\t}" ); 104 | out += "\t\n"; 105 | } 106 | if ( !graph.edges().isEmpty() ) { 107 | //System.err.println( "\t{ edge [style=dashed]"); 108 | out+= "\t{ edge [style=dashed]"+"\n"; 109 | for ( EndpointPair edge : graph.edges() ) { 110 | //System.err.println( "\t\t" + edge.source().getHash() + " -> " + edge.target().getHash() ); 111 | out+= "\t\t" + edge.source().getHash() + " -> " + edge.target().getHash()+"\n"; 112 | } 113 | //System.err.println( "\t}" ); 114 | out += "\t\n"; 115 | } 116 | //System.err.println( "}" ); 117 | out += "\"}\"\n"; 118 | //System.err.println( "====" ); 119 | out += "===="+"\n"; 120 | logger.atWarning().log(out); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/org/openolly/reporting/Trace.java: -------------------------------------------------------------------------------- 1 | package org.openolly.reporting; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.concurrent.atomic.AtomicInteger; 6 | 7 | import com.google.common.flogger.FluentLogger; 8 | 9 | public class Trace { 10 | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 11 | 12 | private static AtomicInteger counter = new AtomicInteger(10000); 13 | 14 | private static final ThreadLocal currentTrace = 15 | new ThreadLocal() { 16 | @Override protected Trace initialValue() { 17 | return new Trace(); 18 | } 19 | }; 20 | 21 | private final int traceId = counter.getAndIncrement(); 22 | private List events = new ArrayList(); 23 | 24 | public Trace() { 25 | } 26 | 27 | public static Trace getCurrentTrace() { 28 | return currentTrace.get(); 29 | } 30 | 31 | public int getId() { 32 | return traceId; 33 | } 34 | 35 | public void addEvent(Event event) { 36 | events.add( event ); 37 | } 38 | 39 | public void start() { 40 | events.clear(); 41 | } 42 | 43 | public void stop() { 44 | if ( events.isEmpty() ) return; 45 | printTrace(); 46 | Reporter.update( this ); 47 | RouteGraph.update( this ); 48 | } 49 | 50 | public void printTrace() { 51 | String out = ""; 52 | if ( events.isEmpty() ) return; 53 | //System.out.println( "\n"+toString()); 54 | out += "\n"+toString(); 55 | for ( Event event : events ) { 56 | //System.out.println( " " + event ); 57 | out += " " + event+"\n"; 58 | } 59 | //System.out.println(); 60 | out += "\n"; 61 | logger.atInfo().log(out); 62 | } 63 | 64 | public List getEvents() { 65 | return events; 66 | } 67 | 68 | public List getEventsForRule(String rulename) { 69 | List list = new ArrayList(); 70 | for( Event e : events ) { 71 | if ( e.getRule().equalsIgnoreCase(rulename) ) { 72 | list.add(e); 73 | } 74 | } 75 | return list; 76 | } 77 | 78 | public String toString() { 79 | return "TRACE-" + traceId + "(" + events.size() +")"; 80 | } 81 | 82 | public int getSize() { 83 | return events.size(); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/main/resources/access.jot: -------------------------------------------------------------------------------- 1 | --- 2 | # Java Observability Toolkit (JOT) 3 | 4 | sensors: 5 | 6 | - name: "get-routes" 7 | description: "Identifies the route for this HTTP request" 8 | methods: 9 | - "javax.servlet.Servlet.service" 10 | captures: 11 | - "#P0.getRequestURI()" 12 | 13 | - name: "get-users" 14 | description: "Identifies user names" 15 | methods: 16 | - "javax.servlet.Servlet.service" 17 | captures: 18 | - "#P0.getRemoteUser() ?: \"Guest\"" 19 | 20 | - name: "get-role" 21 | description: "Identifies roles" 22 | methods: 23 | - "javax.servlet.ServletRequest.isUserInRole" 24 | captures: 25 | - "#P0" 26 | 27 | 28 | reports: 29 | 30 | - name: "Test Coverage Matrix" 31 | type: "compare" 32 | rows: "get-routes" 33 | cols: "get-users" 34 | 35 | - name: "Access Control Matrix" 36 | type: "compare" 37 | rows: "get-routes" 38 | cols: "get-role" 39 | 40 | -------------------------------------------------------------------------------- /src/main/resources/ciphers.jot: -------------------------------------------------------------------------------- 1 | --- 2 | # Java Observability Toolkit (JOT) 3 | 4 | sensors: 5 | - name: "get-ciphers" 6 | description: "Identifies encryption ciphers" 7 | methods: 8 | - "javax.crypto.Cipher.getInstance" 9 | captures: 10 | - "#P0" 11 | 12 | reports: 13 | - name: "Encryption Usage" 14 | type: "list" 15 | cols: "get-ciphers" 16 | -------------------------------------------------------------------------------- /src/main/resources/cmdi.jot: -------------------------------------------------------------------------------- 1 | --- 2 | # Java Observability Toolkit (JOT) 3 | 4 | sensors: 5 | 6 | - name: "get-routes" 7 | description: "Identifies the route for this HTTP request" 8 | methods: 9 | - "javax.servlet.Servlet.service" 10 | captures: 11 | - "#P0.getRequestURI()" 12 | 13 | - name: "block-native-process" 14 | description: "Blocks attempts to start native processes" 15 | methods: 16 | - "java.lang.ProcessBuilder." 17 | scopes: 18 | - "javax.servlet.Servlet.service" 19 | captures: 20 | - "#ARGS" 21 | exception: "Attempt to create ProcessBuilder from within Servlet.service() prevented 'block-native-process' JOT sensor" 22 | 23 | 24 | reports: 25 | 26 | - name: "CMDi" 27 | type: "series" 28 | rows: "get-routes" 29 | cols: "block-native-process:13" 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/resources/core.jot: -------------------------------------------------------------------------------- 1 | --- 2 | # Java Observability Toolkit (JOT) 3 | 4 | sensors: 5 | 6 | - name: "get-ciphers" 7 | description: "Identifies encryption ciphers" 8 | methods: 9 | - "javax.crypto.Cipher.getInstance" 10 | captures: 11 | - "#P0" 12 | matchers: 13 | - "!null" 14 | 15 | 16 | - name: "native-libraries" 17 | description: "Identifies the use of native libraries" 18 | methods: 19 | - "java.lang.System.load" 20 | - "java.lang.System.loadLibrary" 21 | - "java.lang.System.mapLibraryName" 22 | captures: 23 | - "#P0" 24 | matchers: 25 | - "!null" 26 | 27 | 28 | - name: "get-unsafe-queries" 29 | description: "Identifies unparameterized database queries" 30 | methods: 31 | - "java.sql.Statement.execute" 32 | - "java.sql.Statement.addBatch" 33 | - "java.sql.Statement.executeQuery" 34 | - "java.sql.Statement.executeUpdate" 35 | excludes: 36 | - "java.sql.PreparedStatement" #these calls are harmless in PreparedStatement 37 | captures: 38 | - "#ARGS" 39 | 40 | 41 | - name: "get-safe-queries" 42 | description: "Identifies parameterized database queries" 43 | methods: 44 | - "java.sql.Connection.prepareCall" 45 | - "java.sql.Connection.prepareStatement" 46 | captures: 47 | - "#ARGS" 48 | 49 | 50 | - name: "get-files" 51 | description: "Identifies the use of Files outside certain paths" 52 | methods: 53 | - "java.io.File." 54 | captures: 55 | - "#OBJ.getCanonicalPath()" 56 | matchers: 57 | - "!\\/apache\\-tomcat" 58 | - "!sdkman" 59 | - "!eclipse\\-workspace" 60 | 61 | 62 | - name: "get-sockets" 63 | description: "Identifies the use of sockets" 64 | methods: 65 | - "java.net.Socket." 66 | captures: 67 | - "\"\"+#P0+\":\"+#P1" # using ""+#P0 is a shorthand String conversion 68 | debug: "true" 69 | matchers: 70 | - "!null\\:null" 71 | - "!\\:8080" # any port that's not 8080 72 | 73 | 74 | - name: "get-exceptions" 75 | description: "Gathers information from all exceptions whether they are caught or not" 76 | methods: 77 | - "Exception." 78 | scopes: 79 | - "javax.servlet.Servlet.service" 80 | captures: 81 | - "#P0" 82 | 83 | -------------------------------------------------------------------------------- /src/main/resources/eli.jot: -------------------------------------------------------------------------------- 1 | --- 2 | # Java Observability Toolkit (JOT) 3 | 4 | sensors: 5 | 6 | - name: "get-routes" 7 | description: "Identifies the route for this HTTP request" 8 | methods: 9 | - "javax.servlet.Servlet.service" 10 | captures: 11 | - "#P0.getRequestURI()" 12 | 13 | - name: "sandbox-expressions" 14 | description: "Prevents harmful methods from being used during expresssion evaluation" 15 | methods: 16 | - "java.lang.ProcessBuilder." 17 | - "java.io.Socket." 18 | scopes: 19 | - "javax.el.ValueExpression.getValue" 20 | captures: 21 | - "#P0" 22 | exception: "Attempt to escape expression language sandbox prevented by JST rule 'sandbox-expressions'" 23 | 24 | reports: 25 | 26 | - name: "Expression Language Injection Attempt Log" 27 | type: "series" 28 | rows: "get-routes" 29 | cols: "sandbox-expressions:13" 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/resources/jee.jot: -------------------------------------------------------------------------------- 1 | --- 2 | # Java Observability Toolkit (JOT) 3 | 4 | sensors: 5 | 6 | - name: "get-routes" 7 | description: "Identifies the route for this HTTP request" 8 | methods: 9 | - "javax.servlet.Servlet.service" 10 | captures: 11 | - "#P0.getRequestURI()" 12 | 13 | 14 | - name: "get-users" 15 | description: "Identifies user names" 16 | methods: 17 | - "javax.servlet.Servlet.service" 18 | captures: 19 | - "#P0.getRemoteUser()==null ? \"Guest\" : #P0.getRemoteUser()" 20 | 21 | 22 | - name: "get-role" 23 | description: "Identifies roles" 24 | methods: 25 | - "javax.servlet.ServletRequest.isUserInRole" 26 | captures: 27 | - "#P0" 28 | 29 | 30 | - name: "get-role-membership" 31 | description: "Figures out which users have which roles" 32 | methods: 33 | - "javax.servlet.ServletRequest.isUserInRole" 34 | captures: 35 | - "#RET ? #P0 : \"null\"" 36 | matchers: 37 | - "!null" 38 | debug: "true" 39 | 40 | 41 | - name: "get-received-param-names" 42 | description: "Identifies parameter names used in HTTP requests" 43 | methods: 44 | - "javax.servlet.Servlet.service" 45 | captures: 46 | - "#P0.getParameterNames()" 47 | matchers: 48 | - "!null" 49 | 50 | 51 | - name: "get-accessed-param-names" 52 | description: "Identifies parameter names referenced by the app" 53 | methods: 54 | - "javax.servlet.ServletRequest.getParameter" 55 | - "javax.servlet.ServletRequest.getParameterValues" 56 | captures: 57 | - "#P0" 58 | matchers: 59 | - "!null" 60 | 61 | 62 | - name: "get-redirects" 63 | description: "Extracts any response redirects in this app" 64 | methods: 65 | - "javax.servlet.ServletResponse.sendRedirect" 66 | captures: 67 | - "#P0" 68 | matchers: 69 | - "!null" 70 | 71 | 72 | - name: "get-forwards" 73 | description: "Extracts any request forwards in this app" 74 | methods: 75 | - "javax.servlet.ServletRequest.getRequestDispatcher" 76 | - "javax.servlet.ServletContext.getRequestDispatcher" 77 | captures: 78 | - "#P0" 79 | matchers: 80 | - "!null" 81 | 82 | 83 | - name: "get-filters" 84 | description: Extracts the filters in this app" 85 | methods: 86 | - "javax.servlet.ServletContext.addFilter" 87 | captures: 88 | - "#RET.getUrlPatternMappings()" 89 | matchers: 90 | - "!null" 91 | 92 | 93 | - name: "get-servlets" 94 | description: "Identifies the servlets in this app" 95 | methods: 96 | - "javax.servlet.ServletContext.addServlet" 97 | captures: 98 | - "#RET.getMappings()" 99 | matchers: 100 | - "!null" 101 | 102 | 103 | - name: "get-cache-header" 104 | description: "Identifies whether cache header is set" 105 | methods: 106 | - "javax.servlet.ServletResponse.setHeader" 107 | captures: 108 | - "#P0.equalsIgnoreCase(\"Cache-Control\")?#P1:null" 109 | matchers: 110 | - "!null" 111 | 112 | 113 | - name: "get-privacy" 114 | description: "Identifies PII creation" 115 | methods: 116 | - "javax.servlet.ServletRequest.getParameter" 117 | - "javax.servlet.ServletRequest.getParameterValues" 118 | scopes: 119 | - "javax.servlet.Servlet.service" 120 | captures: 121 | - "#P0" 122 | matchers: 123 | - "(?!000|666)[0-8][0-9]{2}-(?!00)[0-9]{2}-(?!0000)[0-9]{4}" 124 | - "(?4[0-9]{12}(?:[0-9]{3})?)" 125 | - "(?5[1-5][0-9]{14})" 126 | - "(?6(?:011|5[0-9]{2})[0-9]{12})" 127 | - "(?3[47][0-9]{13})" 128 | - "(?3(?:0[0-5]|[68][0-9])?[0-9]{11})" 129 | - "(?(?:2131|1800|35[0-9]{3})[0-9]{11})" 130 | 131 | 132 | ##### detect attacks ##### 133 | 134 | - name: "block-nasty-expressions" 135 | description: "Blocks expresssions containing dangerous patterns from being evaluated" 136 | methods: 137 | - "javax.servlet.jsp.el.ExpressionEvaluator.evaluate" 138 | - "org.springframework.expression.parseExpression" 139 | captures: 140 | - "#P0" 141 | matchers: 142 | - "\\.class" 143 | exception: "Attack detected. Go away or I'm calling the FBI." 144 | 145 | 146 | - name: "sandbox-expressions" 147 | description: "Prevents harmful methods from being used during expresssion evaluation" 148 | methods: 149 | - "java.lang.ProcessBuilder." 150 | - "java.io.Socket." 151 | scopes: 152 | - "javax.servlet.jsp.el.ExpressionEvaluator.evaluate" 153 | - "org.springframework.expression.parseExpression" 154 | captures: 155 | - "#P0" 156 | exception: "Attack detected. Go away or I'm calling the FBI." 157 | 158 | -------------------------------------------------------------------------------- /src/main/resources/reports.jot: -------------------------------------------------------------------------------- 1 | --- 2 | # Java Observability Toolkit (JOT) 3 | 4 | 5 | reports: 6 | 7 | - name: "Test Coverage Matrix" 8 | type: "compare" 9 | rows: "get-routes" 10 | cols: "get-users" 11 | 12 | - name: "Access Control Matrix" 13 | type: "compare" 14 | rows: "get-routes" 15 | cols: "get-role" 16 | 17 | - name: "Role Membership" 18 | type: "compare" 19 | rows: "get-users" 20 | cols: "get-role-membership" 21 | 22 | - name: "Cache Headers" 23 | type: "table" 24 | rows: "get-routes" 25 | cols: "get-cache-header" 26 | 27 | - name: "Encryption Algorithms" 28 | type: "compare" 29 | rows: "get-routes" 30 | cols: "get-ciphers" 31 | 32 | - name: "Unauthorized Files" 33 | type: "table" 34 | rows: "get-routes" 35 | cols: "get-files" 36 | 37 | - name: "Database Queries" 38 | type: "table" 39 | rows: "get-routes" 40 | cols: "get-unsafe-queries" 41 | 42 | - name: "Encryption Users" 43 | type: "compare" 44 | rows: "get-users" 45 | cols: "get-ciphers" 46 | 47 | - name: "HTTP Parameters" 48 | type: "table" 49 | rows: "get-routes" 50 | cols: "get-accessed-param-names, get-received-param-names" 51 | 52 | - name: "Request Forwards" 53 | type: "table" 54 | rows: "get-routes" 55 | cols: "get-forwards" 56 | 57 | - name: "Timeseries" 58 | type: "series" 59 | rows: "unused" 60 | cols: "get-ciphers" 61 | 62 | - name: "Exceptions" 63 | type: "table" 64 | rows: "get-routes" 65 | cols: "get-exceptions" 66 | 67 | -------------------------------------------------------------------------------- /src/main/resources/sqli.jot: -------------------------------------------------------------------------------- 1 | --- 2 | # Java Observability Toolkit (JOT) 3 | 4 | sensors: 5 | 6 | - name: "get-routes" 7 | description: "Identifies the route for this HTTP request" 8 | methods: 9 | - "javax.servlet.Servlet.service" 10 | captures: 11 | - "#P0.getRequestURI()" 12 | 13 | - name: "get-unsafe-queries" 14 | description: "Identifies unparameterized database queries" 15 | methods: 16 | - "java.sql.Statement.execute" 17 | - "java.sql.Statement.addBatch" 18 | - "java.sql.Statement.executeQuery" 19 | - "java.sql.Statement.executeUpdate" 20 | excludes: 21 | - "java.sql.PreparedStatement" # not vulnerable subclass 22 | captures: 23 | - "#ARGS" 24 | 25 | reports: 26 | 27 | - name: "SQLi" 28 | type: "series" 29 | rows: "get-routes" 30 | cols: "get-unsafe-queries:2" 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/main/resources/ticketbook.jot: -------------------------------------------------------------------------------- 1 | --- 2 | # Java Observability Toolkit (JOT) 3 | 4 | sensors: 5 | 6 | - name: "get-ciphers" 7 | description: "Identifies encryption ciphers" 8 | methods: 9 | - "javax.crypto.Cipher.getInstance" 10 | captures: 11 | - "#P0" 12 | matchers: 13 | - "!null" 14 | 15 | 16 | - name: "native-libraries" 17 | description: "Identifies the use of native libraries" 18 | methods: 19 | - "java.lang.System.load" 20 | - "java.lang.System.loadLibrary" 21 | - "java.lang.System.mapLibraryName" 22 | captures: 23 | - "#P0" 24 | matchers: 25 | - "!null" 26 | 27 | 28 | - name: "get-unsafe-queries" 29 | description: "Identifies unparameterized database queries" 30 | methods: 31 | - "java.sql.Statement.execute" 32 | - "java.sql.Statement.addBatch" 33 | - "java.sql.Statement.executeQuery" 34 | - "java.sql.Statement.executeUpdate" 35 | excludes: 36 | - "java.sql.PreparedStatement" #these calls are harmless in PreparedStatement 37 | captures: 38 | - "#ARGS" 39 | 40 | 41 | - name: "get-files" 42 | description: "Identifies the use of Files outside certain paths" 43 | methods: 44 | - "java.io.File." 45 | captures: 46 | - "#OBJ.getCanonicalPath()" 47 | matchers: 48 | - "!\\/apache\\-tomcat" 49 | - "!sdkman" 50 | - "!eclipse\\-workspace" 51 | 52 | 53 | - name: "get-sockets" 54 | description: "Identifies the use of sockets" 55 | methods: 56 | - "java.net.Socket." 57 | captures: 58 | - "\"\"+#P0+\":\"+#P1" # using ""+#P0 is a shorthand String conversion 59 | debug: "true" 60 | matchers: 61 | - "!null\\:null" 62 | - "!\\:8080" # any port that's not 8080 63 | 64 | 65 | - name: "route" 66 | description: "Identifies the route for this HTTP request" 67 | methods: 68 | - "javax.servlet.Servlet.service" 69 | captures: 70 | - "#P0.getRequestURI()" 71 | 72 | 73 | - name: "check-role" 74 | description: "Identifies roles" 75 | methods: 76 | - "javax.servlet.ServletRequest.isUserInRole" 77 | captures: 78 | - "#P0" 79 | 80 | 81 | - name: "get-parameter" 82 | description: "Identifies parameter names referenced by the app" 83 | methods: 84 | - "javax.servlet.ServletRequest.getParameter" 85 | - "javax.servlet.ServletRequest.getParameterValues" 86 | - "javax.servlet.ServletRequest.getQueryString" 87 | captures: 88 | - "#P0 + \":\" + #RET" 89 | matchers: 90 | - "!null" 91 | 92 | - name: "get-header" 93 | description: "Identifies parameter names referenced by the app" 94 | methods: 95 | - "javax.servlet.ServletRequest.getHeader" 96 | - "javax.servlet.ServletRequest.getHeaders" 97 | captures: 98 | - "#P0 + \":\" + #RET" 99 | matchers: 100 | - "!null" 101 | 102 | - name: "get-header" 103 | description: "Identifies parameter names referenced by the app" 104 | methods: 105 | - "javax.servlet.ServletRequest.getCookies" 106 | captures: 107 | - "#P0 + \":\" + #RET" 108 | matchers: 109 | - "!null" 110 | 111 | - name: "multipart" 112 | description: "Identifies parameter names referenced by the app" 113 | methods: 114 | - "javax.servlet.ServletRequest.getPart" 115 | - "javax.servlet.ServletRequest.getParts" 116 | captures: 117 | - "#P0" 118 | matchers: 119 | - "!null" 120 | 121 | 122 | - name: "send-redirect" 123 | description: "Extracts any response redirects in this app" 124 | methods: 125 | - "javax.servlet.ServletResponse.sendRedirect" 126 | captures: 127 | - "#P0" 128 | matchers: 129 | - "!null" 130 | 131 | 132 | - name: "request-forward" 133 | description: "Extracts any request forwards in this app" 134 | methods: 135 | - "javax.servlet.ServletRequest.getRequestDispatcher" 136 | - "javax.servlet.ServletContext.getRequestDispatcher" 137 | captures: 138 | - "#P0" 139 | matchers: 140 | - "!null" 141 | 142 | 143 | - name: "set-header" 144 | description: "Identifies whether headers are being set" 145 | methods: 146 | - "javax.servlet.ServletResponse.setHeader" 147 | captures: 148 | - "#P0 + \":\" + #P1" 149 | matchers: 150 | - "!null" 151 | 152 | 153 | - name: "credit-card" 154 | description: "Identifies PII creation" 155 | methods: 156 | - "javax.servlet.ServletRequest.getParameter" 157 | - "javax.servlet.ServletRequest.getParameterValues" 158 | scopes: 159 | - "javax.servlet.Servlet.service" 160 | captures: 161 | - "#P0" 162 | matchers: 163 | - "(?!000|666)[0-8][0-9]{2}-(?!00)[0-9]{2}-(?!0000)[0-9]{4}" 164 | - "(?4[0-9]{12}(?:[0-9]{3})?)" 165 | - "(?5[1-5][0-9]{14})" 166 | - "(?6(?:011|5[0-9]{2})[0-9]{12})" 167 | - "(?3[47][0-9]{13})" 168 | - "(?3(?:0[0-5]|[68][0-9])?[0-9]{11})" 169 | - "(?(?:2131|1800|35[0-9]{3})[0-9]{11})" 170 | 171 | - name: "native-process" 172 | description: "Detects attempts to start native processes" 173 | methods: 174 | - "java.lang.ProcessBuilder." 175 | scopes: 176 | - "javax.servlet.Servlet.service" 177 | captures: 178 | - "#ARGS" 179 | 180 | 181 | - name: "expression-language" 182 | description: "Detects harmful methods from being used during expresssion evaluation" 183 | methods: 184 | - "javax.el.ValueExpression.getValue" 185 | scopes: 186 | - "javax.servlet.Servlet.service" 187 | captures: 188 | - "#OBJ.getExpressionString()" 189 | 190 | -------------------------------------------------------------------------------- /src/test/java/org/openolly/AllTests.java: -------------------------------------------------------------------------------- 1 | package org.openolly; 2 | 3 | import org.junit.platform.runner.JUnitPlatform; 4 | import org.junit.platform.suite.api.SelectPackages; 5 | import org.junit.runner.RunWith; 6 | 7 | 8 | @RunWith(JUnitPlatform.class) 9 | @SelectPackages("org.openolly") 10 | 11 | public class AllTests 12 | { 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/org/openolly/ConfigReaderTest.java: -------------------------------------------------------------------------------- 1 | package org.openolly; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.Test; 8 | import org.openolly.config.ConfigReader; 9 | import org.openolly.reporting.Reporter; 10 | 11 | class ConfigReaderTest { 12 | 13 | @Test 14 | void readerTest() throws IOException { 15 | File ruleDir = new File("src/test/resources/rules");; 16 | ConfigReader.init( ruleDir.getAbsolutePath() ); 17 | //Assertions.assertEquals(22, Sensor.getSensors().size() ); 18 | //Assertions.assertEquals(14, Reporter.getReports().size() ); 19 | } 20 | 21 | 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/test/java/org/openolly/ExpressionTest.java: -------------------------------------------------------------------------------- 1 | package org.openolly; 2 | 3 | import java.util.Enumeration; 4 | import java.util.List; 5 | import java.util.Vector; 6 | 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | 10 | class ExpressionTest { 11 | 12 | @Test 13 | void testConvert() throws Exception { 14 | Vector v = new Vector(); 15 | v.add( "foo" ); 16 | v.add( "bar" ); 17 | Enumeration e = v.elements(); 18 | Expression expr = new Expression("#P0"); 19 | Object o = expr.convert( e ); 20 | Assertions.assertTrue( List.class.isAssignableFrom( o.getClass() ) ); 21 | Assertions.assertEquals( 2, ((List)o).size() ); 22 | } 23 | 24 | @Test 25 | void testNull() throws Exception { 26 | Expression expr = new Expression("#P0"); 27 | Object o = expr.convert( null ); 28 | Assertions.assertEquals( null, o ); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/org/openolly/GraphSerializerTest.java: -------------------------------------------------------------------------------- 1 | package org.openolly; 2 | 3 | import java.util.Enumeration; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.Set; 7 | import java.util.TreeSet; 8 | import java.util.Vector; 9 | 10 | import com.google.common.graph.Graph; 11 | import com.google.common.graph.GraphBuilder; 12 | import com.google.common.graph.MutableGraph; 13 | 14 | import org.junit.jupiter.api.Assertions; 15 | import org.junit.jupiter.api.Test; 16 | import org.openolly.config.GraphSerializer; 17 | import org.openolly.reporting.Event; 18 | import org.openolly.reporting.RouteGraph; 19 | 20 | class GraphSerializerTest { 21 | 22 | @Test 23 | void testSerialize() { 24 | try { 25 | MutableGraph g = GraphBuilder.directed().allowsSelfLoops(true).build(); 26 | Event a = new Event("a","capture",Thread.currentThread().getStackTrace()); 27 | Event b = new Event("b","capture",Thread.currentThread().getStackTrace()); 28 | Event c = new Event("c","capture",Thread.currentThread().getStackTrace()); 29 | Event d = new Event("d","capture",Thread.currentThread().getStackTrace()); 30 | g.putEdge(a,b); 31 | g.putEdge(a,c); 32 | g.putEdge(b,d); 33 | g.putEdge(c,d); 34 | System.err.println( ">>>" + g.edges() ); 35 | String jsonString = GraphSerializer.serialize( g.nodes() ); 36 | RouteGraph.dump(g); 37 | System.out.println( "==SERIALIZED=="); 38 | System.out.println( jsonString ); 39 | //Graph x = GraphSerializer.deserialize( jsonString ); 40 | } catch( Exception e ) { 41 | e.printStackTrace(); 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/org/openolly/SafeStringTest.java: -------------------------------------------------------------------------------- 1 | package org.openolly; 2 | 3 | import java.util.Enumeration; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.TreeSet; 7 | import java.util.Vector; 8 | 9 | import org.junit.jupiter.api.Assertions; 10 | import org.junit.jupiter.api.Test; 11 | 12 | class SafeStringTest { 13 | 14 | @Test 15 | void testNull() { 16 | Assertions.assertEquals( "null" , SafeString.format(null,false)); 17 | } 18 | 19 | @Test 20 | void testString() { 21 | Assertions.assertEquals( "foobar" , SafeString.format("foobar",false)); 22 | } 23 | 24 | @Test 25 | void testEntry() { 26 | Map map = new HashMap(); 27 | map.put("foo","bar"); 28 | Map.Entry entry = map.entrySet().iterator().next(); 29 | Assertions.assertEquals( "[foo=bar]" , SafeString.format(entry,false)); 30 | } 31 | 32 | @Test 33 | void testEnumeration() { 34 | Vector v = new Vector(); 35 | v.add( "foo" ); 36 | v.add( "bar" ); 37 | Enumeration e = v.elements(); 38 | Assertions.assertEquals( "[foo, bar]" , SafeString.format(e,false)); 39 | } 40 | 41 | @Test 42 | void testArray() { 43 | Object[] arr = {"foo","bar"}; 44 | Assertions.assertEquals( "[foo, bar]" , SafeString.format(arr,false)); 45 | } 46 | 47 | void testPrimitiveArray() { 48 | int[] arr = {1,2}; 49 | Assertions.assertEquals( "[1, 2]" , SafeString.format(arr,false)); 50 | } 51 | 52 | @Test 53 | void testDeepArray() { 54 | Object[] arr = {"foo","bar",new String[]{"blue","red"}}; 55 | Assertions.assertEquals( "[foo, bar, [blue, red]]" , SafeString.format(arr,false)); 56 | } 57 | 58 | @Test 59 | void testCollection() { 60 | TreeSet set = new TreeSet(); 61 | set.add( "foo" ); 62 | set.add( "bar" ); 63 | Assertions.assertEquals( "[bar, foo]" , SafeString.format(set,false)); 64 | } 65 | 66 | @Test 67 | void testMap() { 68 | Map map = new HashMap(); 69 | map.put("foo1","bar"); 70 | map.put("foo2","bar"); 71 | Assertions.assertEquals( "[[foo1=bar],[foo2=bar]]", SafeString.format(map,false)); 72 | } 73 | 74 | // 75 | // @BeforeAll 76 | // static void setup(){ 77 | // System.out.println("@BeforeAll executed"); 78 | // } 79 | // 80 | // @BeforeEach 81 | // void setupThis(){ 82 | // System.out.println("@BeforeEach executed"); 83 | // } 84 | // 85 | // @AfterEach 86 | // void tearThis(){ 87 | // System.out.println("@AfterEach executed"); 88 | // } 89 | // 90 | // @AfterAll 91 | // static void tear(){ 92 | // System.out.println("@AfterAll executed"); 93 | // } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/org/openolly/ScopeTest.java: -------------------------------------------------------------------------------- 1 | package org.openolly; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | import org.openolly.advice.scope.BinaryScope; 6 | 7 | public class ScopeTest { 8 | 9 | @Test 10 | public void testEnterScope() { 11 | BinaryScope scope1 = new BinaryScope(); 12 | scope1.enterScope(); 13 | Assertions.assertTrue( scope1.inScope() ); 14 | } 15 | 16 | private BinaryScope scope = new BinaryScope(); 17 | private int x = 0; 18 | 19 | public void two(int b) { 20 | if ( scope.inScope() ) return; 21 | scope.enterScope(); 22 | x = 11; 23 | scope.leaveScope(); 24 | } 25 | public void one(int b) { 26 | if ( scope.inScope() ) return; 27 | scope.enterScope(); 28 | x = 5; 29 | two(x); 30 | scope.leaveScope(); 31 | } 32 | 33 | @Test 34 | public void testInNestedSensor() { 35 | one(10); 36 | Assertions.assertEquals( 5, x ); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/org/openolly/SensorTest.java: -------------------------------------------------------------------------------- 1 | package org.openolly; 2 | 3 | import static org.junit.jupiter.api.Assertions.fail; 4 | 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Test; 7 | import org.openolly.advice.SensorException; 8 | 9 | class SensorTest { 10 | 11 | @Test 12 | void testExecute() { 13 | Sensor s = new Sensor().capture("#P0"); 14 | Object ret = s.execute("test", "a.b.Class.Method", "foo", new String[] { "bar" }, "zoo", 15 | new Exception().getStackTrace()); 16 | Assertions.assertEquals("[bar]", SafeString.format(ret, false)); 17 | } 18 | 19 | @Test 20 | void testExecuteExpression() { 21 | Sensor s = new Sensor().capture("#P0.toUpperCase()"); 22 | Object ret = s.execute("test", "a.b.Class.Method", "foo", new String[] { "bar" }, "zoo", 23 | new Exception().getStackTrace()); 24 | Assertions.assertEquals("[BAR]", SafeString.format(ret, false)); 25 | } 26 | 27 | @Test 28 | void testNoScope() { 29 | Sensor s = new Sensor().method("a.Class.method").capture("#P0").scope("s.Class.method"); 30 | Object ret = s.execute("test", "sig", "foo", new String[] { "bar" }, "zoo", new Exception().getStackTrace()); 31 | Assertions.assertEquals("[]", SafeString.format(ret, false)); 32 | } 33 | 34 | @Test 35 | void testInScope() { 36 | Sensor s = new Sensor().method("a.Class.method").capture("#P0").scope("s.Class.method"); 37 | 38 | s.getScopes().iterator().next().enterScope(); 39 | 40 | Object ret = s.execute("test", "a.b.Class.Method", "foo", new String[] { "bar" }, "zoo", 41 | new Exception().getStackTrace()); 42 | Assertions.assertEquals("[bar]", SafeString.format(ret, false)); 43 | } 44 | 45 | @Test 46 | void testNotInScope() { 47 | Sensor s = new Sensor().method("a.Class.method").capture("#P0").scope("s.Class.method"); 48 | 49 | Object ret = s.execute("test", "a.b.Class.Method", "foo", new String[] { "bar" }, "zoo", 50 | new Exception().getStackTrace()); 51 | Assertions.assertEquals("[]", SafeString.format(ret, false)); 52 | } 53 | 54 | @Test 55 | void testNegativeScope() { 56 | Sensor s = new Sensor().method("a.Class.method").capture("#P0").scope("!s.Class.method"); 57 | 58 | s.getScopes().iterator().next().enterScope(); 59 | 60 | Object ret = s.execute("test", "a.b.Class.Method", "foo", new String[] { "bar" }, "zoo", 61 | new Exception().getStackTrace()); 62 | Assertions.assertEquals("[]", SafeString.format(ret, false)); 63 | } 64 | 65 | @Test 66 | void testException() { 67 | Sensor s = new Sensor().method("a.b.Class.method").capture("#P0").exception("Exception"); 68 | 69 | try { 70 | Object ret = s.execute("test", "a.b.Class.Method", "foo", new String[] { "bar" }, "zoo", 71 | new Exception().getStackTrace()); 72 | } catch (SensorException e) { 73 | return; 74 | } 75 | fail("Did not throw SensorException"); 76 | } 77 | 78 | @Test 79 | void testMatches() { 80 | Sensor s = new Sensor().method("a.b.Class.method").capture("#P0").matcher("ba"); 81 | Object ret = s.execute("test", "a.b.Class.Method", "foo", new String[] { "bar" }, "zoo", 82 | new Exception().getStackTrace()); 83 | Assertions.assertEquals("[bar]", SafeString.format(ret, false)); 84 | } 85 | 86 | @Test 87 | void testNotMatches() { 88 | Sensor s = new Sensor().method("a.b.Class.method").capture("#P0").matcher("\\[fo"); 89 | Object ret = s.execute("test", "a.b.Class.Method", "foo", new String[] { "bar" }, "zoo", 90 | new Exception().getStackTrace()); 91 | Assertions.assertEquals("[]", SafeString.format(ret, false)); 92 | } 93 | 94 | @Test 95 | void testNegativeMatches() { 96 | Sensor s = new Sensor().method("a.b.Class.method").capture("#P0").matcher("!^\\[ca"); 97 | Object ret = s.execute("test", "a.b.Class.Method", "foo", new String[] { "bar" }, "zoo", 98 | new Exception().getStackTrace()); 99 | Assertions.assertEquals("[bar]", SafeString.format(ret, false)); 100 | } 101 | 102 | @Test 103 | void testMultiPositiveMatches() { 104 | Sensor s = new Sensor().method("a.b.Class.method").capture("#P0").matcher("bar").matcher("foo"); 105 | Object ret = s.execute("test", "a.b.Class.Method", "foo", new String[] { "bar" }, "zoo", 106 | new Exception().getStackTrace()); 107 | Assertions.assertEquals("[bar]", SafeString.format(ret, false)); 108 | } 109 | 110 | @Test 111 | void testMultiNegativeNonMatches() { 112 | Sensor s = new Sensor().method("a.b.Class.method").capture("#P0").matcher("!ca").matcher("!da").matcher("!ea"); 113 | Object ret = s.execute("test", "a.b.Class.Method", "obj", new String[] { "bar" }, "zoo", 114 | new Exception().getStackTrace()); 115 | Assertions.assertEquals("[bar]", SafeString.format(ret, false)); 116 | Object ret2 = s.execute("test", "a.b.Class.Method", "obj", new String[] { "acad" }, "zoo", 117 | new Exception().getStackTrace()); 118 | Assertions.assertEquals("[]", SafeString.format(ret2, false)); 119 | } 120 | 121 | @Test 122 | void testMultiNegativeYesMatches() { 123 | Sensor s = new Sensor().method("a.b.Class.method").capture("#P0").matcher("!ca").matcher("!da").matcher("!ba"); 124 | Object ret = s.execute("test", "a.b.Class.Method", "foo", new String[] { "bar" }, "zoo", 125 | new Exception().getStackTrace()); 126 | Assertions.assertEquals("[]", SafeString.format(ret, false)); 127 | } 128 | 129 | @Test 130 | void testMixedMatches() { 131 | Sensor s = new Sensor().method("a.b.Class.method").capture("#P0").matcher("ba").matcher("!ba"); 132 | Object ret = s.execute("test", "a.b.Class.Method", "foo", new String[] { "bar" }, "zoo", 133 | new Exception().getStackTrace()); 134 | Assertions.assertEquals("[]", SafeString.format(ret, false)); 135 | } 136 | 137 | @Test 138 | void testMiddleMatch() { 139 | Sensor s = new Sensor() 140 | .method("a.b.Class.method") 141 | .capture("#P0") 142 | .matcher("!\\/apache\\-tomcat"); 143 | Object ret = s.execute( "test", "a.b.Class.Method", "foo", new String[]{"/Users/jeffwilliams/git/apache-tomcat-8.5.43/lib/catalina.jar"}, "zoo", new Exception().getStackTrace() ); 144 | Assertions.assertEquals( "[]" , SafeString.format(ret, false)); 145 | } 146 | 147 | } 148 | 149 | -------------------------------------------------------------------------------- /src/test/java/org/openolly/TableTest.java: -------------------------------------------------------------------------------- 1 | package org.openolly; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | import org.openolly.reporting.Event; 6 | import org.openolly.reporting.Report; 7 | import org.openolly.reporting.Trace; 8 | 9 | class TableTest { 10 | 11 | @Test 12 | void testTable() { 13 | Trace trace = new Trace(); 14 | StackTraceElement[] stack = Thread.currentThread().getStackTrace(); 15 | trace.addEvent( new Event("rule1", "/route1", stack) ); 16 | trace.addEvent( new Event("rule2", "RoleA:value", stack) ); 17 | trace.addEvent( new Event("rule2", "RoleB:value", stack) ); 18 | trace.addEvent( new Event("rule2", "RoleD:value", stack) ); 19 | trace.addEvent( new Event("rule2", "RoleE:value", stack) ); 20 | System.out.println( "TRACE: " + trace ); 21 | Report t = new Report() 22 | .name("Test Report") 23 | .type("compare") 24 | .rows("rule1") 25 | .cols("rule2"); 26 | t.update( trace ); 27 | Assertions.assertEquals("[value]", t.get("/route1", "RoleA").toString() ); 28 | } 29 | 30 | @Test 31 | void testList() { 32 | Trace trace = new Trace(); 33 | StackTraceElement[] stack = Thread.currentThread().getStackTrace(); 34 | trace.addEvent( new Event("rule1", "/route1", stack) ); 35 | trace.addEvent( new Event("rule2", "DES", stack) ); 36 | trace.addEvent( new Event("rule2", "AES", stack) ); 37 | trace.addEvent( new Event("rule2", "CBC", stack) ); 38 | trace.addEvent( new Event("rule2", "MD5", stack) ); 39 | System.out.println( "TRACE: " + trace ); 40 | Report t = new Report() 41 | .name("Test Report") 42 | .type("list") 43 | .cols("rule2"); 44 | t.update( trace ); 45 | t.dump(); 46 | String caller = stack[2].toString(); 47 | Assertions.assertEquals("[AES, CBC, DES, MD5]", t.get(caller, "rule2").toString() ); 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /src/test/java/org/openolly/TraceTest.java: -------------------------------------------------------------------------------- 1 | package org.openolly; 2 | 3 | import java.security.SecureRandom; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | import org.openolly.reporting.Trace; 10 | 11 | class TraceTest { 12 | 13 | @Test 14 | void testTrace() throws Exception { 15 | final List list = new ArrayList(); 16 | // for ( int i=0; i<100; i++ ) { 17 | // Thread t = new Thread(new Runnable() { 18 | // @Override public void run() { 19 | // Event e = new Event("rule" + Thread.currentThread().getName(),"caller",Thread.currentThread().getName()); 20 | // Trace.getCurrentTrace().addEvent(e); 21 | // Trace.getCurrentTrace().addEvent(e); 22 | // Trace.getCurrentTrace().addEvent(e); 23 | // Trace.getCurrentTrace().printTrace(); 24 | // } 25 | // }); 26 | // t.start(); 27 | // list.add( Trace.getCurrentTrace().getSize()); 28 | // } 29 | // Thread.sleep(2000); 30 | // for( Integer i : list ) { 31 | // System.out.println( ">" + i ); 32 | // Assertions.assertEquals(3, i); 33 | // } 34 | } 35 | 36 | 37 | @Test 38 | void testTrace2() throws Exception { 39 | for ( int i=0; i<100; i++ ) { 40 | Thread t = new Thread(new Runnable() { 41 | SecureRandom rng = new SecureRandom(); 42 | @Override public void run() { 43 | int tid = Trace.getCurrentTrace().getId(); 44 | for (int b=0; b<10; b++ ) { 45 | try { 46 | Thread.sleep( rng.nextInt() * 100 ); 47 | Assertions.assertEquals(tid, Trace.getCurrentTrace().getId() ); 48 | } catch( Exception e ) {} 49 | } 50 | } 51 | }); 52 | t.start(); 53 | } 54 | } 55 | 56 | } 57 | 58 | -------------------------------------------------------------------------------- /src/test/resources/junit.jot: -------------------------------------------------------------------------------- 1 | --- 2 | # Java Observability Toolkit (JOT) 3 | 4 | sensors: 5 | 6 | - name: "banned-methods" 7 | description: "Fails any JUnit tests that cause banned methods to be invoked" 8 | methods: 9 | - "java.lang.ProcessBuilder." 10 | scopes: 11 | - "org.junit.platform.commons.util.ReflectionUtils.invokeMethod" 12 | exception: "Banned methods are prohibited by Security Directive 27B/6" 13 | -------------------------------------------------------------------------------- /src/test/resources/rules/core.jot: -------------------------------------------------------------------------------- 1 | --- 2 | # Java Observability Toolkit (JOT) 3 | # https://openo11y.org 4 | 5 | sensors: 6 | 7 | - name: "get-ciphers" 8 | description: "Identifies encryption ciphers" 9 | methods: 10 | - "javax.crypto.Cipher.getInstance" 11 | captures: 12 | - "#P0" 13 | matchers: 14 | - "!null" 15 | 16 | 17 | - name: "native-libraries" 18 | description: "Identifies the use of native libraries" 19 | methods: 20 | - "java.lang.System.load" 21 | - "java.lang.System.loadLibrary" 22 | - "java.lang.System.mapLibraryName" 23 | captures: 24 | - "#P0" 25 | matchers: 26 | - "!null" 27 | 28 | 29 | - name: "get-unsafe-queries" 30 | description: "Identifies unparameterized database queries" 31 | methods: 32 | - "java.sql.Statement.execute" 33 | - "java.sql.Statement.addBatch" 34 | - "java.sql.Statement.executeQuery" 35 | - "java.sql.Statement.executeUpdate" 36 | excludes: 37 | - "java.sql.PreparedStatement" #these calls are harmless in PreparedStatement 38 | captures: 39 | - "#ARGS" 40 | 41 | 42 | - name: "get-safe-queries" 43 | description: "Identifies parameterized database queries" 44 | methods: 45 | - "java.sql.Connection.prepareCall" 46 | - "java.sql.Connection.prepareStatement" 47 | captures: 48 | - "#ARGS" 49 | 50 | 51 | - name: "get-files" 52 | description: "Identifies the use of Files outside certain paths" 53 | methods: 54 | - "java.io.File." 55 | captures: 56 | - "#OBJ.getCanonicalPath()" 57 | matchers: 58 | - "!\\/apache\\-tomcat" 59 | - "!sdkman" 60 | - "!eclipse\\-workspace" 61 | 62 | 63 | - name: "get-sockets" 64 | description: "Identifies the use of sockets" 65 | methods: 66 | - "java.net.Socket." 67 | captures: 68 | - "\"\"+#P0+\":\"+#P1" # using ""+#P0 is a shorthand String conversion 69 | debug: "true" 70 | matchers: 71 | - "!null\\:null" 72 | - "!\\:8080" # any port that's not 8080 73 | 74 | 75 | - name: "get-exceptions" 76 | description: "Gathers information from all exceptions whether they are caught or not" 77 | methods: 78 | - "Exception." 79 | scopes: 80 | - "javax.servlet.Servlet.service" 81 | captures: 82 | - "#P0" 83 | 84 | -------------------------------------------------------------------------------- /src/test/resources/rules/jee.jot: -------------------------------------------------------------------------------- 1 | --- 2 | # Java Observability Toolkit (JOT) 3 | # https://openo11y.org 4 | 5 | sensors: 6 | 7 | - name: "get-routes" 8 | description: "Identifies the route for this HTTP request" 9 | methods: 10 | - "javax.servlet.Servlet.service" 11 | captures: 12 | - "#P0.getRequestURI()" 13 | 14 | 15 | - name: "get-users" 16 | description: "Identifies user names" 17 | methods: 18 | - "javax.servlet.Servlet.service" 19 | captures: 20 | - "#P0.getRemoteUser()==null ? \"Guest\" : #P0.getRemoteUser()" 21 | 22 | 23 | - name: "get-role" 24 | description: "Identifies roles" 25 | methods: 26 | - "javax.servlet.ServletRequest.isUserInRole" 27 | captures: 28 | - "#P0" 29 | 30 | 31 | - name: "get-role-membership" 32 | description: "Figures out which users have which roles" 33 | methods: 34 | - "javax.servlet.ServletRequest.isUserInRole" 35 | captures: 36 | - "#RET ? #P0 : \"null\"" 37 | matchers: 38 | - "!null" 39 | debug: "true" 40 | 41 | 42 | - name: "get-received-param-names" 43 | description: "Identifies parameter names used in HTTP requests" 44 | methods: 45 | - "javax.servlet.Servlet.service" 46 | captures: 47 | - "#P0.getParameterNames()" 48 | matchers: 49 | - "!null" 50 | 51 | 52 | - name: "get-accessed-param-names" 53 | description: "Identifies parameter names referenced by the app" 54 | methods: 55 | - "javax.servlet.ServletRequest.getParameter" 56 | - "javax.servlet.ServletRequest.getParameterValues" 57 | captures: 58 | - "#P0" 59 | matchers: 60 | - "!null" 61 | 62 | 63 | - name: "get-redirects" 64 | description: "Extracts any response redirects in this app" 65 | methods: 66 | - "javax.servlet.ServletResponse.sendRedirect" 67 | captures: 68 | - "#P0" 69 | matchers: 70 | - "!null" 71 | 72 | 73 | - name: "get-forwards" 74 | description: "Extracts any request forwards in this app" 75 | methods: 76 | - "javax.servlet.ServletRequest.getRequestDispatcher" 77 | - "javax.servlet.ServletContext.getRequestDispatcher" 78 | captures: 79 | - "#P0" 80 | matchers: 81 | - "!null" 82 | 83 | 84 | - name: "get-filters" 85 | description: Extracts the filters in this app" 86 | methods: 87 | - "javax.servlet.ServletContext.addFilter" 88 | captures: 89 | - "#RET.getUrlPatternMappings()" 90 | matchers: 91 | - "!null" 92 | 93 | 94 | - name: "get-servlets" 95 | description: "Identifies the servlets in this app" 96 | methods: 97 | - "javax.servlet.ServletContext.addServlet" 98 | captures: 99 | - "#RET.getMappings()" 100 | matchers: 101 | - "!null" 102 | 103 | 104 | - name: "get-cache-header" 105 | description: "Identifies whether cache header is set" 106 | methods: 107 | - "javax.servlet.ServletResponse.setHeader" 108 | captures: 109 | - "#P0.equalsIgnoreCase(\"Cache-Control\")?#P1:null" 110 | matchers: 111 | - "!null" 112 | 113 | 114 | - name: "get-privacy" 115 | description: "Identifies PII creation" 116 | methods: 117 | - "javax.servlet.ServletRequest.getParameter" 118 | - "javax.servlet.ServletRequest.getParameterValues" 119 | scopes: 120 | - "javax.servlet.Servlet.service" 121 | captures: 122 | - "#P0" 123 | matchers: 124 | - "(?!000|666)[0-8][0-9]{2}-(?!00)[0-9]{2}-(?!0000)[0-9]{4}" 125 | - "(?4[0-9]{12}(?:[0-9]{3})?)" 126 | - "(?5[1-5][0-9]{14})" 127 | - "(?6(?:011|5[0-9]{2})[0-9]{12})" 128 | - "(?3[47][0-9]{13})" 129 | - "(?3(?:0[0-5]|[68][0-9])?[0-9]{11})" 130 | - "(?(?:2131|1800|35[0-9]{3})[0-9]{11})" 131 | 132 | 133 | ##### detect attacks ##### 134 | 135 | - name: "block-nasty-expressions" 136 | description: "Blocks expresssions containing dangerous patterns from being evaluated" 137 | methods: 138 | - "javax.servlet.jsp.el.ExpressionEvaluator.evaluate" 139 | - "org.springframework.expression.parseExpression" 140 | captures: 141 | - "#P0" 142 | matchers: 143 | - "\\.class" 144 | exception: "Attack detected. Go away or I'm calling the FBI." 145 | 146 | 147 | - name: "sandbox-expressions" 148 | description: "Prevents harmful methods from being used during expresssion evaluation" 149 | methods: 150 | - "java.lang.ProcessBuilder." 151 | - "java.io.Socket." 152 | scopes: 153 | - "javax.servlet.jsp.el.ExpressionEvaluator.evaluate" 154 | - "org.springframework.expression.parseExpression" 155 | captures: 156 | - "#P0" 157 | exception: "Attack detected. Go away or I'm calling the FBI." 158 | 159 | -------------------------------------------------------------------------------- /src/test/resources/rules/reports.jot: -------------------------------------------------------------------------------- 1 | --- 2 | # Java Observability Toolkit (JOT) 3 | # https://openo11y.org 4 | 5 | 6 | reports: 7 | 8 | - name: "Test Coverage Matrix" 9 | type: "compare" 10 | rows: "get-routes" 11 | cols: "get-users" 12 | 13 | - name: "Access Control Matrix" 14 | type: "compare" 15 | rows: "get-routes" 16 | cols: "get-role" 17 | 18 | - name: "Role Membership" 19 | type: "compare" 20 | rows: "get-users" 21 | cols: "get-role-membership" 22 | 23 | - name: "Cache Headers" 24 | type: "table" 25 | rows: "get-routes" 26 | cols: "get-cache-header" 27 | 28 | - name: "Encryption Algorithms" 29 | type: "compare" 30 | rows: "get-routes" 31 | cols: "get-ciphers" 32 | 33 | - name: "Unauthorized Files" 34 | type: "table" 35 | rows: "get-routes" 36 | cols: "get-files" 37 | 38 | - name: "Database Queries" 39 | type: "table" 40 | rows: "get-routes" 41 | cols: "get-unsafe-queries" 42 | 43 | - name: "Encryption Users" 44 | type: "compare" 45 | rows: "get-users" 46 | cols: "get-ciphers" 47 | 48 | - name: "HTTP Parameters" 49 | type: "table" 50 | rows: "get-routes" 51 | cols: "get-accessed-param-names, get-received-param-names" 52 | 53 | - name: "Request Forwards" 54 | type: "table" 55 | rows: "get-routes" 56 | cols: "get-forwards" 57 | 58 | - name: "Timeseries" 59 | type: "series" 60 | rows: "unused" 61 | cols: "get-ciphers" 62 | 63 | - name: "Exceptions" 64 | type: "table" 65 | rows: "get-routes" 66 | cols: "get-exceptions" 67 | 68 | -------------------------------------------------------------------------------- /src/test/resources/testrule.jot: -------------------------------------------------------------------------------- 1 | - name: "get-files" 2 | description: "Identifies the use of Files" 3 | methods: 4 | - "java.io.File." 5 | captures: 6 | - "#OBJ.getCanonicalPath() + \" \" + ( #OBJ.exists() ? \"EXISTS\" : \"DOES NOT EXIST\" )" 7 | 8 | 9 | --------------------------------------------------------------------------------