├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── blank.yml │ ├── build.yml │ ├── codeql.yml │ ├── release.yml │ └── run-ui-tests.yml ├── .gitignore ├── .idea └── gradle.xml ├── .run ├── Run IDE for UI Tests.run.xml ├── Run Plugin.run.xml ├── Run Qodana.run.xml ├── Run Tests.run.xml └── Run Verifications.run.xml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── build.gradle.kts ├── docs ├── settings.png └── vuln_tree.png ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── qodana.yml ├── settings.gradle.kts └── src ├── main ├── kotlin │ └── com │ │ └── github │ │ └── adrienpessu │ │ └── sarifviewer │ │ ├── MyBundle.kt │ │ ├── actions │ │ ├── OpenLocalAction.kt │ │ └── RefreshAction.kt │ │ ├── configurable │ │ ├── SettingComponent.kt │ │ ├── Settings.kt │ │ └── SettingsState.kt │ │ ├── exception │ │ └── SarifViewerException.kt │ │ ├── listeners │ │ └── SarifViewerActivationListener.kt │ │ ├── models │ │ ├── BranchItemComboBox.kt │ │ ├── Leaf.kt │ │ ├── Root.kt │ │ ├── Tool.kt │ │ └── View.kt │ │ ├── services │ │ └── SarifService.kt │ │ ├── toolWindow │ │ ├── MyCustomInlayRenderer.kt │ │ └── SarifViewerWindowFactory.kt │ │ └── utils │ │ └── GitHubInstance.kt └── resources │ ├── META-INF │ └── plugin.xml │ ├── com.github.adrienpessu.sarifviewer │ ├── alert-fill.svg │ └── file-directory.svg │ └── messages │ └── MyBundle.properties └── test ├── kotlin └── com │ └── github │ └── adrienpessu │ └── sarifviewer │ ├── MyPluginTest.kt │ └── util │ └── GithubInstanceTest.kt └── testData ├── rename ├── foo.xml └── foo_after.xml └── result.sarif /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This project is maintained with love by: 2 | 3 | - @adrienpessu -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration: 2 | # https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | # Maintain dependencies for Gradle dependencies 7 | - package-ecosystem: "gradle" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | # Maintain dependencies for GitHub Actions 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | -------------------------------------------------------------------------------- /.github/workflows/blank.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 11 | jobs: 12 | # This workflow contains a single job called "build" 13 | build: 14 | # The type of runner that the job will run on 15 | runs-on: ubuntu-latest 16 | 17 | # Steps represent a sequence of tasks that will be executed as part of the job 18 | steps: 19 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 20 | - uses: actions/checkout@v4 21 | 22 | # Runs a set of commands using the runners shell 23 | - name: Run a multi-line script 24 | run: | 25 | gem install github-linguist 26 | echo Add other actions to build, 27 | github-linguist 28 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow is created for testing and preparing the plugin release in the following steps: 2 | # - Validate Gradle Wrapper. 3 | # - Run 'test' and 'verifyPlugin' tasks. 4 | # - Run Qodana inspections. 5 | # - Run the 'buildPlugin' task and prepare artifact for further tests. 6 | # - Run the 'runPluginVerifier' task. 7 | # - Create a draft release. 8 | # 9 | # The workflow is triggered on push and pull_request events. 10 | # 11 | # GitHub Actions reference: https://help.github.com/en/actions 12 | # 13 | ## JBIJPPTPL 14 | 15 | name: Build 16 | on: 17 | # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g., for dependabot pull requests) 18 | push: 19 | branches: [ main ] 20 | # Trigger the workflow on any pull request 21 | pull_request: 22 | 23 | jobs: 24 | 25 | # Prepare environment and build the plugin 26 | build: 27 | name: Build 28 | runs-on: ubuntu-latest 29 | outputs: 30 | version: ${{ steps.properties.outputs.version }} 31 | changelog: ${{ steps.properties.outputs.changelog }} 32 | pluginVerifierHomeDir: ${{ steps.properties.outputs.pluginVerifierHomeDir }} 33 | steps: 34 | 35 | # Check out current repository 36 | - name: Fetch Sources 37 | uses: actions/checkout@v4 38 | 39 | # Validate wrapper 40 | - name: Gradle Wrapper Validation 41 | uses: gradle/wrapper-validation-action@v3.4.2 42 | 43 | # Set up Java environment for the next steps 44 | - name: Setup Java 45 | uses: actions/setup-java@v4 46 | with: 47 | distribution: zulu 48 | java-version: 17 49 | 50 | # Setup Gradle 51 | - name: Setup Gradle 52 | uses: gradle/gradle-build-action@v3.4.2 53 | with: 54 | gradle-home-cache-cleanup: true 55 | 56 | # Set environment variables 57 | - name: Export Properties 58 | id: properties 59 | shell: bash 60 | run: | 61 | PROPERTIES="$(./gradlew properties --console=plain -q)" 62 | VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" 63 | CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" 64 | 65 | echo "version=$VERSION" >> $GITHUB_OUTPUT 66 | echo "pluginVerifierHomeDir=~/.pluginVerifier" >> $GITHUB_OUTPUT 67 | 68 | echo "changelog<> $GITHUB_OUTPUT 69 | echo "$CHANGELOG" >> $GITHUB_OUTPUT 70 | echo "EOF" >> $GITHUB_OUTPUT 71 | 72 | ./gradlew listProductsReleases # prepare list of IDEs for Plugin Verifier 73 | 74 | # Build plugin 75 | - name: Build plugin 76 | run: ./gradlew buildPlugin 77 | 78 | # Prepare plugin archive content for creating artifact 79 | - name: Prepare Plugin Artifact 80 | id: artifact 81 | shell: bash 82 | run: | 83 | cd ${{ github.workspace }}/build/distributions 84 | FILENAME=`ls *.zip` 85 | unzip "$FILENAME" -d content 86 | 87 | echo "filename=${FILENAME:0:-4}" >> $GITHUB_OUTPUT 88 | 89 | # Store already-built plugin as an artifact for downloading 90 | - name: Upload artifact 91 | uses: actions/upload-artifact@v4 92 | with: 93 | name: ${{ steps.artifact.outputs.filename }} 94 | path: ./build/distributions/content/*/* 95 | 96 | # Run tests and upload a code coverage report 97 | test: 98 | name: Test 99 | needs: [ build ] 100 | runs-on: ubuntu-latest 101 | steps: 102 | 103 | # Check out current repository 104 | - name: Fetch Sources 105 | uses: actions/checkout@v4 106 | 107 | # Set up Java environment for the next steps 108 | - name: Setup Java 109 | uses: actions/setup-java@v4 110 | with: 111 | distribution: zulu 112 | java-version: 17 113 | 114 | # Setup Gradle 115 | - name: Setup Gradle 116 | uses: gradle/gradle-build-action@v3.4.2 117 | with: 118 | gradle-home-cache-cleanup: true 119 | 120 | # Run tests 121 | - name: Run Tests 122 | run: ./gradlew check 123 | 124 | # Collect Tests Result of failed tests 125 | - name: Collect Tests Result 126 | if: ${{ failure() }} 127 | uses: actions/upload-artifact@v4 128 | with: 129 | name: tests-result 130 | path: ${{ github.workspace }}/build/reports/tests 131 | 132 | # Upload the Kover report to CodeCov 133 | - name: Upload Code Coverage Report 134 | uses: codecov/codecov-action@v5 135 | with: 136 | files: ${{ github.workspace }}/build/reports/kover/report.xml 137 | 138 | # Run Qodana inspections and provide report 139 | inspectCode: 140 | name: Inspect code 141 | needs: [ build ] 142 | runs-on: ubuntu-latest 143 | permissions: 144 | contents: write 145 | checks: write 146 | pull-requests: write 147 | steps: 148 | 149 | # Free GitHub Actions Environment Disk Space 150 | - name: Maximize Build Space 151 | uses: jlumbroso/free-disk-space@main 152 | with: 153 | tool-cache: false 154 | large-packages: false 155 | 156 | # Check out current repository 157 | - name: Fetch Sources 158 | uses: actions/checkout@v4 159 | 160 | # Set up Java environment for the next steps 161 | - name: Setup Java 162 | uses: actions/setup-java@v4 163 | with: 164 | distribution: zulu 165 | java-version: 17 166 | 167 | # Run Qodana inspections 168 | - name: Qodana - Code Inspection 169 | uses: JetBrains/qodana-action@v2025.1.1 170 | with: 171 | cache-default-branch-only: true 172 | 173 | # Run plugin structure verification along with IntelliJ Plugin Verifier 174 | verify: 175 | name: Verify plugin 176 | needs: [ build ] 177 | runs-on: ubuntu-latest 178 | steps: 179 | 180 | # Free GitHub Actions Environment Disk Space 181 | - name: Maximize Build Space 182 | uses: jlumbroso/free-disk-space@main 183 | with: 184 | tool-cache: false 185 | large-packages: false 186 | 187 | # Check out current repository 188 | - name: Fetch Sources 189 | uses: actions/checkout@v4 190 | 191 | # Set up Java environment for the next steps 192 | - name: Setup Java 193 | uses: actions/setup-java@v4 194 | with: 195 | distribution: zulu 196 | java-version: 17 197 | 198 | # Setup Gradle 199 | - name: Setup Gradle 200 | uses: gradle/gradle-build-action@v3.4.2 201 | with: 202 | gradle-home-cache-cleanup: true 203 | 204 | # Cache Plugin Verifier IDEs 205 | - name: Setup Plugin Verifier IDEs Cache 206 | uses: actions/cache@v4 207 | with: 208 | path: ${{ needs.build.outputs.pluginVerifierHomeDir }}/ides 209 | key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }} 210 | 211 | # Run Verify Plugin task and IntelliJ Plugin Verifier tool 212 | - name: Run Plugin Verification tasks 213 | run: ./gradlew runPluginVerifier -Dplugin.verifier.home.dir=${{ needs.build.outputs.pluginVerifierHomeDir }} 214 | 215 | # Collect Plugin Verifier Result 216 | - name: Collect Plugin Verifier Result 217 | if: ${{ always() }} 218 | uses: actions/upload-artifact@v4 219 | with: 220 | name: pluginVerifier-result 221 | path: ${{ github.workspace }}/build/reports/pluginVerifier 222 | 223 | # Prepare a draft release for GitHub Releases page for the manual verification 224 | # If accepted and published, release workflow would be triggered 225 | releaseDraft: 226 | name: Release draft 227 | if: github.event_name != 'pull_request' 228 | needs: [ build, test, inspectCode, verify ] 229 | runs-on: ubuntu-latest 230 | permissions: 231 | contents: write 232 | steps: 233 | 234 | # Check out current repository 235 | - name: Fetch Sources 236 | uses: actions/checkout@v4 237 | 238 | # Set up Java environment for the next steps 239 | - name: Setup Java 240 | uses: actions/setup-java@v4 241 | with: 242 | distribution: zulu 243 | java-version: 17 244 | 245 | # Remove old release drafts by using the curl request for the available releases with a draft flag 246 | - name: Remove Old Release Drafts 247 | env: 248 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 249 | run: | 250 | gh api repos/{owner}/{repo}/releases \ 251 | --jq '.[] | select(.draft == true) | .id' \ 252 | | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{} 253 | 254 | # Create a new release draft which is not publicly visible and requires manual acceptance 255 | - name: Create Release Draft 256 | env: 257 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 258 | run: | 259 | gh release create v${{ needs.build.outputs.version }} \ 260 | --draft \ 261 | --title "v${{ needs.build.outputs.version }}" \ 262 | --notes "$(cat << 'EOM' 263 | ${{ needs.build.outputs.changelog }} 264 | EOM 265 | )" 266 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | branches: [ "main" ] 19 | schedule: 20 | - cron: '38 17 * * 4' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze (${{ matrix.language }}) 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners (GitHub.com only) 29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | permissions: 32 | # required for all workflows 33 | security-events: write 34 | 35 | # required to fetch internal or private CodeQL packs 36 | packages: read 37 | 38 | # only required for workflows in private repositories 39 | actions: read 40 | contents: read 41 | 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | include: 46 | - language: java-kotlin 47 | build-mode: autobuild 48 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 49 | # Use `c-cpp` to analyze code written in C, C++ or both 50 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 51 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 52 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 53 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 54 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 55 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@v4 59 | 60 | # Initializes the CodeQL tools for scanning. 61 | - name: Initialize CodeQL 62 | uses: github/codeql-action/init@v3 63 | with: 64 | languages: ${{ matrix.language }} 65 | build-mode: ${{ matrix.build-mode }} 66 | # If you wish to specify custom queries, you can do so here or in a config file. 67 | # By default, queries listed here will override any specified in a config file. 68 | # Prefix the list here with "+" to use these queries and those in the config file. 69 | 70 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 71 | # queries: security-extended,security-and-quality 72 | 73 | # If the analyze step fails for one of the languages you are analyzing with 74 | # "We were unable to automatically build your code", modify the matrix above 75 | # to set the build mode to "manual" for that language. Then modify this step 76 | # to build your code. 77 | # ℹ️ Command-line programs to run using the OS shell. 78 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 79 | - if: matrix.build-mode == 'manual' 80 | shell: bash 81 | run: | 82 | echo 'If you are using a "manual" build mode for one or more of the' \ 83 | 'languages you are analyzing, replace this with the commands to build' \ 84 | 'your code, for example:' 85 | echo ' make bootstrap' 86 | echo ' make release' 87 | exit 1 88 | 89 | - name: Perform CodeQL Analysis 90 | uses: github/codeql-action/analyze@v3 91 | with: 92 | category: "/language:${{matrix.language}}" 93 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow created for handling the release process based on the draft release prepared with the Build workflow. 2 | # Running the publishPlugin task requires all following secrets to be provided: PUBLISH_TOKEN, PRIVATE_KEY, PRIVATE_KEY_PASSWORD, CERTIFICATE_CHAIN. 3 | # See https://plugins.jetbrains.com/docs/intellij/plugin-signing.html for more information. 4 | 5 | name: Release 6 | on: 7 | release: 8 | types: [prereleased, released] 9 | 10 | jobs: 11 | 12 | # Prepare and publish the plugin to JetBrains Marketplace repository 13 | release: 14 | name: Publish Plugin 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: write 18 | pull-requests: write 19 | steps: 20 | 21 | # Check out current repository 22 | - name: Fetch Sources 23 | uses: actions/checkout@v4 24 | with: 25 | ref: ${{ github.event.release.tag_name }} 26 | 27 | # Get the current tag and replace the version in gradle.properties 28 | - name: Set Version 29 | run: | 30 | VERSION=$(echo "${{ github.event.release.tag_name }}" | sed -e 's/^v//' -e 's/-.*//') 31 | sed -i "s/^pluginVersion *=.*/pluginVersion=$VERSION/" gradle.properties 32 | 33 | # Set up Java environment for the next steps 34 | - name: Setup Java 35 | uses: actions/setup-java@v4 36 | with: 37 | distribution: zulu 38 | java-version: 17 39 | 40 | # Setup Gradle 41 | - name: Setup Gradle 42 | uses: gradle/gradle-build-action@v3.4.2 43 | with: 44 | gradle-home-cache-cleanup: true 45 | 46 | # Set environment variables 47 | - name: Export Properties 48 | id: properties 49 | shell: bash 50 | run: | 51 | CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d' 52 | ${{ github.event.release.body }} 53 | EOM 54 | )" 55 | 56 | echo "changelog<> $GITHUB_OUTPUT 57 | echo "$CHANGELOG" >> $GITHUB_OUTPUT 58 | echo "EOF" >> $GITHUB_OUTPUT 59 | 60 | # Update Unreleased section with the current release note 61 | - name: Patch Changelog 62 | if: ${{ steps.properties.outputs.changelog != '' }} 63 | env: 64 | CHANGELOG: ${{ steps.properties.outputs.changelog }} 65 | run: | 66 | ./gradlew patchChangelog --release-note="$CHANGELOG" 67 | 68 | # Publish the plugin to JetBrains Marketplace 69 | - name: Publish Plugin 70 | env: 71 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 72 | CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }} 73 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} 74 | PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }} 75 | run: ./gradlew publishPlugin 76 | 77 | # Upload artifact as a release asset 78 | - name: Upload Release Asset 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/* 82 | -------------------------------------------------------------------------------- /.github/workflows/run-ui-tests.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow for launching UI tests on Linux, Windows, and Mac in the following steps: 2 | # - Prepare and launch IDE with your plugin and robot-server plugin, which is needed to interact with the UI. 3 | # - Wait for IDE to start. 4 | # - Run UI tests with a separate Gradle task. 5 | # 6 | # Please check https://github.com/JetBrains/intellij-ui-test-robot for information about UI tests with IntelliJ Platform. 7 | # 8 | # Workflow is triggered manually. 9 | 10 | name: Run UI Tests 11 | on: 12 | workflow_dispatch 13 | 14 | jobs: 15 | 16 | testUI: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | include: 22 | - os: ubuntu-latest 23 | runIde: | 24 | export DISPLAY=:99.0 25 | Xvfb -ac :99 -screen 0 1920x1080x16 & 26 | gradle runIdeForUiTests & 27 | - os: windows-latest 28 | runIde: start gradlew.bat runIdeForUiTests 29 | - os: macos-latest 30 | runIde: ./gradlew runIdeForUiTests & 31 | 32 | steps: 33 | 34 | # Check out current repository 35 | - name: Fetch Sources 36 | uses: actions/checkout@v4 37 | 38 | # Set up Java environment for the next steps 39 | - name: Setup Java 40 | uses: actions/setup-java@v4 41 | with: 42 | distribution: zulu 43 | java-version: 17 44 | 45 | # Setup Gradle 46 | - name: Setup Gradle 47 | uses: gradle/gradle-build-action@v3.4.2 48 | with: 49 | gradle-home-cache-cleanup: true 50 | 51 | # Run IDEA prepared for UI testing 52 | - name: Run IDE 53 | run: ${{ matrix.runIde }} 54 | 55 | # Wait for IDEA to be started 56 | - name: Health Check 57 | uses: jtalk/url-health-check-action@v4 58 | with: 59 | url: http://127.0.0.1:8082 60 | max-attempts: 15 61 | retry-delay: 30s 62 | 63 | # Run tests 64 | - name: Tests 65 | run: ./gradlew test 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | .qodana 4 | build 5 | .kotlin 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 17 | -------------------------------------------------------------------------------- /.run/Run IDE for UI Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | false 23 | 24 | 25 | -------------------------------------------------------------------------------- /.run/Run Plugin.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /.run/Run Qodana.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 16 | 22 | 24 | true 25 | true 26 | false 27 | false 28 | 29 | 30 | -------------------------------------------------------------------------------- /.run/Run Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.run/Run Verifications.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 25 | 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # SARIF-viewer Changelog 4 | 5 | ## [Unreleased] 6 | 7 | ## [0.0.1] - 2023-12-05 8 | 9 | ### Added 10 | 11 | - Initial scaffold created from [IntelliJ Platform Plugin Template](https://github.com/JetBrains/intellij-platform-plugin-template) 12 | 13 | [Unreleased]: https://github.com/adrienpessu/SARIF-viewer/compare/v0.0.1...HEAD 14 | [0.0.1]: https://github.com/adrienpessu/SARIF-viewer/commits/v0.0.1 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at opensource@github.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | [fork]: https://github.com/advanced-security/SARIF-viewer/fork 4 | [pr]: https://github.com/advanced-security/SARIF-viewer/compare 5 | [code-of-conduct]: CODE_OF_CONDUCT.md 6 | 7 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. 8 | 9 | Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE.md). 10 | 11 | Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. 12 | 13 | ## Prerequisites for running and testing code 14 | 15 | These are one time installations required to be able to test your changes locally as part of the pull request (PR) submission process. 16 | 17 | 1. ./gradlew build 18 | 19 | ## Submitting a pull request 20 | 21 | 1. [Fork][fork] and clone the repository 22 | 1. Configure and install the dependencies 23 | 1. Make sure the tests pass on your machine: `./gradlew test` 24 | 1. Create a new branch: `git checkout -b my-branch-name` 25 | 1. Make your change, add tests, and make sure the tests and linter still pass 26 | 1. Push to your fork and [submit a pull request][pr] 27 | 1. Pat yourself on the back and wait for your pull request to be reviewed and merged. 28 | 29 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 30 | 31 | - Write tests. 32 | - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. 33 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 34 | 35 | ## Resources 36 | 37 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 38 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 39 | - [GitHub Help](https://help.github.com) 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2018 GitHub, Inc. and contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SARIF-viewer 2 | 3 | ![Version](https://img.shields.io/jetbrains/plugin/v/23159-sarif-viewer) ![example branch parameter](https://github.com/advanced-security/SARIF-viewer/actions/workflows/build.yml/badge.svg?branch=main) 4 | 5 | 6 | docs/vuln_tree.png 7 | 8 | 9 | 10 | SARIF viewer to view the results of static analysis tools in the IDE. 11 | The Sarif comes from GitHub Advanced Security (GHAS) or from the local file system. 12 | 13 | You must provide in the settings a personal access token (PAT) to access the GitHub API with as least the following scopes: 14 | - Pull request read 15 | - Code scanning read 16 | - Metadata read 17 | 18 | 19 | 20 | 21 | ## Installation 22 | 23 | ### Manual 24 | 25 | - Download the signed zip file release from GitHub Releases : https://github.com/advanced-security/SARIF-viewer/releases 26 | - Add it to you IDE via `Settings > Plugins > Install Plugin from Disk...` 27 | 28 | ## Configuration 29 | 30 | You must provide a personal access token (PAT) to access the GitHub API with as least the following scopes: 31 | - Pull request read 32 | - Code scanning read 33 | - Metadata read 34 | 35 | And add it to the plugin configuration via `Settings > Tools > Sarif Viewer` 36 | 37 | If you are using GHES, you must also provide the URL and the corresponding token of your GHES instance. 38 | 39 | docs/settings.png 40 | 41 | ## Usage 42 | 43 | If there is a scan done one the current branch, the plugin will automatically display the results in the tool window. 44 | 45 | When you change branch, the plugin will automatically display the results of the new branch. 46 | If the current branch has one or more pull request, you will be able to select with the combobox the PR to display the results of. 47 | 48 | The result will be grouped by vulnerabilities and you will be able to navigate to the source code by clicking on the result. Also a detail will also be displayed with the path of the vulnerability and the description to help you remediate. 49 | 50 | ## 🤝  Found a bug? Missing a specific feature? 51 | 52 | Feel free to **file a new issue** with a respective [title and description](https://github.com/advanced-security/SARIF-viewer/issues) repository. If you already found a solution to your problem, **we would love to review your pull request**! 53 | 54 | ## License 55 | 56 | This project is licensed under the terms of the MIT open source license. Please refer to [MIT](./LICENSE.txt) for the full terms. 57 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | Thanks for helping make GitHub safe for everyone. 2 | 3 | # Security 4 | 5 | GitHub takes the security of our software products and services seriously, including all of the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub). 6 | 7 | Even though [open source repositories are outside of the scope of our bug bounty program](https://bounty.github.com/index.html#scope) and therefore not eligible for bounty rewards, we will ensure that your finding gets passed along to the appropriate maintainers for remediation. 8 | 9 | ## Reporting Security Issues 10 | 11 | If you believe you have found a security vulnerability in any GitHub-owned repository, please report it to us through coordinated disclosure. 12 | 13 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** 14 | 15 | Instead, please send an email to opensource-security[@]github.com. 16 | 17 | Please include as much of the information listed below as you can to help us better understand and resolve the issue: 18 | 19 | * The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting) 20 | * Full paths of source file(s) related to the manifestation of the issue 21 | * The location of the affected source code (tag/branch/commit or direct URL) 22 | * Any special configuration required to reproduce the issue 23 | * Step-by-step instructions to reproduce the issue 24 | * Proof-of-concept or exploit code (if possible) 25 | * Impact of the issue, including how an attacker might exploit the issue 26 | 27 | This information will help us triage your report more quickly. 28 | 29 | ## Policy 30 | 31 | See [GitHub's Safe Harbor Policy](https://docs.github.com/en/github/site-policy/github-bug-bounty-program-legal-safe-harbor#1-safe-harbor-terms) -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | The maintainer of this repo has not updated this file 2 | 3 | # Support 4 | 5 | ## How to file issues and get help 6 | 7 | This project uses GitHub issues to track bugs and feature requests. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new issue. 8 | 9 | For help or questions about using this project, please raise an issue and tag @adrienpessu 10 | 11 | This solution is not actively developed but is maintained by GitHub staff. We will do our best to respond to support and community questions in a timely manner. 12 | 13 | 14 | ## GitHub Support Policy 15 | 16 | Support for this project is limited to the resources listed above. -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.changelog.Changelog 2 | import org.jetbrains.changelog.markdownToHTML 3 | 4 | fun properties(key: String) = providers.gradleProperty(key) 5 | fun environment(key: String) = providers.environmentVariable(key) 6 | 7 | plugins { 8 | id("java") // Java support 9 | alias(libs.plugins.kotlin) // Kotlin support 10 | alias(libs.plugins.gradleIntelliJPlugin) // Gradle IntelliJ Plugin 11 | alias(libs.plugins.changelog) // Gradle Changelog Plugin 12 | alias(libs.plugins.qodana) // Gradle Qodana Plugin 13 | alias(libs.plugins.kover) // Gradle Kover Plugin 14 | } 15 | 16 | group = properties("pluginGroup").get() 17 | version = properties("pluginVersion").get() 18 | 19 | // Configure project's dependencies 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | // Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog 25 | dependencies { 26 | // implementation(libs.annotations) 27 | implementation("com.contrastsecurity:java-sarif:2.0") 28 | constraints { 29 | implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3") 30 | } 31 | testImplementation("org.assertj:assertj-core:3.27.3") 32 | } 33 | 34 | // Set the JVM language level used to build the project. Use Java 11 for 2020.3+, and Java 17 for 2022.2+. 35 | kotlin { 36 | jvmToolchain(17) 37 | } 38 | 39 | // Configure Gradle IntelliJ Plugin - read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html 40 | intellij { 41 | pluginName = properties("pluginName") 42 | version = properties("platformVersion") 43 | type = properties("platformType") 44 | 45 | // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file. 46 | plugins = properties("platformPlugins").map { it.split(',').map(String::trim).filter(String::isNotEmpty) } 47 | } 48 | 49 | // Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin 50 | changelog { 51 | groups.empty() 52 | repositoryUrl = properties("pluginRepositoryUrl") 53 | } 54 | 55 | // Configure Gradle Qodana Plugin - read more: https://github.com/JetBrains/gradle-qodana-plugin 56 | qodana { 57 | cachePath = provider { file(".qodana").canonicalPath } 58 | reportPath = provider { file("build/reports/inspections").canonicalPath } 59 | saveReport = true 60 | showReport = environment("QODANA_SHOW_REPORT").map { it.toBoolean() }.getOrElse(false) 61 | } 62 | 63 | // Configure Gradle Kover Plugin - read more: https://github.com/Kotlin/kotlinx-kover#configuration 64 | kover { 65 | reports { 66 | total { 67 | xml { 68 | onCheck = true 69 | } 70 | } 71 | } 72 | } 73 | 74 | tasks { 75 | wrapper { 76 | gradleVersion = properties("gradleVersion").get() 77 | } 78 | 79 | patchPluginXml { 80 | version = properties("pluginVersion") 81 | sinceBuild = properties("pluginSinceBuild") 82 | untilBuild = properties("pluginUntilBuild") 83 | 84 | // Extract the section from README.md and provide for the plugin's manifest 85 | pluginDescription = providers.fileContents(layout.projectDirectory.file("README.md")).asText.map { 86 | val start = "" 87 | val end = "" 88 | 89 | with (it.lines()) { 90 | if (!containsAll(listOf(start, end))) { 91 | throw GradleException("Plugin description section not found in README.md:\n$start ... $end") 92 | } 93 | subList(indexOf(start) + 1, indexOf(end)).joinToString("\n").let(::markdownToHTML) 94 | } 95 | } 96 | 97 | val changelog = project.changelog // local variable for configuration cache compatibility 98 | // Get the latest available change notes from the changelog file 99 | changeNotes = properties("pluginVersion").map { pluginVersion -> 100 | with(changelog) { 101 | renderItem( 102 | (getOrNull(pluginVersion) ?: getUnreleased()) 103 | .withHeader(false) 104 | .withEmptySections(false), 105 | Changelog.OutputType.HTML, 106 | ) 107 | } 108 | } 109 | } 110 | 111 | // Configure UI tests plugin 112 | // Read more: https://github.com/JetBrains/intellij-ui-test-robot 113 | runIdeForUiTests { 114 | systemProperty("robot-server.port", "8082") 115 | systemProperty("ide.mac.message.dialogs.as.sheets", "false") 116 | systemProperty("jb.privacy.policy.text", "") 117 | systemProperty("jb.consents.confirmation.enabled", "false") 118 | } 119 | 120 | signPlugin { 121 | certificateChain = environment("CERTIFICATE_CHAIN") 122 | privateKey = environment("PRIVATE_KEY") 123 | password = environment("PRIVATE_KEY_PASSWORD") 124 | } 125 | 126 | publishPlugin { 127 | dependsOn("patchChangelog") 128 | token = environment("PUBLISH_TOKEN") 129 | // The pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 130 | // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: 131 | // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel 132 | var channels = properties("pluginVersion").map { listOf(it.split('-').getOrElse(1) { "default" }.split('.').first()) } 133 | channels 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /docs/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/advanced-security/SARIF-viewer/15c715e8c5259da4446eca468cce3919cb4c5030/docs/settings.png -------------------------------------------------------------------------------- /docs/vuln_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/advanced-security/SARIF-viewer/15c715e8c5259da4446eca468cce3919cb4c5030/docs/vuln_tree.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 2 | 3 | pluginGroup = com.github.adrienpessu.sarifviewer 4 | pluginName = SARIF-viewer 5 | pluginRepositoryUrl = https://github.com/adrienpessu/SARIF-viewer 6 | # SemVer format -> https://semver.org 7 | pluginVersion = 1.2.2 8 | 9 | # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 10 | pluginSinceBuild = 223 11 | pluginUntilBuild = 252.* 12 | 13 | # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension 14 | platformType = IC 15 | platformVersion = 2022.3.3 16 | 17 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 18 | # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 19 | platformPlugins = Git4Idea 20 | 21 | # Gradle Releases -> https://github.com/gradle/gradle/releases 22 | gradleVersion = 8.5 23 | 24 | # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib 25 | kotlin.stdlib.default.dependency = false 26 | 27 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 28 | org.gradle.configuration-cache = true 29 | 30 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 31 | org.gradle.caching = true 32 | 33 | # Enable Gradle Kotlin DSL Lazy Property Assignment -> https://docs.gradle.org/current/userguide/kotlin_dsl.html#kotdsl:assignment 34 | systemProp.org.gradle.unsafe.kotlin.assignment = true 35 | 36 | kotlin.daemon.jvmargs=-Xmx1g 37 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | # libraries 3 | annotations = "26.0.2" 4 | 5 | # plugins 6 | kotlin = "2.1.21" 7 | changelog = "2.2.0" 8 | gradleIntelliJPlugin = "1.17.4" 9 | qodana = "0.1.13" 10 | kover = "0.9.1" 11 | 12 | [libraries] 13 | annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" } 14 | 15 | [plugins] 16 | changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } 17 | gradleIntelliJPlugin = { id = "org.jetbrains.intellij", version.ref = "gradleIntelliJPlugin" } 18 | kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 19 | kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } 20 | qodana = { id = "org.jetbrains.qodana", version.ref = "qodana" } 21 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/advanced-security/SARIF-viewer/15c715e8c5259da4446eca468cce3919cb4c5030/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command; 206 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 207 | # shell script including quotes and variable substitutions, so put them in 208 | # double quotes to make sure that they get re-expanded; and 209 | # * put everything else in single quotes, so that it's not re-expanded. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /qodana.yml: -------------------------------------------------------------------------------- 1 | # Qodana configuration: 2 | # https://www.jetbrains.com/help/qodana/qodana-yaml.html 3 | 4 | version: 1.0 5 | linter: jetbrains/qodana-jvm-community:latest 6 | projectJDK: 17 7 | profile: 8 | name: qodana.recommended 9 | exclude: 10 | - name: All 11 | paths: 12 | - .qodana 13 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "SARIF-viewer" 2 | 3 | dependencyResolutionManagement { 4 | versionCatalogs { 5 | create("viewer") { 6 | library("java-sarif", "com.contrastsecurity:java-sarif:2.0") 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/adrienpessu/sarifviewer/MyBundle.kt: -------------------------------------------------------------------------------- 1 | package com.github.adrienpessu.sarifviewer 2 | 3 | import com.intellij.DynamicBundle 4 | import org.jetbrains.annotations.NonNls 5 | import org.jetbrains.annotations.PropertyKey 6 | 7 | @NonNls 8 | private const val BUNDLE = "messages.MyBundle" 9 | 10 | object MyBundle : DynamicBundle(BUNDLE) { 11 | 12 | @JvmStatic 13 | fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = 14 | getMessage(key, *params) 15 | 16 | @Suppress("unused") 17 | @JvmStatic 18 | fun messagePointer(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = 19 | getLazyMessage(key, *params) 20 | } 21 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/adrienpessu/sarifviewer/actions/OpenLocalAction.kt: -------------------------------------------------------------------------------- 1 | package com.github.adrienpessu.sarifviewer.actions 2 | 3 | import com.github.adrienpessu.sarifviewer.toolWindow.SarifViewerWindowFactory 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | 7 | class OpenLocalAction : AnAction("Open local Sarif file") { 8 | 9 | var myToolWindow: SarifViewerWindowFactory.MyToolWindow? = null 10 | 11 | override fun actionPerformed(e: AnActionEvent) { 12 | myToolWindow?.openLocalFile() 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/adrienpessu/sarifviewer/actions/RefreshAction.kt: -------------------------------------------------------------------------------- 1 | package com.github.adrienpessu.sarifviewer.actions 2 | 3 | import com.github.adrienpessu.sarifviewer.exception.SarifViewerException 4 | import com.github.adrienpessu.sarifviewer.toolWindow.SarifViewerWindowFactory 5 | import com.intellij.openapi.actionSystem.AnAction 6 | import com.intellij.openapi.actionSystem.AnActionEvent 7 | 8 | class RefreshAction : AnAction("Refresh from GitHub") { 9 | var myToolWindow: SarifViewerWindowFactory.MyToolWindow? = null 10 | 11 | override fun actionPerformed(e: AnActionEvent) { 12 | val gitHubInstance = myToolWindow?.github?: throw SarifViewerException.INVALID_REPOSITORY 13 | val repositoryFullName = myToolWindow?.repositoryFullName?: throw SarifViewerException.INVALID_REPOSITORY 14 | val currentBranch = myToolWindow?.currentBranch?: throw SarifViewerException.INVALID_BRANCH 15 | 16 | myToolWindow?.refresh(currentBranch, gitHubInstance, repositoryFullName) 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/adrienpessu/sarifviewer/configurable/SettingComponent.kt: -------------------------------------------------------------------------------- 1 | package com.github.adrienpessu.sarifviewer.configurable 2 | 3 | import com.intellij.ui.components.JBLabel 4 | import com.intellij.ui.components.JBPasswordField 5 | import com.intellij.ui.components.JBTextField 6 | import com.intellij.util.ui.FormBuilder 7 | import java.awt.GridBagConstraints 8 | import java.awt.GridBagLayout 9 | import javax.swing.JComponent 10 | import javax.swing.JPanel 11 | import javax.swing.JTextField 12 | import javax.swing.JToggleButton 13 | 14 | 15 | class SettingComponent { 16 | private var myMainPanel: JPanel? = null 17 | private var ghTokenTextField: JTextField =JBTextField() 18 | private var ghTokenPasswordField: JTextField = JBPasswordField() 19 | private val ghesHostnameText = JBTextField() 20 | private var ghesTokenTextField: JTextField = JBTextField() 21 | private var ghesTokenPasswordField: JTextField = JBPasswordField() 22 | private val toggleButton = JToggleButton("Show/Hide PAT") 23 | 24 | private var isGhTokenVisible: Boolean = false 25 | 26 | init { 27 | 28 | ghTokenTextField.isVisible = isGhTokenVisible 29 | ghesTokenTextField.isVisible = isGhTokenVisible 30 | myMainPanel = FormBuilder.createFormBuilder() 31 | .addComponent(JBLabel("GitHub.com PAT ")) 32 | .addComponent(ghTokenTextField) 33 | .addComponent(ghTokenPasswordField) 34 | .addSeparator(48) 35 | .addComponent(JBLabel("GHES Hostname ")) 36 | .addComponent(ghesHostnameText) 37 | .addComponent(JBLabel("GHES PAT ")) 38 | .addComponent(ghesTokenTextField) 39 | .addComponent(ghesTokenPasswordField) 40 | .addComponentFillVertically(JPanel(), 0) 41 | .addSeparator() 42 | .addLabeledComponent("", toggleButton, 1, false) 43 | .panel 44 | 45 | toggleButton.addActionListener { 46 | isGhTokenVisible = !isGhTokenVisible 47 | if (isGhTokenVisible) { 48 | ghTokenTextField.text = ghTokenPasswordField.text 49 | ghTokenTextField.isVisible = true 50 | ghTokenPasswordField.isVisible = false 51 | 52 | ghesTokenTextField.text = ghesTokenPasswordField.text 53 | ghesTokenTextField.isVisible = true 54 | ghesTokenPasswordField.isVisible = false 55 | } else { 56 | ghTokenPasswordField.text = ghTokenTextField.text 57 | ghTokenTextField.isVisible = false 58 | ghTokenPasswordField.isVisible = true 59 | 60 | ghesTokenPasswordField.text = ghesTokenTextField.text 61 | ghesTokenTextField.isVisible = false 62 | ghesTokenPasswordField.isVisible = true 63 | } 64 | myMainPanel!!.revalidate() // Notify the layout manager 65 | myMainPanel!!.repaint() // Redraw the components 66 | } 67 | } 68 | 69 | 70 | fun getPanel(): JPanel { 71 | return myMainPanel!! 72 | } 73 | 74 | fun getPreferredFocusedComponent(): JComponent { 75 | return if (toggleButton.isSelected) { 76 | ghTokenTextField 77 | } else { 78 | ghTokenPasswordField 79 | } 80 | } 81 | 82 | fun getGhTokenText(): String { 83 | return if (isGhTokenVisible) { 84 | ghTokenTextField.text 85 | } else { 86 | ghTokenPasswordField.text 87 | } 88 | } 89 | 90 | fun setGhTokenText(newText: String) { 91 | ghTokenTextField.text = newText 92 | ghTokenPasswordField.text = newText 93 | } 94 | 95 | fun getGhesHostnameText(): String { 96 | return ghesHostnameText.text 97 | } 98 | 99 | fun getGhesTokenText(): String { 100 | return if (isGhTokenVisible) { 101 | ghesTokenTextField.text 102 | } else { 103 | ghesTokenPasswordField.text 104 | } 105 | } 106 | 107 | fun setGhesHostnameText(newText: String) { 108 | ghesHostnameText.text = newText 109 | } 110 | 111 | fun setGhesTokenText(newText: String) { 112 | ghesTokenTextField.text = newText 113 | ghesTokenPasswordField.text = newText 114 | } 115 | } 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/adrienpessu/sarifviewer/configurable/Settings.kt: -------------------------------------------------------------------------------- 1 | package com.github.adrienpessu.sarifviewer.configurable 2 | 3 | import com.intellij.openapi.Disposable 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.options.Configurable 6 | import com.intellij.util.messages.Topic 7 | import javax.swing.JComponent 8 | 9 | 10 | class Settings : Configurable, Configurable.NoScroll, Disposable { 11 | private var mySettingsComponent: SettingComponent? = null 12 | 13 | interface SettingsSavedListener { 14 | fun settingsSaved() 15 | } 16 | 17 | override fun getDisplayName(): String { 18 | return "SARIF Viewer Settings" 19 | } 20 | 21 | override fun getPreferredFocusedComponent(): JComponent { 22 | return mySettingsComponent!!.getPreferredFocusedComponent() 23 | } 24 | 25 | override fun createComponent(): JComponent { 26 | mySettingsComponent = SettingComponent() 27 | mySettingsComponent!!.setGhTokenText(SettingsState.instance.state.pat) 28 | mySettingsComponent!!.setGhesHostnameText(SettingsState.instance.state.ghesHostname) 29 | mySettingsComponent!!.setGhesTokenText(SettingsState.instance.state.ghesPat) 30 | return mySettingsComponent!!.getPanel() 31 | } 32 | 33 | override fun isModified(): Boolean = 34 | listOf( 35 | mySettingsComponent!!.getGhTokenText() != SettingsState.instance.state.pat, 36 | mySettingsComponent!!.getGhesHostnameText() != SettingsState.instance.state.ghesHostname, 37 | mySettingsComponent!!.getGhesTokenText() != SettingsState.instance.state.ghesPat, 38 | ).any() 39 | 40 | override fun apply() { 41 | val settings: SettingsState = SettingsState.instance 42 | settings.state.pat = mySettingsComponent!!.getGhTokenText() 43 | settings.state.ghesHostname = mySettingsComponent!!.getGhesHostnameText() 44 | settings.state.ghesPat = mySettingsComponent!!.getGhesTokenText() 45 | 46 | ApplicationManager.getApplication().messageBus.syncPublisher(SETTINGS_SAVED_TOPIC).settingsSaved() 47 | 48 | } 49 | 50 | override fun reset() { 51 | mySettingsComponent?.setGhTokenText(SettingsState.instance.state.pat) 52 | mySettingsComponent?.setGhesHostnameText(SettingsState.instance.state.ghesHostname) 53 | mySettingsComponent?.setGhesTokenText(SettingsState.instance.state.ghesPat) 54 | } 55 | 56 | override fun disposeUIResources() { 57 | mySettingsComponent = null 58 | } 59 | 60 | override fun dispose() { 61 | mySettingsComponent = null 62 | } 63 | 64 | companion object { 65 | val SETTINGS_SAVED_TOPIC = Topic.create("SettingsSaved", SettingsSavedListener::class.java) 66 | } 67 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/adrienpessu/sarifviewer/configurable/SettingsState.kt: -------------------------------------------------------------------------------- 1 | package com.github.adrienpessu.sarifviewer.configurable 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.components.* 5 | 6 | 7 | @State( 8 | name = "SettingsState", 9 | storages = [Storage("sarif-viewer-plugin.xml")] 10 | ) 11 | open class SettingsState : PersistentStateComponent { 12 | 13 | // this is how we're going to call the component from different classes 14 | companion object { 15 | val instance: SettingsState 16 | get() = ApplicationManager.getApplication().getService(SettingsState::class.java) ?: SettingsState() 17 | } 18 | 19 | // the component will always keep our state as a variable 20 | var pluginState: PluginState = PluginState() 21 | 22 | // just an obligatory override from PersistentStateComponent 23 | override fun getState(): PluginState { 24 | return pluginState 25 | } 26 | 27 | // after automatically loading our save state, we will keep reference to it 28 | override fun loadState(paramState: PluginState) { 29 | pluginState = paramState 30 | } 31 | 32 | // the POKO class that always keeps our state 33 | class PluginState { 34 | var pat = "Insert your GitHub PAT here" 35 | var ghesHostname = "Insert your GitHub Enterprise Server hostname here" 36 | var ghesPat = "Insert your GitHub Enterprise Server PAT here" 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/adrienpessu/sarifviewer/exception/SarifViewerException.kt: -------------------------------------------------------------------------------- 1 | package com.github.adrienpessu.sarifviewer.exception 2 | 3 | class SarifViewerException(val code: Int, override val message: String): Exception(message) { 4 | companion object { 5 | val INVALID_PAT = SarifViewerException(1, "Invalid GitHub PAT") 6 | val UNAUTHORIZED = SarifViewerException(2, "Unauthorized: \nthe token provided doesn't have the authorization to access the current repository\nor the current branch haven't been pushed") 7 | val INVALID_REPOSITORY = SarifViewerException(4, "Invalid repository or no analyses available") 8 | val INVALID_BRANCH = SarifViewerException(5, "Invalid branch") 9 | val INVALID_SARIF = SarifViewerException(7, "Invalid SARIF file") 10 | val INVALID_VIEW = SarifViewerException(8, "Invalid view type") 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/adrienpessu/sarifviewer/listeners/SarifViewerActivationListener.kt: -------------------------------------------------------------------------------- 1 | package com.github.adrienpessu.sarifviewer.listeners 2 | 3 | import com.intellij.openapi.application.ApplicationActivationListener 4 | import com.intellij.openapi.diagnostic.thisLogger 5 | import com.intellij.openapi.wm.IdeFrame 6 | 7 | internal class SarifViewerActivationListener : ApplicationActivationListener { 8 | 9 | override fun applicationActivated(ideFrame: IdeFrame) { 10 | thisLogger().info("SARIF view plugin activated") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/adrienpessu/sarifviewer/models/BranchItemComboBox.kt: -------------------------------------------------------------------------------- 1 | package com.github.adrienpessu.sarifviewer.models 2 | 3 | data class BranchItemComboBox( 4 | val prNumber: Int = 0, 5 | val head: String = "", 6 | val base: String = "", 7 | val prTitle: String = "", 8 | val commit: String = "", 9 | ) { 10 | override fun toString(): String { 11 | return if (prNumber == 0) { 12 | head 13 | } else { 14 | "pr$prNumber ($prTitle)" 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/adrienpessu/sarifviewer/models/Leaf.kt: -------------------------------------------------------------------------------- 1 | package com.github.adrienpessu.sarifviewer.models 2 | 3 | data class Leaf( 4 | val leafName: String, 5 | val address: String, 6 | val steps: List, 7 | val location: String, 8 | val ruleId: String, 9 | val ruleName: String, 10 | val ruleDescription: String, 11 | val level: String, 12 | val kind: String, 13 | val githubAlertNumber: String, 14 | val githubAlertUrl: String, 15 | ) { 16 | override fun toString(): String { 17 | 18 | val icon = when (level) { 19 | "error" -> "🛑" 20 | "warning" -> "⚠️" 21 | "note" -> "📝" 22 | else -> "" 23 | } 24 | 25 | return "$icon ${address.split("/").last()} $ruleDescription" 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/adrienpessu/sarifviewer/models/Root.kt: -------------------------------------------------------------------------------- 1 | package com.github.adrienpessu.sarifviewer.models 2 | 3 | import java.util.* 4 | 5 | data class Root ( 6 | var ref: String? = null, 7 | var commit_sha: String? = null, 8 | var analysis_key: String? = null, 9 | var environment: String? = null, 10 | var category: String? = null, 11 | var error: String? = null, 12 | var created_at: Date? = null, 13 | var results_count: Int = 0, 14 | var rules_count: Int = 0, 15 | var id: Int = 0, 16 | var url: String? = null, 17 | var sarif_id: String? = null, 18 | var tool: Tool? = null, 19 | var deletable: Boolean = false, 20 | var warning: String? = null 21 | ) 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/adrienpessu/sarifviewer/models/Tool.kt: -------------------------------------------------------------------------------- 1 | package com.github.adrienpessu.sarifviewer.models 2 | 3 | data class Tool( 4 | var name: String? = null, 5 | var guid: Any? = null, 6 | var version: String? = null 7 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/github/adrienpessu/sarifviewer/models/View.kt: -------------------------------------------------------------------------------- 1 | package com.github.adrienpessu.sarifviewer.models 2 | 3 | data class View( 4 | val key: String = "", 5 | val value: String = "" 6 | ) { 7 | override fun toString() = value 8 | 9 | companion object { 10 | val RULE = View("rules", "View by rules") 11 | val LOCATION = View("location", "View by location") 12 | val views = arrayOf(RULE, LOCATION) 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/adrienpessu/sarifviewer/services/SarifService.kt: -------------------------------------------------------------------------------- 1 | package com.github.adrienpessu.sarifviewer.services 2 | 3 | import com.contrastsecurity.sarif.Result 4 | import com.contrastsecurity.sarif.SarifSchema210 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | import com.fasterxml.jackson.module.kotlin.readValue 7 | import com.github.adrienpessu.sarifviewer.exception.SarifViewerException 8 | import com.github.adrienpessu.sarifviewer.models.Leaf 9 | import com.github.adrienpessu.sarifviewer.models.Root 10 | import com.github.adrienpessu.sarifviewer.models.View 11 | import com.github.adrienpessu.sarifviewer.utils.GitHubInstance 12 | import com.intellij.openapi.components.Service 13 | import com.intellij.util.alsoIfNull 14 | import java.net.HttpURLConnection 15 | import java.net.URI 16 | 17 | 18 | @Service(Service.Level.PROJECT) 19 | class SarifService { 20 | 21 | fun getSarifFromGitHub(github: GitHubInstance, repositoryFullName: String, branchName: String): List { 22 | val analysisFromGitHub = getAnalysisFromGitHub(github, repositoryFullName, branchName) 23 | val objectMapper = ObjectMapper() 24 | val analysis: List = objectMapper.readValue(analysisFromGitHub) 25 | 26 | val ids = analysis.filter { it.commit_sha == analysis.first().commit_sha }.map { it.id } 27 | 28 | return ids.map { id -> 29 | val sarifFromGitHub = getSarifFromGitHub(github, repositoryFullName, id) 30 | val sarif: SarifSchema210 = objectMapper.readValue(sarifFromGitHub) 31 | sarif.alsoIfNull { SarifSchema210() } 32 | } 33 | } 34 | 35 | fun analyseResult(results: List): HashMap> { 36 | val map = HashMap>() 37 | results.forEach { result -> 38 | val element = leaf(result) 39 | val key = result.rule?.id ?: result.correlationGuid?.toString() ?: result.message.text 40 | if (map.containsKey(key)) { 41 | map[key]?.add(element) 42 | } else { 43 | map[key] = mutableListOf(element) 44 | } 45 | } 46 | return map 47 | 48 | } 49 | 50 | fun analyseSarif(sarif: SarifSchema210, view: View): HashMap> { 51 | 52 | when (view) { 53 | View.RULE -> { 54 | val map = HashMap>() 55 | try { 56 | sarif.runs.forEach { run -> 57 | run?.results?.forEach { result -> 58 | val element = leaf(result) 59 | val key = result.rule?.id ?: result.correlationGuid?.toString() ?: result.message.text 60 | if (map.containsKey(key)) { 61 | map[key]?.add(element) 62 | } else { 63 | map[key] = mutableListOf(element) 64 | } 65 | } 66 | } 67 | } catch (e: Exception) { 68 | throw SarifViewerException.INVALID_SARIF 69 | } 70 | return map 71 | } 72 | View.LOCATION -> { 73 | val map = HashMap>() 74 | try { 75 | sarif.runs.forEach { run -> 76 | run?.results?.forEach { result -> 77 | val element = leaf(result) 78 | val key = result.locations[0].physicalLocation.artifactLocation.uri 79 | if (map.containsKey(key)) { 80 | map[key]?.add(element) 81 | } else { 82 | map[key] = mutableListOf(element) 83 | } 84 | } 85 | } 86 | } catch (e: Exception) { 87 | throw SarifViewerException.INVALID_SARIF 88 | } 89 | return map 90 | } 91 | else -> { 92 | throw SarifViewerException.INVALID_VIEW 93 | } 94 | } 95 | 96 | 97 | } 98 | 99 | private fun leaf(result: Result): Leaf { 100 | val additionalProperties = result.properties?.additionalProperties ?: mapOf() 101 | val element = Leaf( 102 | leafName = result.message.text ?: "", 103 | address = "${result.locations[0].physicalLocation.artifactLocation.uri}:${result.locations[0].physicalLocation.region.startLine ?: 0}:${result.locations[0].physicalLocation.region.startColumn ?: 0}", 104 | steps = result.codeFlows?.get(0)?.threadFlows?.get(0)?.locations?.map { "${it.location.physicalLocation.artifactLocation.uri}:${it.location.physicalLocation.region.startLine}:${it.location.physicalLocation.region.startColumn}" } 105 | ?: listOf(), 106 | location = result.locations[0].physicalLocation.artifactLocation.uri, 107 | ruleId = result.ruleId, 108 | ruleName = result.rule?.id ?: "", 109 | ruleDescription = result.message.text ?: "", 110 | level = result.level.toString(), 111 | kind = result.kind.toString(), 112 | githubAlertNumber = additionalProperties["github/alertNumber"]?.toString() ?: "", 113 | githubAlertUrl = additionalProperties["github/alertUrl"]?.toString() ?: "" 114 | ) 115 | return element 116 | } 117 | 118 | fun getPullRequests(github: GitHubInstance, repositoryFullName: String, branchName: String = "main"): List<*>? { 119 | val head = "${repositoryFullName.split("/")[0]}:$branchName" 120 | val connection = URI("${github.apiBase}/repos/$repositoryFullName/pulls?state=open&head=$head") 121 | .toURL() 122 | .openConnection() as HttpURLConnection 123 | 124 | connection.apply { 125 | requestMethod = "GET" 126 | doInput = true 127 | doOutput = true 128 | 129 | setRequestProperty("Accept", "application/vnd.github.v3+json") 130 | setRequestProperty("X-GitHub-Api-Version", "2022-11-28") 131 | setRequestProperty("Authorization", "Bearer ${github.token}") 132 | } 133 | 134 | handleExceptions(connection) 135 | 136 | val response = connection.inputStream.bufferedReader().readText() 137 | 138 | connection.disconnect() 139 | return ObjectMapper().readValue(response, List::class.java) 140 | } 141 | 142 | private fun getAnalysisFromGitHub( 143 | github: GitHubInstance, 144 | repositoryFullName: String, 145 | branchName: String = "main" 146 | ): String { 147 | 148 | val s = "${github.apiBase}/repos/$repositoryFullName/code-scanning/analyses?ref=$branchName" 149 | val connection = URI(s) 150 | .toURL() 151 | .openConnection() as HttpURLConnection 152 | 153 | connection.apply { 154 | requestMethod = "GET" 155 | doInput = true 156 | doOutput = true 157 | 158 | setRequestProperty("Accept", "application/vnd.github.v3+json") 159 | setRequestProperty("X-GitHub-Api-Version", "2022-11-28") 160 | setRequestProperty("Authorization", "Bearer ${github.token}") 161 | } 162 | 163 | handleExceptions(connection) 164 | 165 | val response = connection.inputStream.bufferedReader().readText() 166 | 167 | connection.disconnect() 168 | 169 | return response 170 | } 171 | 172 | private fun handleExceptions(connection: HttpURLConnection) { 173 | when (connection.responseCode) { 174 | 401 -> { 175 | throw SarifViewerException.INVALID_PAT 176 | } 177 | 178 | 404 -> { 179 | throw SarifViewerException.INVALID_REPOSITORY 180 | } 181 | 182 | 422 -> { 183 | throw SarifViewerException.INVALID_BRANCH 184 | } 185 | 186 | 403 -> { 187 | throw SarifViewerException.UNAUTHORIZED 188 | } 189 | } 190 | } 191 | 192 | private fun getSarifFromGitHub(github: GitHubInstance, repositoryFullName: String, analysisId: Int): String { 193 | val connection = URI("${github.apiBase}/repos/$repositoryFullName/code-scanning/analyses/$analysisId") 194 | .toURL() 195 | .openConnection() as HttpURLConnection 196 | 197 | connection.apply { 198 | requestMethod = "GET" 199 | doInput = true 200 | doOutput = true 201 | 202 | setRequestProperty("Accept", "application/sarif+json") 203 | setRequestProperty("X-GitHub-Api-Version", "2022-11-28") 204 | setRequestProperty("Authorization", "Bearer ${github.token}") 205 | } 206 | 207 | handleExceptions(connection) 208 | 209 | val response = connection.inputStream.bufferedReader().readText() 210 | 211 | connection.disconnect() 212 | 213 | return response 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/adrienpessu/sarifviewer/toolWindow/MyCustomInlayRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.github.adrienpessu.sarifviewer.toolWindow 2 | 3 | import com.intellij.openapi.editor.EditorCustomElementRenderer 4 | import com.intellij.openapi.editor.Inlay 5 | import com.intellij.openapi.editor.impl.EditorImpl 6 | import com.intellij.openapi.editor.markup.TextAttributes 7 | import java.awt.Font 8 | import java.awt.Graphics 9 | import java.awt.Rectangle 10 | 11 | class MyCustomInlayRenderer(private val text: String) : EditorCustomElementRenderer { 12 | 13 | private val myFont = Font("Courrier new", Font.ITALIC, 12) 14 | override fun paint(inlay: Inlay<*>, g: Graphics, targetRegion: Rectangle, textAttributes: TextAttributes) { 15 | 16 | (inlay.editor as EditorImpl).apply { 17 | g.font = myFont 18 | g.color = colorsScheme.defaultForeground 19 | g.drawString(text, targetRegion.x, targetRegion.y + ascent) 20 | } 21 | } 22 | 23 | override fun calcWidthInPixels(inlay: Inlay<*>): Int { 24 | return (inlay.editor as EditorImpl).getFontMetrics(myFont.style).stringWidth(text) 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/adrienpessu/sarifviewer/toolWindow/SarifViewerWindowFactory.kt: -------------------------------------------------------------------------------- 1 | package com.github.adrienpessu.sarifviewer.toolWindow 2 | 3 | import com.contrastsecurity.sarif.Result 4 | import com.contrastsecurity.sarif.SarifSchema210 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | import com.github.adrienpessu.sarifviewer.actions.OpenLocalAction 7 | import com.github.adrienpessu.sarifviewer.actions.RefreshAction 8 | import com.github.adrienpessu.sarifviewer.configurable.Settings 9 | import com.github.adrienpessu.sarifviewer.configurable.SettingsState 10 | import com.github.adrienpessu.sarifviewer.exception.SarifViewerException 11 | import com.github.adrienpessu.sarifviewer.models.BranchItemComboBox 12 | import com.github.adrienpessu.sarifviewer.models.Leaf 13 | import com.github.adrienpessu.sarifviewer.models.View 14 | import com.github.adrienpessu.sarifviewer.services.SarifService 15 | import com.github.adrienpessu.sarifviewer.utils.GitHubInstance 16 | import com.intellij.notification.Notification 17 | import com.intellij.notification.NotificationGroupManager 18 | import com.intellij.notification.NotificationType 19 | import com.intellij.notification.Notifications 20 | import com.intellij.openapi.actionSystem.ActionManager 21 | import com.intellij.openapi.actionSystem.AnAction 22 | import com.intellij.openapi.components.service 23 | import com.intellij.openapi.diagnostic.thisLogger 24 | import com.intellij.openapi.editor.Editor 25 | import com.intellij.openapi.fileEditor.FileEditorManager 26 | import com.intellij.openapi.fileEditor.OpenFileDescriptor 27 | import com.intellij.openapi.project.DumbService 28 | import com.intellij.openapi.project.Project 29 | import com.intellij.openapi.ui.ComboBox 30 | import com.intellij.openapi.vfs.VirtualFileManager 31 | import com.intellij.openapi.wm.ToolWindow 32 | import com.intellij.openapi.wm.ToolWindowFactory 33 | import com.intellij.ui.ScrollPaneFactory 34 | import com.intellij.ui.components.JBPanel 35 | import com.intellij.ui.components.JBTabbedPane 36 | import com.intellij.ui.content.ContentFactory 37 | import com.intellij.ui.table.JBTable 38 | import com.jetbrains.rd.util.printlnError 39 | import git4idea.GitLocalBranch 40 | import git4idea.repo.GitRepository 41 | import git4idea.repo.GitRepositoryChangeListener 42 | import git4idea.repo.GitRepositoryManager 43 | import java.awt.Component 44 | import java.awt.Cursor 45 | import java.awt.Desktop 46 | import java.awt.Dimension 47 | import java.awt.event.ActionListener 48 | import java.awt.event.MouseAdapter 49 | import java.awt.event.MouseEvent 50 | import java.io.File 51 | import java.net.URI 52 | import java.nio.charset.Charset 53 | import java.nio.file.Files 54 | import java.nio.file.Path 55 | import javax.swing.* 56 | import javax.swing.event.TreeSelectionEvent 57 | import javax.swing.event.TreeSelectionListener 58 | import javax.swing.filechooser.FileNameExtensionFilter 59 | import javax.swing.table.DefaultTableCellRenderer 60 | import javax.swing.table.DefaultTableModel 61 | import javax.swing.tree.DefaultMutableTreeNode 62 | import javax.swing.tree.DefaultTreeModel 63 | 64 | 65 | class SarifViewerWindowFactory : ToolWindowFactory { 66 | 67 | init { 68 | thisLogger().warn("Don't forget to remove all non-needed sample code files with their corresponding registration entries in `plugin.xml`.") 69 | } 70 | 71 | override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { 72 | val myToolWindow = MyToolWindow(toolWindow) 73 | val content = ContentFactory.getInstance().createContent(myToolWindow.getContent(), project.name, false) 74 | 75 | toolWindow.contentManager.addContent(content) 76 | } 77 | 78 | override fun shouldBeAvailable(project: Project) = true 79 | 80 | class MyToolWindow(toolWindow: ToolWindow) { 81 | 82 | init { 83 | val actionManager = ActionManager.getInstance() 84 | 85 | val openLocalFileAction = actionManager.getAction("OpenLocalFileAction") 86 | (openLocalFileAction as OpenLocalAction).myToolWindow = this 87 | val refreshAction = actionManager.getAction("RefreshAction") 88 | (refreshAction as RefreshAction).myToolWindow = this 89 | val actions = ArrayList() 90 | actions.add(openLocalFileAction) 91 | actions.add(refreshAction) 92 | 93 | toolWindow.setTitleActions(actions) 94 | } 95 | 96 | internal var github: GitHubInstance? = null 97 | internal var repositoryFullName: String? = null 98 | internal var currentBranch: GitLocalBranch? = null 99 | 100 | private var localMode = false 101 | private val service = toolWindow.project.service() 102 | private val project = toolWindow.project 103 | private var main = ScrollPaneFactory.createScrollPane() 104 | private val details = JBTabbedPane() 105 | private val splitPane = JSplitPane(JSplitPane.VERTICAL_SPLIT, false, main, details) 106 | private var myList = com.intellij.ui.treeStructure.Tree() 107 | private var comboBranchPR = ComboBox(arrayOf(BranchItemComboBox(0, "main", "", ""))) 108 | private val tableInfos = JBTable(DefaultTableModel(arrayOf("Property", "Value"), 0)) 109 | private val tableSteps = JBTable(DefaultTableModel(arrayOf("Path"), 0)) 110 | private val steps = JPanel() 111 | private val errorField = JLabel("Error message here ") 112 | private val errorToolbar = JToolBar("", JToolBar.HORIZONTAL) 113 | private val loadingPanel = JPanel() 114 | private var sarifGitHubRef = "" 115 | private var loading = false 116 | private var disableComboBoxEvent = false 117 | private var currentView = View.RULE 118 | private var cacheSarif: SarifSchema210? = null 119 | private var currentLeaf: Leaf? = null 120 | 121 | fun getContent() = JBPanel>().apply { 122 | 123 | manageTreeIcons() 124 | buildSkeleton() 125 | 126 | val messageBus = project.messageBus 127 | 128 | messageBus.connect().subscribe(Settings.SETTINGS_SAVED_TOPIC, object : Settings.SettingsSavedListener { 129 | override fun settingsSaved() { 130 | val repository = GitRepositoryManager.getInstance(project).repositories.firstOrNull() 131 | if (!localMode) { 132 | clearJSplitPane() 133 | if (repository != null) { 134 | val worker = object : SwingWorker() { 135 | override fun doInBackground() { 136 | toggleLoading() 137 | loadDataAndUI(repository) 138 | toggleLoading() 139 | } 140 | } 141 | worker.execute() 142 | } 143 | } 144 | } 145 | }) 146 | 147 | messageBus.connect().subscribe(GitRepository.GIT_REPO_CHANGE, object : GitRepositoryChangeListener { 148 | override fun repositoryChanged(repository: GitRepository) { 149 | if (!localMode) { 150 | clearJSplitPane() 151 | if (repository != null) { 152 | toggleLoading() 153 | loadDataAndUI(repository) 154 | toggleLoading() 155 | } 156 | } 157 | } 158 | }) 159 | } 160 | 161 | private fun JBPanel>.loadDataAndUI( 162 | repository: GitRepository, 163 | selectedCombo: BranchItemComboBox? = null 164 | ) { 165 | currentBranch = repository.currentBranch 166 | 167 | val remote = repository.remotes.firstOrNull { 168 | GitHubInstance.extractHostname(it.firstUrl) in 169 | setOf(GitHubInstance.DOT_COM.hostname, SettingsState.instance.pluginState.ghesHostname) 170 | } 171 | 172 | github = GitHubInstance.fromRemoteUrl(remote?.firstUrl.orEmpty()) 173 | if (github == null) { 174 | displayError("Could not find a configured GitHub instance that matches $remote") 175 | return 176 | } 177 | 178 | if (github == GitHubInstance.DOT_COM) { 179 | github!!.token = SettingsState.instance.pluginState.pat 180 | } else if (github!!.hostname == SettingsState.instance.pluginState.ghesHostname) { 181 | github!!.token = SettingsState.instance.pluginState.ghesPat 182 | } 183 | 184 | repositoryFullName = github!!.extractRepoNwo(remote?.firstUrl) 185 | if (repositoryFullName == null) { 186 | displayError("Could not determine repository owner and name from remote URL: $remote") 187 | return 188 | } 189 | 190 | if (selectedCombo == null) { 191 | sarifGitHubRef = "refs/heads/${currentBranch?.name ?: "refs/heads/main"}" 192 | } 193 | 194 | if (github!!.token == SettingsState().pluginState.pat || github!!.token.isEmpty()) { 195 | displayError("No GitHub PAT found for ${github!!.hostname}") 196 | return 197 | } 198 | 199 | if (repositoryFullName!!.isNotEmpty() && currentBranch?.name?.isNotEmpty() == true) { 200 | try { 201 | if (selectedCombo == null) { 202 | populateCombo(currentBranch, github!!, repositoryFullName!!) 203 | } 204 | 205 | val map = extractSarif(github!!, repositoryFullName!!, selectedCombo?.head) 206 | if (map.isEmpty()) { 207 | emptyNode(map, repositoryFullName) 208 | } else { 209 | thisLogger().info("Load result for the repository $repositoryFullName and ref $sarifGitHubRef") 210 | } 211 | buildContent(map) 212 | } catch (e: SarifViewerException) { 213 | thisLogger().warn(e.message) 214 | displayError(e.message) 215 | return 216 | } 217 | 218 | 219 | } else { 220 | displayError("No remote found") 221 | } 222 | } 223 | 224 | private fun emptyNode( 225 | map: HashMap>, 226 | repositoryFullName: String? 227 | ) { 228 | val element = Leaf( 229 | leafName = "", 230 | address = "", 231 | steps = listOf(), 232 | location = "", 233 | ruleId = "", 234 | ruleName = "", 235 | ruleDescription = "", 236 | level = "", 237 | kind = "", 238 | githubAlertNumber = "", 239 | githubAlertUrl = "", 240 | ) 241 | map["No SARIF file found for the repository $repositoryFullName and ref $sarifGitHubRef"] = 242 | listOf(element).toMutableList() 243 | } 244 | 245 | private fun toggleLoading(forcedValue: Boolean? = null) { 246 | loading = forcedValue ?: !loading 247 | loadingPanel.isVisible = loading 248 | } 249 | 250 | private fun displayError(message: String) { 251 | clearJSplitPane() 252 | errorToolbar.isVisible = true 253 | errorField.text = message 254 | 255 | NotificationGroupManager.getInstance() 256 | .getNotificationGroup("SARIF viewer") 257 | .createNotification(message, NotificationType.ERROR) 258 | .notify(project) 259 | 260 | thisLogger().info(message) 261 | } 262 | 263 | private fun JBPanel>.buildSkeleton() { 264 | steps.layout = BoxLayout(steps, BoxLayout.Y_AXIS) 265 | tableSteps.size = Dimension(steps.width, steps.height) 266 | steps.add(tableSteps) 267 | 268 | // Add the table to a scroll pane 269 | val scrollPane = JScrollPane(tableInfos) 270 | 271 | details.addTab("Infos", scrollPane) 272 | details.addTab("Steps", steps) 273 | 274 | layout = BoxLayout(this, BoxLayout.Y_AXIS) 275 | 276 | doLayout() 277 | 278 | 279 | errorToolbar.setSize(100, 10) 280 | errorToolbar.isFloatable = false 281 | errorToolbar.isRollover = true 282 | errorToolbar.alignmentX = Component.LEFT_ALIGNMENT 283 | errorToolbar.add(errorField) 284 | errorToolbar.isVisible = false 285 | add(errorToolbar) 286 | 287 | val jToolBar = JToolBar("", JToolBar.HORIZONTAL) 288 | jToolBar.isFloatable = false 289 | jToolBar.isRollover = true 290 | jToolBar.alignmentX = Component.LEFT_ALIGNMENT 291 | 292 | val viewComboBox = ComboBox(View.views) 293 | 294 | viewComboBox.maximumSize = Dimension(100, 30) 295 | 296 | viewComboBox.selectedItem = currentView 297 | 298 | viewComboBox.addActionListener(ActionListener() { event -> 299 | if (event.source is ComboBox<*>) { 300 | val link = event.source as ComboBox 301 | val selectedItem = link.selectedItem as View 302 | if (event.actionCommand == "comboBoxChanged" && !DumbService.isDumb(project) && selectedItem.key != currentView.key) { 303 | val worker = object : SwingWorker() { 304 | override fun doInBackground() { 305 | toggleLoading() 306 | currentView = selectedItem 307 | clearJSplitPane() 308 | var map = HashMap>() 309 | if (localMode) { 310 | if (cacheSarif?.runs?.isEmpty() == false) { 311 | map = service.analyseSarif(cacheSarif!!, currentView) 312 | } 313 | } else { 314 | map = extractSarif(github!!, repositoryFullName!!, sarifGitHubRef) 315 | treeBuilding(map) 316 | } 317 | treeBuilding(map) 318 | toggleLoading() 319 | 320 | } 321 | } 322 | worker.execute() 323 | } 324 | } 325 | }) 326 | jToolBar.add(viewComboBox) 327 | 328 | val jLabel = JLabel("Branch/PR: ") 329 | jLabel.maximumSize = Dimension(100, jToolBar.preferredSize.height) 330 | 331 | jToolBar.add(jLabel) 332 | 333 | comboBranchPR.addActionListener(ActionListener() { event -> 334 | val comboBox = event.source as JComboBox<*> 335 | if (event.actionCommand == "comboBoxChanged" && comboBox.selectedItem != null 336 | && !disableComboBoxEvent && !DumbService.isDumb(project) 337 | ) { 338 | val selectedOption = comboBox.selectedItem as BranchItemComboBox 339 | sarifGitHubRef = if (selectedOption.prNumber != 0) { 340 | "refs/pull/${selectedOption.prNumber}/merge" 341 | } else { 342 | "refs/heads/${selectedOption.head}" 343 | } 344 | 345 | clearJSplitPane() 346 | val repository = GitRepositoryManager.getInstance(project).repositories.firstOrNull() 347 | if (repository != null) { 348 | // Create a SwingWorker to perform the time-consuming task in a separate thread 349 | val worker = object : SwingWorker() { 350 | override fun doInBackground() { 351 | toggleLoading(true) 352 | loadDataAndUI(repository, selectedOption) 353 | toggleLoading(false) 354 | } 355 | } 356 | worker.execute() 357 | } else { 358 | add(JLabel("No Git repository found")) 359 | } 360 | } 361 | }) 362 | 363 | jToolBar.add(comboBranchPR) 364 | add(jToolBar) 365 | 366 | loadingPanel.layout = BoxLayout(loadingPanel, BoxLayout.Y_AXIS) 367 | loadingPanel.add(JLabel("Loading...")) 368 | loadingPanel.add(JLabel("Please wait...")) 369 | loadingPanel.isVisible = false 370 | add(loadingPanel) 371 | 372 | add(splitPane) 373 | 374 | details.isVisible = false 375 | } 376 | 377 | private fun buildContent( 378 | map: HashMap> 379 | ) { 380 | treeBuilding(map) 381 | } 382 | 383 | fun openLocalFile() { 384 | val fileChooser = JFileChooser() 385 | fileChooser.fileFilter = FileNameExtensionFilter("SARIF files", "sarif") 386 | SwingUtilities.invokeLater { 387 | val returnValue = fileChooser.showOpenDialog(null) 388 | if (returnValue == JFileChooser.APPROVE_OPTION) { 389 | val selectedFile: File = fileChooser.selectedFile 390 | val extractSarifFromFile = extractSarifFromFile(selectedFile) 391 | treeBuilding(extractSarifFromFile) 392 | localMode = true 393 | } 394 | } 395 | } 396 | 397 | fun refresh( 398 | currentBranch: GitLocalBranch, 399 | github: GitHubInstance, 400 | repositoryFullName: String 401 | ) { 402 | localMode = false 403 | val worker = object : SwingWorker() { 404 | override fun doInBackground() { 405 | toggleLoading(true) 406 | clearJSplitPane() 407 | populateCombo(currentBranch, github, repositoryFullName) 408 | val mapSarif = extractSarif(github, repositoryFullName) 409 | toggleLoading(false) 410 | if (mapSarif.isEmpty()) { 411 | emptyNode(mapSarif, repositoryFullName) 412 | } else { 413 | thisLogger().info("Load result for the repository $repositoryFullName and branch ${currentBranch.name}") 414 | } 415 | buildContent(mapSarif) 416 | } 417 | } 418 | worker.execute() 419 | } 420 | 421 | private fun treeBuilding(map: HashMap>) { 422 | val root = DefaultMutableTreeNode(project.name) 423 | 424 | map.forEach { (key, value) -> 425 | val ruleNode = DefaultMutableTreeNode("$key (${value.size})") 426 | value.forEach { location -> 427 | val locationNode = DefaultMutableTreeNode(location) 428 | ruleNode.add(locationNode) 429 | } 430 | root.add(ruleNode) 431 | } 432 | 433 | myList = com.intellij.ui.treeStructure.Tree(root) 434 | 435 | myList.isRootVisible = false 436 | myList.showsRootHandles = true 437 | main = ScrollPaneFactory.createScrollPane(myList) 438 | 439 | details.isVisible = false 440 | 441 | splitPane.leftComponent = main 442 | splitPane.rightComponent = details 443 | 444 | myList.addTreeSelectionListener(object : TreeSelectionListener { 445 | override fun valueChanged(e: TreeSelectionEvent?) { 446 | if (e != null && e.isAddedPath) { 447 | val leaves = map[e.path.parentPath.lastPathComponent.toString().split(" ").first()] 448 | if (!leaves.isNullOrEmpty()) { 449 | currentLeaf = try { 450 | leaves.first { it.address == ((e.path.lastPathComponent as DefaultMutableTreeNode).userObject as Leaf).address } 451 | } catch (e: Exception) { 452 | leaves.first() 453 | } 454 | 455 | val githubAlertUrl = currentLeaf!!.githubAlertUrl 456 | .replace("api.", "") 457 | .replace("api/v3/", "") 458 | .replace("repos/", "") 459 | .replace("code-scanning/alerts", "security/code-scanning") 460 | 461 | tableInfos.clearSelection() 462 | // Create a table model with "Property" and "Value" columns 463 | val defaultTableModel: DefaultTableModel = 464 | object : DefaultTableModel(arrayOf("Property", "Value"), 0) { 465 | override fun isCellEditable(row: Int, column: Int): Boolean { 466 | return false 467 | } 468 | } 469 | tableInfos.model = defaultTableModel 470 | 471 | // Add some data 472 | defaultTableModel.addRow(arrayOf("Name", currentLeaf!!.leafName)) 473 | defaultTableModel.addRow(arrayOf("Level", currentLeaf!!.level)) 474 | defaultTableModel.addRow(arrayOf("Rule's name", currentLeaf!!.ruleName)) 475 | defaultTableModel.addRow(arrayOf("Rule's description", currentLeaf!!.ruleDescription)) 476 | defaultTableModel.addRow(arrayOf("Location", currentLeaf!!.location)) 477 | defaultTableModel.addRow( 478 | arrayOf( 479 | "GitHub alert number", 480 | currentLeaf!!.githubAlertNumber 481 | ) 482 | ) 483 | defaultTableModel.addRow( 484 | arrayOf( 485 | "GitHub alert url", 486 | "$githubAlertUrl$url") 510 | c.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) 511 | } 512 | return c 513 | } 514 | }) 515 | 516 | 517 | tableInfos.addMouseListener(object : MouseAdapter() { 518 | override fun mouseClicked(e: MouseEvent) { 519 | val row = tableInfos.rowAtPoint(e.point) 520 | val column = tableInfos.columnAtPoint(e.point) 521 | if (row == tableInfos.rowCount - 1) { 522 | if (column == tableInfos.columnCount - 1) { 523 | if (Desktop.isDesktopSupported() && Desktop.getDesktop() 524 | .isSupported(Desktop.Action.BROWSE) 525 | ) { 526 | Desktop.getDesktop().browse(URI(githubAlertUrl)) 527 | } 528 | } 529 | } 530 | } 531 | }) 532 | 533 | tableInfos.updateUI() 534 | 535 | tableSteps.clearSelection() 536 | 537 | tableSteps.model = DefaultTableModel(arrayOf("Path"), 0) 538 | 539 | currentLeaf!!.steps.forEachIndexed { index, step -> 540 | (tableSteps.model as DefaultTableModel).addRow(arrayOf("$index $step")) 541 | } 542 | 543 | tableSteps.addMouseListener(object : MouseAdapter() { 544 | override fun mouseClicked(e: MouseEvent) { 545 | val row = tableInfos.rowAtPoint(e.point) 546 | // When the row can't be found, the method returns -1 547 | if (row != -1) { 548 | val path = currentLeaf!!.steps[row].split(":") 549 | openFile(project, path[0], path[1].toInt(), path[2].toInt()) 550 | } 551 | } 552 | }) 553 | 554 | details.isVisible = true 555 | val addr = currentLeaf!!.address.split(":") 556 | openFile( 557 | project, 558 | currentLeaf!!.location, 559 | addr[1].toInt(), 560 | addr[2].toInt(), 561 | currentLeaf!!.level, 562 | currentLeaf!!.ruleId, 563 | currentLeaf!!.ruleDescription 564 | ) 565 | 566 | splitPane.setDividerLocation(0.5) 567 | 568 | } else { 569 | details.isVisible = false 570 | } 571 | } 572 | } 573 | }) 574 | } 575 | 576 | private fun manageTreeIcons() { 577 | val tmp = Files.createTempFile("warning", ".svg").toFile() 578 | val icon: Icon = ImageIcon(tmp.absolutePath) 579 | UIManager.put("Tree.closedIcon", icon) 580 | UIManager.put("Tree.openIcon", icon) 581 | UIManager.put("Tree.leafIcon", icon) 582 | } 583 | 584 | private fun openFile( 585 | project: Project, 586 | path: String, 587 | lineNumber: Int, 588 | columnNumber: Int = 0, 589 | level: String = "", 590 | rule: String = "", 591 | description: String = "" 592 | ) { 593 | 594 | val editor: Editor = FileEditorManager.getInstance(project).selectedTextEditor ?: return 595 | val inlayModel = editor.inlayModel 596 | 597 | inlayModel.getBlockElementsInRange(0, editor.document.textLength) 598 | .filter { it.renderer is MyCustomInlayRenderer } 599 | .forEach { it.dispose() } 600 | 601 | val virtualFile = 602 | VirtualFileManager.getInstance().findFileByNioPath(Path.of("${project.basePath}/$path")) 603 | if (virtualFile != null) { 604 | FileEditorManager.getInstance(project).openTextEditor( 605 | OpenFileDescriptor( 606 | project, 607 | virtualFile, 608 | lineNumber - 1, 609 | columnNumber - 1 610 | ), 611 | true // request focus to editor 612 | ) 613 | val editor: Editor = FileEditorManager.getInstance(project).selectedTextEditor ?: return 614 | val inlayModel = editor.inlayModel 615 | 616 | val offset = editor.document.getLineStartOffset(lineNumber - 1) 617 | 618 | val icon = when (level) { 619 | "error" -> "🛑" 620 | "warning" -> "⚠️" 621 | "note" -> "📝" 622 | else -> "" 623 | } 624 | val description = "$icon $rule: $description" 625 | if (description.isNotEmpty()) { 626 | inlayModel.addBlockElement(offset, true, true, 1, MyCustomInlayRenderer(description)) 627 | } 628 | } else { 629 | // display error message 630 | Notifications.Bus.notify( 631 | Notification( 632 | "Sarif viewer", 633 | "File not found", 634 | "Can't find the file ${project.basePath}/$path", 635 | NotificationType.WARNING 636 | ), project 637 | ) 638 | } 639 | } 640 | 641 | private fun clearJSplitPane() { 642 | if (myList.components != null) { 643 | SwingUtilities.invokeLater { 644 | myList.model = DefaultTreeModel(DefaultMutableTreeNode()) 645 | myList.updateUI() 646 | } 647 | } 648 | 649 | tableInfos.clearSelection() 650 | tableInfos.updateUI() 651 | tableSteps.clearSelection() 652 | tableSteps.updateUI() 653 | details.isVisible = false 654 | errorToolbar.isVisible = false 655 | } 656 | 657 | private fun extractSarif( 658 | github: GitHubInstance, 659 | repositoryFullName: String, 660 | base: String? = null 661 | ): HashMap> { 662 | val sarifs = service.getSarifFromGitHub(github, repositoryFullName, sarifGitHubRef).filterNotNull() 663 | var map = HashMap>() 664 | val results = sarifs.flatMap { it.runs?.get(0)?.results ?: emptyList() } 665 | if (sarifs.isNotEmpty()) { 666 | if (sarifGitHubRef.startsWith("refs/pull/") && base != null) { 667 | val resultsToDisplay = ArrayList() 668 | val sarifMainBranch = service.getSarifFromGitHub(github, repositoryFullName, base).filterNotNull() 669 | val mainResults: List = sarifMainBranch.flatMap { it.runs?.get(0)?.results ?: emptyList() } 670 | 671 | for (currentResult in results) { 672 | if (mainResults.none { 673 | it.ruleId == currentResult.ruleId 674 | && ("${currentResult.locations[0].physicalLocation.artifactLocation.uri}:${currentResult.locations[0].physicalLocation.region.startLine}" == "${it.locations[0].physicalLocation.artifactLocation.uri}:${it.locations[0].physicalLocation.region.startLine}") 675 | }) { 676 | resultsToDisplay.add(currentResult) 677 | } 678 | } 679 | map = service.analyseResult(resultsToDisplay) 680 | } else { 681 | map = sarifs.map { service.analyseSarif(it, currentView) } 682 | .reduce { acc, item -> acc.apply { putAll(item) } } 683 | } 684 | } 685 | 686 | return map 687 | } 688 | 689 | private fun extractSarifFromFile( 690 | file: File 691 | ): HashMap> { 692 | // file to String 693 | val sarifString = file.readText(Charset.defaultCharset()) 694 | val sarif = ObjectMapper().readValue(sarifString, SarifSchema210::class.java) 695 | cacheSarif = sarif 696 | var map = HashMap>() 697 | if (sarif.runs?.isEmpty() == false) { 698 | map = service.analyseSarif(sarif, currentView) 699 | } 700 | 701 | return map 702 | } 703 | 704 | private fun populateCombo( 705 | currentBranch: GitLocalBranch?, 706 | github: GitHubInstance, 707 | repositoryFullName: String 708 | ) { 709 | disableComboBoxEvent = true 710 | comboBranchPR.removeAllItems() 711 | comboBranchPR.addItem(BranchItemComboBox(0, currentBranch?.name ?: "main", "", "")) 712 | val pullRequests = 713 | service.getPullRequests(github, repositoryFullName, sarifGitHubRef.split('/', limit = 3).last()) 714 | if (pullRequests?.isNotEmpty() == true) { 715 | pullRequests.forEach { 716 | val currentPr = it as LinkedHashMap<*, *> 717 | comboBranchPR.addItem( 718 | BranchItemComboBox( 719 | currentPr["number"] as Int, 720 | (currentPr["base"] as LinkedHashMap)["ref"] ?: "", 721 | (currentPr["head"] as LinkedHashMap)["ref"] ?: "", 722 | currentPr["title"].toString(), 723 | (currentPr["head"] as LinkedHashMap)["commit_sha"] ?: "" 724 | ) 725 | ) 726 | } 727 | } 728 | disableComboBoxEvent = false 729 | } 730 | } 731 | } 732 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/adrienpessu/sarifviewer/utils/GitHubInstance.kt: -------------------------------------------------------------------------------- 1 | package com.github.adrienpessu.sarifviewer.utils 2 | 3 | import org.jetbrains.annotations.VisibleForTesting 4 | import java.net.URI 5 | 6 | data class GitHubInstance(val hostname: String, val apiBase: String = "https://$hostname/api/v3") { 7 | // Keep this out of the constructor so that it doesn't accidentally end up in a toString() output 8 | var token: String = "" 9 | 10 | fun extractRepoNwo(remoteUrl: String?): String? { 11 | if (remoteUrl?.startsWith("https") == true) { 12 | return URI(remoteUrl).path.replace(Regex("^/"), "").replace(Regex(".git$"), "") 13 | } else if (remoteUrl?.startsWith("git@") == true) { 14 | return remoteUrl.replace(Regex("^git@$hostname:"), "").replace(Regex(".git$"), "") 15 | } 16 | return null 17 | } 18 | 19 | companion object { 20 | val DOT_COM: GitHubInstance = GitHubInstance("github.com", "https://api.github.com") 21 | 22 | @VisibleForTesting 23 | fun extractHostname(remoteUrl: String?): String? { 24 | return if (remoteUrl?.startsWith("https") == true) { 25 | URI(remoteUrl).host 26 | } else if (remoteUrl?.startsWith("git@") == true) { 27 | remoteUrl.substringAfter("git@").substringBefore(":") 28 | } else { 29 | null 30 | } 31 | } 32 | 33 | fun fromRemoteUrl(remoteUrl: String): GitHubInstance? { 34 | val hostname = extractHostname(remoteUrl) 35 | if (hostname == DOT_COM.hostname) { 36 | return DOT_COM 37 | } else if (!hostname.isNullOrEmpty()){ 38 | return GitHubInstance(hostname) 39 | } 40 | return null 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | com.github.adrienpessu.sarifviewer 4 | SARIF-viewer 5 | adrienpessu 6 | 7 | com.intellij.modules.platform 8 | Git4Idea 9 | 10 | messages.MyBundle 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 28 | 29 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/main/resources/com.github.adrienpessu.sarifviewer/alert-fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/com.github.adrienpessu.sarifviewer/file-directory.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/messages/MyBundle.properties: -------------------------------------------------------------------------------- 1 | projectService=Project service: {0} 2 | randomLabel=The random number is: {0} 3 | shuffle=Shuffle 4 | -------------------------------------------------------------------------------- /src/test/kotlin/com/github/adrienpessu/sarifviewer/MyPluginTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.adrienpessu.sarifviewer 2 | 3 | import com.intellij.ide.highlighter.XmlFileType 4 | import com.intellij.psi.xml.XmlFile 5 | import com.intellij.testFramework.TestDataPath 6 | import com.intellij.testFramework.fixtures.BasePlatformTestCase 7 | import com.intellij.util.PsiErrorElementUtil 8 | 9 | @TestDataPath("\$CONTENT_ROOT/src/test/testData") 10 | class MyPluginTest : BasePlatformTestCase() { 11 | 12 | fun testXMLFile() { 13 | val psiFile = myFixture.configureByText(XmlFileType.INSTANCE, "bar") 14 | val xmlFile = assertInstanceOf(psiFile, XmlFile::class.java) 15 | 16 | assertFalse(PsiErrorElementUtil.hasErrors(project, xmlFile.virtualFile)) 17 | 18 | assertNotNull(xmlFile.rootTag) 19 | 20 | xmlFile.rootTag?.let { 21 | assertEquals("foo", it.name) 22 | assertEquals("bar", it.value.text) 23 | } 24 | } 25 | 26 | fun testRename() { 27 | myFixture.testRename("foo.xml", "foo_after.xml", "a2") 28 | } 29 | 30 | override fun getTestDataPath() = "src/test/testData/rename" 31 | } 32 | -------------------------------------------------------------------------------- /src/test/kotlin/com/github/adrienpessu/sarifviewer/util/GithubInstanceTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.adrienpessu.sarifviewer.util 2 | 3 | import com.github.adrienpessu.sarifviewer.utils.GitHubInstance 4 | import org.assertj.core.api.Assertions.assertThat 5 | import org.junit.Test 6 | 7 | class GithubInstanceTest { 8 | companion object { 9 | const val DOTCOM_GIT_URL = "git@github.com:adrienpessu/SARIF-viewer.git" 10 | const val DOTCOM_HTTPS_URL = "https://github.com/adrienpessu/SARIF-viewer.git" 11 | const val GHES_GIT_URL = "git@github.private.domain:adrienpessu/SARIF-viewer.git" 12 | const val GHES_HTTPS_URL = "https://github.private.domain/adrienpessu/SARIF-viewer.git" 13 | 14 | const val DOTCOM_HOSTNAME = "github.com" 15 | const val GHES_HOSTNAME = "github.private.domain" 16 | const val REPO_NWO = "adrienpessu/SARIF-viewer" 17 | 18 | val DOTCOM_INSTANCE = GitHubInstance.DOT_COM 19 | val GHES_INSTANCE = GitHubInstance(GHES_HOSTNAME) 20 | } 21 | 22 | @Test 23 | fun testExtractHostnameFromSsh() { 24 | assertThat(GitHubInstance.extractHostname(DOTCOM_GIT_URL)).isEqualTo(DOTCOM_HOSTNAME) 25 | assertThat(GitHubInstance.extractHostname(GHES_GIT_URL)).isEqualTo(GHES_HOSTNAME) 26 | } 27 | 28 | @Test 29 | fun testExtractHostnameFromHttps() { 30 | assertThat(GitHubInstance.extractHostname(DOTCOM_HTTPS_URL)).isEqualTo(DOTCOM_HOSTNAME) 31 | assertThat(GitHubInstance.extractHostname(GHES_HTTPS_URL)).isEqualTo(GHES_HOSTNAME) 32 | } 33 | 34 | @Test 35 | fun testExtractRepoNwoFromSsh() { 36 | assertThat(DOTCOM_INSTANCE.extractRepoNwo(DOTCOM_GIT_URL)).isEqualTo(REPO_NWO) 37 | assertThat(GHES_INSTANCE.extractRepoNwo(GHES_GIT_URL)).isEqualTo(REPO_NWO) 38 | } 39 | 40 | @Test 41 | fun testExtractRepoNwoFromHttps() { 42 | assertThat(DOTCOM_INSTANCE.extractRepoNwo(DOTCOM_HTTPS_URL)).isEqualTo(REPO_NWO) 43 | assertThat(GHES_INSTANCE.extractRepoNwo(GHES_HTTPS_URL)).isEqualTo(REPO_NWO) 44 | } 45 | } -------------------------------------------------------------------------------- /src/test/testData/rename/foo.xml: -------------------------------------------------------------------------------- 1 | 2 | 1>Foo 3 | 4 | -------------------------------------------------------------------------------- /src/test/testData/rename/foo_after.xml: -------------------------------------------------------------------------------- 1 | 2 | Foo 3 | 4 | --------------------------------------------------------------------------------