├── .all-contributorsrc ├── .git-blame-ignore-revs ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── early-access.yml │ ├── mutation-testing.yml │ └── release.yml ├── .gitignore ├── .gitpod.Dockerfile ├── .gitpod.yml ├── .mvn └── .gitkeep ├── CONTRIBUTORS.md ├── LICENSE ├── README.md ├── jreleaser.yml ├── pom.xml └── src ├── main ├── assembly │ └── assembly.xml ├── graalvm-native │ └── resource-config.json ├── java │ └── it │ │ └── mulders │ │ └── mcs │ │ ├── App.java │ │ ├── cli │ │ ├── ClassSearchCommand.java │ │ ├── ClasspathVersionProvider.java │ │ ├── Cli.java │ │ ├── SearchCommand.java │ │ └── SystemPropertyLoader.java │ │ ├── common │ │ ├── McsExecutionExceptionHandler.java │ │ ├── McsRuntimeException.java │ │ ├── Result.java │ │ └── SearchResponseBodyHandler.java │ │ ├── dagger │ │ ├── Application.java │ │ ├── CommandLineModule.java │ │ ├── DaggerFactory.java │ │ ├── OutputModule.java │ │ └── SearchModule.java │ │ └── search │ │ ├── ClassnameQuery.java │ │ ├── Constants.java │ │ ├── CoordinateQuery.java │ │ ├── FormatType.java │ │ ├── SearchClient.java │ │ ├── SearchCommandHandler.java │ │ ├── SearchQuery.java │ │ ├── SearchResponse.java │ │ ├── UnsupportedFormatException.java │ │ ├── WildcardSearchQuery.java │ │ ├── printer │ │ ├── BuildrOutput.java │ │ ├── CoordinatePrinter.java │ │ ├── DelegatingOutputPrinter.java │ │ ├── GavOutput.java │ │ ├── GradleGroovyOutput.java │ │ ├── GradleGroovyShortOutput.java │ │ ├── GradleKotlinOutput.java │ │ ├── GrapeOutput.java │ │ ├── IvyXmlOutput.java │ │ ├── JBangOutput.java │ │ ├── LeiningenOutput.java │ │ ├── NoOutputPrinter.java │ │ ├── OutputFactory.java │ │ ├── OutputPrinter.java │ │ ├── PomXmlOutput.java │ │ ├── SbtOutput.java │ │ └── TabularOutputPrinter.java │ │ └── vulnerability │ │ ├── ComponentReportClient.java │ │ ├── ComponentReportResponse.java │ │ ├── ComponentReportResponseBodyHandler.java │ │ └── ComponentReportVulnerabilitySeverity.java └── resources │ └── mcs.properties └── test ├── java └── it │ └── mulders │ └── mcs │ ├── AppIT.java │ ├── AppTest.java │ ├── cli │ ├── ClassSearchCommandTest.java │ ├── ClasspathVersionProviderTest.java │ ├── CliTest.java │ ├── DaggerFactoryTest.java │ ├── MockitoFactory.java │ ├── SearchCommandTest.java │ └── SystemPropertyLoaderTest.java │ ├── common │ ├── McsExecutionExceptionHandlerTest.java │ ├── ResultTest.java │ └── SearchResponseBodyHandlerTest.java │ └── search │ ├── ClassnameQueryTest.java │ ├── CoordinateQueryTest.java │ ├── FormatTypeTest.java │ ├── SearchClientIT.java │ ├── SearchCommandHandlerTest.java │ ├── SearchQueryTest.java │ ├── WildcardSearchQueryTest.java │ ├── printer │ ├── CoordinatePrinterTest.java │ ├── DelegatingOutputPrinterTest.java │ ├── NoOutputPrinterTest.java │ └── TabularOutputPrinterTest.java │ └── vulnerability │ ├── ComponentReportClientIT.java │ ├── ComponentReportResponseBodyHandlerTest.java │ └── ComponentReportVulnerabilitySeverityTest.java └── resources ├── group-artifact-search.json ├── group-artifact-version-search.json ├── mcs.properties ├── no-vulnerabilities-component-report-response.json ├── sample-mcs.config ├── vulnerabilities-component-report-response.json └── wildcard-search-response.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "mcs", 3 | "projectOwner": "mthmulders", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "CONTRIBUTORS.md" 8 | ], 9 | "imageSize": 100, 10 | "contributorsPerLine": 7, 11 | "skipCi": true, 12 | "commitType": "docs", 13 | "commitConvention": "angular", 14 | "contributors": [ 15 | { 16 | "login": "willemvanlent", 17 | "name": "Willem van Lent", 18 | "avatar_url": "https://avatars.githubusercontent.com/u/4223994?v=4", 19 | "profile": "https://github.com/willemvanlent", 20 | "contributions": [ 21 | "code" 22 | ] 23 | }, 24 | { 25 | "login": "hannotify", 26 | "name": "Hanno Embregts", 27 | "avatar_url": "https://avatars.githubusercontent.com/u/11613148?v=4", 28 | "profile": "https://hanno.codes", 29 | "contributions": [ 30 | "code", 31 | "ideas" 32 | ] 33 | }, 34 | { 35 | "login": "BranislavBeno", 36 | "name": "Branislav Beňo", 37 | "avatar_url": "https://avatars.githubusercontent.com/u/57846939?v=4", 38 | "profile": "https://github.com/BranislavBeno", 39 | "contributions": [ 40 | "code" 41 | ] 42 | }, 43 | { 44 | "login": "Giovds", 45 | "name": "Giovanni van der Schelde", 46 | "avatar_url": "https://avatars.githubusercontent.com/u/27761321?v=4", 47 | "profile": "https://giovds.com/", 48 | "contributions": [ 49 | "code" 50 | ] 51 | }, 52 | { 53 | "login": "martinbonnin", 54 | "name": "Martin Bonnin", 55 | "avatar_url": "https://avatars.githubusercontent.com/u/3974977?v=4", 56 | "profile": "https://mbonnin.net", 57 | "contributions": [ 58 | "ideas" 59 | ] 60 | }, 61 | { 62 | "login": "BOTbkcd", 63 | "name": "bot_bkcd", 64 | "avatar_url": "https://avatars.githubusercontent.com/u/83156045?v=4", 65 | "profile": "https://github.com/BOTbkcd", 66 | "contributions": [ 67 | "code" 68 | ] 69 | }, 70 | { 71 | "login": "shaikhu", 72 | "name": "Usman Shaikh", 73 | "avatar_url": "https://avatars.githubusercontent.com/u/38332365?v=4", 74 | "profile": "https://github.com/shaikhu", 75 | "contributions": [ 76 | "code" 77 | ] 78 | }, 79 | { 80 | "login": "jwedel", 81 | "name": "Jan Wedel", 82 | "avatar_url": "https://avatars.githubusercontent.com/u/4849728?v=4", 83 | "profile": "http://return.co.de", 84 | "contributions": [ 85 | "code" 86 | ] 87 | }, 88 | { 89 | "login": "frimtec", 90 | "name": "Markus Friedli", 91 | "avatar_url": "https://avatars.githubusercontent.com/u/3511114?v=4", 92 | "profile": "https://github.com/frimtec", 93 | "contributions": [ 94 | "bug" 95 | ] 96 | }, 97 | { 98 | "login": "slovdahl", 99 | "name": "Sebastian Lövdahl", 100 | "avatar_url": "https://avatars.githubusercontent.com/u/1417619?v=4", 101 | "profile": "https://github.com/slovdahl", 102 | "contributions": [ 103 | "bug" 104 | ] 105 | }, 106 | { 107 | "login": "dhinojosa", 108 | "name": "Daniel Hinojosa", 109 | "avatar_url": "https://avatars.githubusercontent.com/u/410757?v=4", 110 | "profile": "http://www.evolutionnext.com", 111 | "contributions": [ 112 | "bug" 113 | ] 114 | }, 115 | { 116 | "login": "spencergibb", 117 | "name": "Spencer Gibb", 118 | "avatar_url": "https://avatars.githubusercontent.com/u/594085?v=4", 119 | "profile": "https://gibb.tech", 120 | "contributions": [ 121 | "ideas" 122 | ] 123 | }, 124 | { 125 | "login": "jeffmaury", 126 | "name": "Jeff MAURY", 127 | "avatar_url": "https://avatars.githubusercontent.com/u/695993?v=4", 128 | "profile": "http://riadiscuss.jeffmaury.com", 129 | "contributions": [ 130 | "bug", 131 | "ideas" 132 | ] 133 | }, 134 | { 135 | "login": "jludvice", 136 | "name": "Josef Ludvicek", 137 | "avatar_url": "https://avatars.githubusercontent.com/u/8707241?v=4", 138 | "profile": "https://github.com/jludvice", 139 | "contributions": [ 140 | "bug" 141 | ] 142 | }, 143 | { 144 | "login": "bmarwell", 145 | "name": "Benjamin Marwell", 146 | "avatar_url": "https://avatars.githubusercontent.com/u/1413391?v=4", 147 | "profile": "https://blog.bmarwell.de/", 148 | "contributions": [ 149 | "ideas" 150 | ] 151 | }, 152 | { 153 | "login": "AbdelHajou", 154 | "name": "Abdel Hajou", 155 | "avatar_url": "https://avatars.githubusercontent.com/u/62144407?v=4", 156 | "profile": "https://github.com/AbdelHajou", 157 | "contributions": [ 158 | "ideas" 159 | ] 160 | }, 161 | { 162 | "login": "barrantesgerman", 163 | "name": "Herman Barrantes", 164 | "avatar_url": "https://avatars.githubusercontent.com/u/1646195?v=4", 165 | "profile": "https://www.hermanbarrantes.dev/", 166 | "contributions": [ 167 | "ideas" 168 | ] 169 | }, 170 | { 171 | "login": "aalmiray", 172 | "name": "Andres Almiray", 173 | "avatar_url": "https://avatars.githubusercontent.com/u/13969?v=4", 174 | "profile": "https://andresalmiray.com/", 175 | "contributions": [ 176 | "ideas" 177 | ] 178 | }, 179 | { 180 | "login": "maddingo", 181 | "name": "Martin Goldhahn", 182 | "avatar_url": "https://avatars.githubusercontent.com/u/362120?v=4", 183 | "profile": "https://github.com/maddingo", 184 | "contributions": [ 185 | "code" 186 | ] 187 | }, 188 | { 189 | "login": "maxandersen", 190 | "name": "Max Rydahl Andersen", 191 | "avatar_url": "https://avatars.githubusercontent.com/u/54129?v=4", 192 | "profile": "https://xam.dk", 193 | "contributions": [ 194 | "ideas", 195 | "code" 196 | ] 197 | }, 198 | { 199 | "login": "jreleaser", 200 | "name": "JReleaser", 201 | "avatar_url": "https://avatars.githubusercontent.com/u/81020166?v=4", 202 | "profile": "https://jreleaser.org", 203 | "contributions": [ 204 | "tool" 205 | ] 206 | } 207 | ] 208 | } 209 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | 794ddb1f7943af888bafb9dcc0930f775b6e0cbc -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | - package-ecosystem: github-actions 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | time: "04:00" 14 | open-pull-requests-limit: 10 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | if: contains(github.event.head_commit.message, 'Releasing version') != true && contains(github.event.head_commit.message, 'Prepare next version') != true 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Check out repository 16 | uses: actions/checkout@v4.2.2 17 | with: 18 | # Disabling shallow clone is recommended for improving relevancy of reporting 19 | fetch-depth: 0 20 | - name: Set up JDK 21 | uses: actions/setup-java@v4.7.1 22 | with: 23 | java-version: 21 24 | distribution: 'adopt' 25 | cache: maven 26 | - name: Cache Maven packages 27 | id: restore-maven-package-cache 28 | uses: actions/cache/restore@v4.2.3 29 | with: 30 | path: ~/.m2 31 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 32 | restore-keys: ${{ runner.os }}-m2 33 | - name: Build with Maven 34 | run: mvn -B verify 35 | - name: Save downloaded Maven packages 36 | uses: actions/cache/save@v4.2.3 37 | with: 38 | path: ~/.m2 39 | key: ${{ steps.restore-maven-package-cache.outputs.cache-primary-key }} 40 | if: github.event_name != 'pull_request' -------------------------------------------------------------------------------- /.github/workflows/early-access.yml: -------------------------------------------------------------------------------- 1 | # Inspired by & copied from JReleaser sample: 2 | # https://github.com/jreleaser/jreleaser/blob/main/.github/workflows/trigger-early-access.yml 3 | 4 | name: Publish Early Access builds 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | # Build native executable per runner 14 | build: 15 | if: contains(github.event.head_commit.message, 'Releasing version') != true && contains(github.event.head_commit.message, 'Prepare next version') != true 16 | name: build-${{ matrix.os }} 17 | strategy: 18 | fail-fast: true 19 | matrix: 20 | os: [ ubuntu-latest, macos-13, macos-14, windows-latest ] 21 | gu-binary: [ gu, gu.cmd ] 22 | exclude: 23 | - os: ubuntu-latest 24 | gu-binary: gu.cmd 25 | - os: macos-13 26 | gu-binary: gu.cmd 27 | - os: macos-14 28 | gu-binary: gu.cmd 29 | - os: windows-latest 30 | gu-binary: gu 31 | runs-on: ${{ matrix.os }} 32 | 33 | steps: 34 | - name: Download all build artifacts 35 | uses: actions/download-artifact@v4.3.0 36 | 37 | - name: Check out repository 38 | uses: actions/checkout@v4.2.2 39 | with: 40 | ref: ${{ steps.head.outputs.content }} 41 | 42 | # This action supports Windows; it does nothing on Linux and macOS. 43 | - name: Add Developer Command Prompt for Microsoft Visual C++ 44 | uses: ilammy/msvc-dev-cmd@v1.13.0 45 | 46 | - name: Set up JDK 47 | uses: actions/setup-java@v4.7.1 48 | with: 49 | distribution: 'graalvm' 50 | java-version: 21 51 | 52 | - name: Get musl toolchain and compile libz against it 53 | id: prepare-musl 54 | run: | 55 | TMP_DIR=$(mktemp -d) 56 | pushd $TMP_DIR 57 | curl -LOJ http://more.musl.cc/10/x86_64-linux-musl/x86_64-linux-musl-native.tgz 58 | tar -xvf x86_64-linux-musl-native.tgz 59 | 60 | curl -LOJ https://zlib.net/fossils/zlib-1.3.tar.gz 61 | tar -xzf zlib-1.3.tar.gz 62 | cd zlib-1.3 63 | 64 | TOOLCHAIN_DIR=$TMP_DIR/x86_64-linux-musl-native 65 | CC=$TOOLCHAIN_DIR/bin/gcc 66 | 67 | ./configure --prefix=$TOOLCHAIN_DIR --static 68 | make 69 | make install 70 | 71 | echo "TOOLCHAIN_DIR=$TOOLCHAIN_DIR" >> $GITHUB_OUTPUT 72 | if: matrix.os == 'ubuntu-latest' 73 | 74 | - name: Cache Maven packages 75 | id: restore-maven-package-cache 76 | uses: actions/cache/restore@v4.2.3 77 | with: 78 | path: ~/.m2 79 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 80 | restore-keys: ${{ runner.os }}-m2 81 | 82 | - name: Build static native image for Linux 83 | run: | 84 | PATH=${TOOLCHAIN_DIR}/bin:$PATH; mvn -B -Pnative package 85 | env: 86 | TOOLCHAIN_DIR: ${{ steps.prepare-musl.outputs.TOOLCHAIN_DIR }} 87 | if: matrix.os == 'ubuntu-latest' 88 | 89 | - name: Build static native image for Windows / macOS 90 | run: | 91 | mvn -B -Pnative package 92 | if: matrix.os != 'ubuntu-latest' 93 | 94 | - name: Create distribution 95 | run: mvn -B -Pdist package -DskipTests 96 | 97 | - name: Upload build artifacts 98 | uses: actions/upload-artifact@v4.6.2 99 | with: 100 | name: artifacts-${{ matrix.os }} 101 | path: | 102 | target/distributions/*.zip 103 | target/distributions/*.tar.gz 104 | 105 | - name: Save downloaded Maven packages 106 | uses: actions/cache/save@v4.2.3 107 | with: 108 | path: ~/.m2 109 | key: ${{ steps.restore-maven-package-cache.outputs.cache-primary-key }} 110 | if: github.event_name != 'pull_request' 111 | 112 | # Collect all executables and release 113 | release: 114 | needs: [ build ] 115 | runs-on: ubuntu-latest 116 | permissions: write-all 117 | if: github.event_name != 'pull_request' 118 | 119 | steps: 120 | - name: Check out repository 121 | uses: actions/checkout@v4.2.2 122 | with: 123 | fetch-depth: 0 124 | 125 | - name: Check out correct Git ref 126 | run: git checkout ${{ steps.head.outputs.content }} 127 | 128 | - name: Download all build artifacts 129 | uses: actions/download-artifact@v4.3.0 130 | with: 131 | path: /tmp/artifacts 132 | 133 | - name: Move build artifacts to correct folder 134 | run: | 135 | targets=("ubuntu-latest" "macos-13" "macos-14" "windows-latest") 136 | 137 | mkdir -p artifacts 138 | 139 | find /tmp/artifacts/ -name "mcs*" -exec mv -v {} artifacts/ \; 140 | 141 | - name: Cache Maven packages 142 | uses: actions/cache@v4.2.3 143 | with: 144 | path: ~/.m2 145 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 146 | restore-keys: ${{ runner.os }}-m2 147 | 148 | - name: Release with JReleaser 149 | run: mvn -B -Prelease -DartifactsDir=artifacts jreleaser:full-release 150 | env: 151 | JRELEASER_GITHUB_TOKEN: ${{ secrets.GH_PAT }} 152 | 153 | - name: Capture JReleaser output 154 | if: always() 155 | uses: actions/upload-artifact@v4.6.2 156 | with: 157 | name: jreleaser-release-output 158 | retention-days: 7 159 | path: | 160 | target/jreleaser/trace.log 161 | target/jreleaser/output.properties -------------------------------------------------------------------------------- /.github/workflows/mutation-testing.yml: -------------------------------------------------------------------------------- 1 | name: Mutation testing 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | mutation-testing: 11 | if: contains(github.event.head_commit.message, 'Releasing version') != true && contains(github.event.head_commit.message, 'Prepare next version') != true 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Check out repository 16 | uses: actions/checkout@v4.2.2 17 | with: 18 | # Disabling shallow clone is recommended for improving relevancy of reporting 19 | fetch-depth: 0 20 | 21 | - name: Cache Maven packages 22 | uses: actions/cache@v4.2.3 23 | with: 24 | path: ~/.m2 25 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 26 | restore-keys: ${{ runner.os }}-m2 27 | 28 | - name: Set up JDK 29 | uses: actions/setup-java@v4.7.1 30 | with: 31 | java-version: 21 32 | distribution: 'adopt' 33 | cache: maven 34 | 35 | - name: Run Pitest 36 | run: mvn -B test-compile org.pitest:pitest-maven:mutationCoverage 37 | env: 38 | STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Inspired by & copied from JReleaser sample: 2 | # https://github.com/aalmiray/q-cli/blob/main/.github/workflows/release.yml 3 | 4 | name: Release 5 | 6 | on: 7 | workflow_dispatch: 8 | inputs: 9 | version: 10 | description: "Release version" 11 | required: true 12 | next: 13 | description: "Next version" 14 | required: false 15 | 16 | jobs: 17 | version: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Check out repository 22 | uses: actions/checkout@v4.2.2 23 | 24 | - name: Set up Java 25 | uses: actions/setup-java@v4.7.1 26 | with: 27 | java-version: 21 28 | distribution: 'adopt' 29 | 30 | - name: Cache Maven packages 31 | uses: actions/cache/restore@v4.2.3 32 | with: 33 | path: ~/.m2 34 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 35 | restore-keys: ${{ runner.os }}-m2 36 | 37 | - name: Set release version 38 | id: version 39 | run: | 40 | RELEASE_VERSION=${{ github.event.inputs.version }} 41 | NEXT_VERSION=${{ github.event.inputs.next }} 42 | PLAIN_VERSION=`echo ${RELEASE_VERSION} | awk 'match($0, /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)/) { print substr($0, RSTART, RLENGTH); }'` 43 | COMPUTED_NEXT_VERSION="${PLAIN_VERSION}-SNAPSHOT" 44 | if [ -z $NEXT_VERSION ] 45 | then 46 | NEXT_VERSION=$COMPUTED_NEXT_VERSION 47 | fi 48 | mvn -B versions:set versions:commit -DnewVersion=$RELEASE_VERSION 49 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 50 | git config --global user.name "GitHub Action" 51 | git commit -a -m "chore: Releasing version $RELEASE_VERSION" 52 | git push origin HEAD:main 53 | git rev-parse HEAD > HEAD 54 | echo $RELEASE_VERSION > RELEASE_VERSION 55 | echo $PLAIN_VERSION > PLAIN_VERSION 56 | echo $NEXT_VERSION > NEXT_VERSION 57 | 58 | - name: Upload version files 59 | uses: actions/upload-artifact@v4.6.2 60 | with: 61 | name: artifacts 62 | path: | 63 | HEAD 64 | *_VERSION 65 | 66 | # Build native executable per runner 67 | build: 68 | needs: [ version ] 69 | name: build-${{ matrix.os }} 70 | strategy: 71 | fail-fast: true 72 | matrix: 73 | os: [ ubuntu-latest, macos-13, macos-14, windows-latest ] 74 | gu-binary: [ gu, gu.cmd ] 75 | exclude: 76 | - os: ubuntu-latest 77 | gu-binary: gu.cmd 78 | - os: macos-13 79 | gu-binary: gu.cmd 80 | - os: macos-14 81 | gu-binary: gu.cmd 82 | - os: windows-latest 83 | gu-binary: gu 84 | runs-on: ${{ matrix.os }} 85 | 86 | steps: 87 | - name: Download all build artifacts 88 | uses: actions/download-artifact@v4.3.0 89 | 90 | - name: Read HEAD ref 91 | id: head 92 | uses: juliangruber/read-file-action@v1.1.7 93 | with: 94 | path: artifacts/HEAD 95 | 96 | - name: Check out repository 97 | uses: actions/checkout@v4.2.2 98 | with: 99 | ref: ${{ steps.head.outputs.content }} 100 | 101 | # This action supports Windows; it does nothing on Linux and macOS. 102 | - name: Add Developer Command Prompt for Microsoft Visual C++ 103 | uses: ilammy/msvc-dev-cmd@v1.13.0 104 | 105 | - name: Set up JDK 106 | uses: actions/setup-java@v4.7.1 107 | with: 108 | distribution: 'graalvm' 109 | java-version: 21 110 | 111 | - name: Get musl toolchain and compile libz against it 112 | id: prepare-musl 113 | run: | 114 | TMP_DIR=$(mktemp -d) 115 | pushd $TMP_DIR 116 | curl -LOJ http://more.musl.cc/10/x86_64-linux-musl/x86_64-linux-musl-native.tgz 117 | tar -xvf x86_64-linux-musl-native.tgz 118 | 119 | curl -LOJ https://zlib.net/fossils/zlib-1.3.tar.gz 120 | tar -xzf zlib-1.3.tar.gz 121 | cd zlib-1.3 122 | 123 | TOOLCHAIN_DIR=$TMP_DIR/x86_64-linux-musl-native 124 | CC=$TOOLCHAIN_DIR/bin/gcc 125 | 126 | ./configure --prefix=$TOOLCHAIN_DIR --static 127 | make 128 | make install 129 | 130 | echo "TOOLCHAIN_DIR=$TOOLCHAIN_DIR" >> $GITHUB_OUTPUT 131 | if: matrix.os == 'ubuntu-latest' 132 | 133 | - name: Cache Maven packages 134 | uses: actions/cache@v4.2.3 135 | with: 136 | path: ~/.m2 137 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 138 | restore-keys: ${{ runner.os }}-m2 139 | 140 | - name: Build static native image for Linux 141 | run: | 142 | PATH=${TOOLCHAIN_DIR}/bin:$PATH; mvn -B -Pnative package 143 | env: 144 | TOOLCHAIN_DIR: ${{ steps.prepare-musl.outputs.TOOLCHAIN_DIR }} 145 | if: matrix.os == 'ubuntu-latest' 146 | 147 | - name: Build static native image for Windows / macOS 148 | run: | 149 | mvn -B -Pnative package 150 | if: matrix.os != 'ubuntu-latest' 151 | 152 | - name: Create distribution 153 | run: mvn -B -Pdist package -DskipTests 154 | 155 | - name: Upload build artifacts 156 | uses: actions/upload-artifact@v4.6.2 157 | with: 158 | name: artifacts-${{ matrix.os }} 159 | path: | 160 | target/distributions/*.zip 161 | target/distributions/*.tar.gz 162 | 163 | # Collect all executables and release 164 | release: 165 | needs: [ build ] 166 | runs-on: ubuntu-latest 167 | permissions: write-all 168 | 169 | steps: 170 | # must read HEAD before checkout 171 | - name: Download all build artifacts 172 | uses: actions/download-artifact@v4.3.0 173 | 174 | - name: Read HEAD ref 175 | id: head 176 | uses: juliangruber/read-file-action@v1.1.7 177 | with: 178 | path: artifacts/HEAD 179 | 180 | - name: Read versions 181 | id: version 182 | run: | 183 | RELEASE_VERSION=`cat artifacts/RELEASE_VERSION` 184 | PLAIN_VERSION=`cat artifacts/PLAIN_VERSION` 185 | NEXT_VERSION=`cat artifacts/NEXT_VERSION` 186 | echo "RELEASE_VERSION = $RELEASE_VERSION" 187 | echo "PLAIN_VERSION = $PLAIN_VERSION" 188 | echo "NEXT_VERSION = $NEXT_VERSION" 189 | echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_OUTPUT 190 | echo "PLAIN_VERSION=$PLAIN_VERSION" >> $GITHUB_OUTPUT 191 | echo "NEXT_VERSION=$NEXT_VERSION" >> $GITHUB_OUTPUT 192 | 193 | - name: Check out repository 194 | uses: actions/checkout@v4.2.2 195 | with: 196 | fetch-depth: 0 197 | 198 | - name: Check out correct Git ref 199 | run: git checkout ${{ steps.head.outputs.content }} 200 | 201 | # checkout will clobber downloaded artifacts; we have to download them again 202 | - name: Download all build artifacts 203 | uses: actions/download-artifact@v4.3.0 204 | with: 205 | path: /tmp/artifacts 206 | 207 | - name: Move build artifacts to correct folder 208 | run: | 209 | targets=("ubuntu-latest" "macos-13" "macos-14" "windows-latest") 210 | 211 | mkdir -p artifacts 212 | 213 | find /tmp/artifacts/ -name "mcs*" -exec mv -v {} artifacts/ \; 214 | 215 | - name: Set up Java 216 | uses: actions/setup-java@v4.7.1 217 | with: 218 | java-version: 21 219 | distribution: 'adopt' 220 | 221 | - name: Cache Maven packages 222 | uses: actions/cache@v4.2.3 223 | with: 224 | path: ~/.m2 225 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 226 | restore-keys: ${{ runner.os }}-m2 227 | 228 | - name: Release with JReleaser 229 | run: mvn -B -Prelease -DartifactsDir=artifacts jreleaser:full-release 230 | env: 231 | JRELEASER_GITHUB_TOKEN: ${{ secrets.GH_PAT }} 232 | JRELEASER_HOMEBREW_GITHUB_TOKEN: ${{ secrets.GH_PAT }} 233 | JRELEASER_SNAP_GITHUB_TOKEN: ${{ secrets.GH_PAT }} 234 | JRELEASER_CHOCOLATEY_GITHUB_TOKEN: ${{ secrets.GH_PAT }} 235 | JRELEASER_SCOOP_GITHUB_TOKEN: ${{ secrets.GH_PAT }} 236 | JRELEASER_SDKMAN_CONSUMER_KEY: ${{ secrets.JRELEASER_SDKMAN_CONSUMER_KEY }} 237 | JRELEASER_SDKMAN_CONSUMER_TOKEN: ${{ secrets.JRELEASER_SDKMAN_CONSUMER_TOKEN }} 238 | JRELEASER_TWITTER_CONSUMER_KEY: ${{ secrets.JRELEASER_TWITTER_CONSUMER_KEY }} 239 | JRELEASER_TWITTER_CONSUMER_SECRET: ${{ secrets.JRELEASER_TWITTER_CONSUMER_SECRET }} 240 | JRELEASER_TWITTER_ACCESS_TOKEN: ${{ secrets.JRELEASER_TWITTER_ACCESS_TOKEN }} 241 | JRELEASER_TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.JRELEASER_TWITTER_ACCESS_TOKEN_SECRET }} 242 | JRELEASER_MASTODON_ACCESS_TOKEN: ${{ secrets.JRELEASER_MASTODON_ACCESS_TOKEN }} 243 | JRELEASER_BLUESKY_PASSWORD: ${{ secrets.JRELEASER_BLUESKY_PASSWORD }} 244 | 245 | - name: Capture JReleaser output 246 | if: always() 247 | uses: actions/upload-artifact@v4.6.2 248 | with: 249 | name: jreleaser-release-output 250 | retention-days: 7 251 | path: | 252 | target/jreleaser/trace.log 253 | target/jreleaser/output.properties 254 | 255 | - name: Set next version 256 | env: 257 | NEXT_VERSION: ${{ steps.version.outputs.NEXT_VERSION }} 258 | run: | 259 | mvn -B versions:set versions:commit -DnewVersion=$NEXT_VERSION 260 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 261 | git config --global user.name "GitHub Action" 262 | git commit -a -m "chore: Prepare next version: $NEXT_VERSION" 263 | git push origin HEAD:main 264 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Maven 2 | target/ 3 | 4 | # IntelliJ 5 | .idea/ 6 | *.iml 7 | 8 | # Eclipse 9 | .classpath 10 | .project -------------------------------------------------------------------------------- /.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full 2 | 3 | USER gitpod 4 | 5 | RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh && \ 6 | sdk install java 22.1.0.r17-grl && \ 7 | sdk default java 22.1.0.r17-grl" -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .gitpod.Dockerfile 3 | tasks: 4 | - init: mvn verify 5 | -------------------------------------------------------------------------------- /.mvn/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthmulders/mcs/742e33aa4d96220939ac4ca938ca6e3e0e494418/.mvn/.gitkeep -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | 2 | [![All Contributors](https://img.shields.io/badge/all_contributors-21-orange.svg?style=flat-square)](#contributors-) 3 | 4 | ## Contributors ✨ 5 | 6 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
Willem van Lent
Willem van Lent

💻
Hanno Embregts
Hanno Embregts

💻 🤔
Branislav Beňo
Branislav Beňo

💻
Giovanni van der Schelde
Giovanni van der Schelde

💻
Martin Bonnin
Martin Bonnin

🤔
bot_bkcd
bot_bkcd

💻
Usman Shaikh
Usman Shaikh

💻
Jan Wedel
Jan Wedel

💻
Markus Friedli
Markus Friedli

🐛
Sebastian Lövdahl
Sebastian Lövdahl

🐛
Daniel Hinojosa
Daniel Hinojosa

🐛
Spencer Gibb
Spencer Gibb

🤔
Jeff MAURY
Jeff MAURY

🐛 🤔
Josef Ludvicek
Josef Ludvicek

🐛
Benjamin Marwell
Benjamin Marwell

🤔
Abdel Hajou
Abdel Hajou

🤔
Herman Barrantes
Herman Barrantes

🤔
Andres Almiray
Andres Almiray

🤔
Martin Goldhahn
Martin Goldhahn

💻
Max Rydahl Andersen
Max Rydahl Andersen

🤔 💻
JReleaser
JReleaser

🔧
42 | 43 | 44 | 45 | 46 | 47 | 48 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Maarten Mulders 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Maven Central Search 2 | 3 | [![Build status](https://github.com/mthmulders/mcs/actions/workflows/build.yml/badge.svg)](https://github.com/mthmulders/mcs/actions/workflows/build.yml) 4 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fmthmulders%2Fmcs%2Fmain)](https://dashboard.stryker-mutator.io/reports/github.com/mthmulders/mcs/main) 5 | [![Snapcraft.io status](https://snapcraft.io/maven-central-search/badge.svg)](https://snapcraft.io/maven-central-search) 6 | 7 | > Use [Maven Central Repository Search](https://search.maven.org/) from your command line! 8 | 9 | Use `mcs` to quickly lookup dependency coordinates in Maven Central, without having to switch to your browser. 10 | 11 | ## Usage 12 | 13 | This tool supports the following modes of searching: 14 | 15 | 1. **Wildcard search** 16 | 17 | ```console 18 | mcs search plexus-utils 19 | ``` 20 | 21 | This will give you all artifacts in Maven Central that have "plexus-utils" in their name. 22 | The output is in a tabular form, showing the exact coordinate of each artifact and the moment when its latest version was deployed. 23 | 24 | 2. **Coordinate search** 25 | 26 | ```console 27 | mcs search org.codehaus.plexus:plexus-utils 28 | mcs search org.codehaus.plexus:plexus-utils:3.4.1 29 | ``` 30 | 31 | If there are multiple hits, you will get the same table output as above. 32 | But if there's only one hit, this will give you by default a pom.xml snippet for the artifact you searched for. 33 | Ready for copy & paste in your favourite IDE! 34 | If you require snippet in different format, use `-f ` or `--format=`. 35 | Supported types are: `maven`, `gradle`, `gradle-short`, `gradle-kotlin`, `sbt`, `ivy`, `grape`, `leiningen`, `buildr`, `jbang`, `gav`. 36 | 37 | 3. **Class-name search** 38 | 39 | ```console 40 | mcs class-search CommandLine 41 | mcs class-search -f picocli.CommandLine 42 | ``` 43 | 44 | This will give you all artifacts in Maven Central that contain a particular class. 45 | If you set the `-f` flag, the search term is considered a "fully classified" class name, so including the package name. 46 | 47 | ## Flags 48 | 49 | * All modes recognise the `-l ` switch, which lets you specify how many results you want to see _at most_. 50 | * In **Wildcard sarch** and **Coordinate search**, you can pass along the `-s` (or `--show-vulnerabilities`) flag. 51 | It will cause MCS to show a summary of reported security vulnerabilities against each result. 52 | If there is only one search result, it will display the CVE numbers reported against that result. 53 | **Note** that this feature will probably soon hit the API limits for the Sonatype OSS Index. See [their documentation](https://ossindex.sonatype.org) for details on how this may impact your usage. 54 | You can specify your credentials using the system properties `ossindex.username` and `ossindex.password`. 55 | See under "Configuring MCS" on how to do this in the most convenient way. 56 | 57 | ## Installation 58 | 59 | You can install mcs using the package manager of your choice: 60 | 61 | | Package manager | Platform | Installation | Remarks | 62 | |-----------------|----------|-------------------------------------|---------| 63 | | **Homebrew** | 🍎 🐧 | `brew install mthmulders/tap/mcs` | ⚠️ 1 | 64 | | **Snap** | 🐧 | `snap install maven-central-search` | | 65 | | **SDKMAN!** | 🍎 🐧 | `sdk install mcs` | | 66 | | **Chocolatey** | 🪟 | `choco install mcs` | | 67 | | **Scoop** | 🪟 | `scoop install mthmulders/mcs` | | 68 | 69 | 1. The Linux binaries only work on x86_64 CPU's. 70 | There Apple binaries for both x86_64 and Apple Silicon, so you don't need Rosetta. 71 | 72 | ### Usage with custom trust store 73 | 74 | In certain situations, such as when you work behind a TLS-intercepting (corporate) firewall, MCS may fail with 75 | 76 | > PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target 77 | 78 | In layman's speak: the default, built-in trust store (the set of trusted X.509 certificates) does not contain anything that allows to trust the certificate(s) presented by the server. 79 | Maven Central uses a certificate that would've been trusted, but the culprit here is the TLS-intercepting (corporate) firewall that presents an internal certificate. 80 | 81 | The solution is to create a trust store that has the "highest" certificate in the certificate chain, e.g. that of the (internal) certificate authority. 82 | You can use a tool like [Portecle](https://portecle.sourceforge.net/) to create such a trust store. 83 | Next, point MCS to that trust store like so 84 | 85 | ``` 86 | mcs -Djavax.net.ssl.trustStore=/path/to/keystore search something 87 | ``` 88 | 89 | ### Usage Behind a Proxy 90 | 91 | If you are running behind a proxy, MCS will respect the `HTTP_PROXY` and `HTTPS_PROXY` environment variables. 92 | 93 | ## Configuring MCS 94 | 95 | Some configuration for MCS is passed through system properties. 96 | You can do this every time you invoke MCS by adding `-Dxxx=yyy`. 97 | To make it more conveniently, you can create a configuration file that will automatically be read by MCS and interpreted as configuration settings. 98 | 99 | To do so, create a directory **.mcs** in your user directory (typically **C:\Users\** on 🪟, **/home/** on 🐧 or **/Users/** on 🍎). 100 | Inside that folder, create a file **mcs.config** and write the following line in it: 101 | 102 | ``` 103 | javax.net.ssl.trustStore=/path/to/keystore 104 | ossindex.username=xxx 105 | ossindex.password=yyy 106 | ``` 107 | 108 | This way, you don't have to remember passing the `-D`. 109 | 110 | ## Contributing 111 | 112 | Probably the easiest way to get a working development environment is to use Gitpod: 113 | 114 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/mthmulders/mcs) 115 | 116 | It will configure a workspace in your browser and show that everything works as expected by running `mvn verify`. 117 | This setup does not touch your computer - as soon as you close your browser tab, it's gone. 118 | 119 | Checkout the [issues](https://github.com/mthmulders/mcs/issues) if you're looking for something to work on. 120 | If you have a new idea, feel free to bring it up using the [discussions](https://github.com/mthmulders/mcs/discussions). 121 | 122 | ## Acknowledgements 123 | 124 | MCS would not have been possible without the contributions of wonderful people around the globe. 125 | The full list is in [CONTRIBUTORS.md](./CONTRIBUTORS.md). 126 | -------------------------------------------------------------------------------- /jreleaser.yml: -------------------------------------------------------------------------------- 1 | project: 2 | name: mcs 3 | description: Maven Central Search 4 | longDescription: Search Maven Central from your command line! 5 | links: 6 | bugTracker: https://github.com/mthmulders/mcs/issues 7 | homepage: https://maarten.mulders.it/projects/mcs/ 8 | vcsBrowser: https://github.com/mthmulders/mcs 9 | authors: 10 | - Maarten Mulders 11 | license: MIT 12 | extraProperties: 13 | inceptionYear: 2021 14 | stereotype: CLI 15 | tags: 16 | - 'maven' 17 | - 'development' 18 | - 'cli' 19 | 20 | release: 21 | github: 22 | enabled: true 23 | owner: mthmulders 24 | name: mcs 25 | username: mthmulders 26 | update: 27 | enabled: true 28 | changelog: 29 | formatted: ALWAYS 30 | preset: 'conventional-commits' 31 | links: true 32 | hide: 33 | contributors: 34 | - 'Maarten Mulders' 35 | - '[bot]' 36 | - 'GitHub' 37 | 38 | announce: 39 | mastodon: 40 | active: RELEASE 41 | host: https://mastodon.online/ 42 | status: 🚀 {{projectName}} {{projectVersion}} has just been released! Check {{releaseNotesUrl}} for the details. 43 | bluesky: 44 | active: RELEASE 45 | host: https://bsky.social 46 | handle: mthmulders.bsky.social 47 | status: 🚀 {{projectName}} {{projectVersion}} has just been released! Check {{releaseNotesUrl}} for the details. 48 | twitter: 49 | active: RELEASE 50 | status: 🚀 {{projectName}} {{projectVersion}} has just been released! Check {{releaseNotesUrl}} for the details. 51 | 52 | distributions: 53 | mcs: 54 | stereotype: CLI 55 | type: BINARY 56 | brew: 57 | active: RELEASE 58 | multiPlatform: true 59 | repository: 60 | active: RELEASE 61 | chocolatey: 62 | active: RELEASE 63 | repository: 64 | active: RELEASE 65 | remoteBuild: true 66 | iconUrl: https://maarten.mulders.it/img/mcs-icon.png 67 | title: Maven Central Search 68 | sdkman: 69 | active: RELEASE 70 | snap: 71 | active: RELEASE 72 | base: core 73 | localPlugs: 74 | - network 75 | architectures: 76 | - buildOn: [amd64] 77 | runOn: [amd64] 78 | repository: 79 | active: RELEASE 80 | name: mcs-snap 81 | owner: mthmulders 82 | packageName: maven-central-search 83 | remoteBuild: true 84 | scoop: 85 | active: RELEASE 86 | repository: 87 | active: RELEASE 88 | name: scoop-mcs 89 | artifacts: 90 | - path: ./{{artifactsDir}}/{{distributionName}}-{{projectVersion}}-linux-x86_64.zip 91 | platform: linux-x86_64 92 | extraProperties: 93 | graalVMNativeImage: true 94 | - path: ./{{artifactsDir}}/{{distributionName}}-{{projectVersion}}-linux-x86_64.tar.gz 95 | platform: linux-x86_64 96 | extraProperties: 97 | graalVMNativeImage: true 98 | - path: ./{{artifactsDir}}/{{distributionName}}-{{projectVersion}}-windows-x86_64.zip 99 | platform: windows-x86_64 100 | - path: ./{{artifactsDir}}/{{distributionName}}-{{projectVersion}}-osx-x86_64.zip 101 | platform: osx-x86_64 102 | extraProperties: 103 | graalVMNativeImage: true 104 | - path: ./{{artifactsDir}}/{{distributionName}}-{{projectVersion}}-osx-aarch_64.zip 105 | platform: osx-aarch_64 106 | extraProperties: 107 | graalVMNativeImage: true 108 | -------------------------------------------------------------------------------- /src/main/assembly/assembly.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | dist 7 | 8 | tar.gz 9 | zip 10 | dir 11 | 12 | 13 | 14 | LICENSE 15 | ./ 16 | 17 | 18 | ${project.build.directory}/${project.artifactId}-${project.version}${executable-suffix} 19 | ./bin 20 | ${project.artifactId}${executable-suffix} 21 | 0755 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/graalvm-native/resource-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources":{ 3 | "includes":[{ 4 | "pattern":"mcs.properties" 5 | }]}, 6 | "bundles":[] 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/App.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs; 2 | 3 | import it.mulders.mcs.dagger.Application; 4 | import it.mulders.mcs.dagger.DaggerApplication; 5 | import java.net.URI; 6 | import java.net.URISyntaxException; 7 | import picocli.CommandLine; 8 | 9 | public class App { 10 | public static void main(final String... args) { 11 | System.exit(doMain(args)); 12 | } 13 | 14 | // Visible for testing 15 | static int doMain(final String... originalArgs) { 16 | final Application components = DaggerApplication.create(); 17 | 18 | var systemPropertyLoader = components.systemPropertyLoader(); 19 | System.setProperties(systemPropertyLoader.getProperties()); 20 | setUpProxy(); 21 | 22 | var commandLine = components.commandLine(); 23 | 24 | var args = isInvocationWithoutSearchCommand(commandLine, originalArgs) 25 | ? prependSearchCommandToArgs(originalArgs) 26 | : originalArgs; 27 | 28 | return commandLine.execute(args); 29 | } 30 | 31 | private static void setUpProxy() { 32 | var httpProxy = System.getenv("HTTP_PROXY"); 33 | var httpsProxy = System.getenv("HTTPS_PROXY"); 34 | 35 | try { 36 | if (httpProxy != null && !httpProxy.isEmpty()) { 37 | final URI uri = new URI(httpProxy); 38 | 39 | System.setProperty("http.proxyHost", uri.getHost()); 40 | System.setProperty("http.proxyPort", Integer.toString(uri.getPort())); 41 | } 42 | 43 | if (httpsProxy != null && !httpsProxy.isEmpty()) { 44 | final URI uri = new URI(httpsProxy); 45 | System.setProperty("https.proxyHost", uri.getHost()); 46 | System.setProperty("https.proxyPort", Integer.toString(uri.getPort())); 47 | } 48 | } catch (URISyntaxException e) { 49 | System.err.printf( 50 | "Error while setting up proxy from environment: HTTP_PROXY=[%s], HTTPS_PROXY=[%s]%n", 51 | httpProxy, httpsProxy); 52 | } 53 | } 54 | 55 | static boolean isInvocationWithoutSearchCommand(CommandLine program, String... args) { 56 | try { 57 | program.parseArgs(args); 58 | return false; 59 | } catch (CommandLine.ParameterException pe1) { 60 | try { 61 | program.parseArgs(prependSearchCommandToArgs(args)); 62 | return true; 63 | } catch (CommandLine.ParameterException pe2) { 64 | return false; 65 | } 66 | } 67 | } 68 | 69 | static String[] prependSearchCommandToArgs(String... originalArgs) { 70 | var args = new String[originalArgs.length + 1]; 71 | args[0] = "search"; 72 | System.arraycopy(originalArgs, 0, args, 1, originalArgs.length); 73 | 74 | return args; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/cli/ClassSearchCommand.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.cli; 2 | 3 | import it.mulders.mcs.search.SearchCommandHandler; 4 | import it.mulders.mcs.search.SearchQuery; 5 | import jakarta.inject.Inject; 6 | import java.util.concurrent.Callable; 7 | import picocli.CommandLine; 8 | 9 | @CommandLine.Command( 10 | name = "class-search", 11 | description = "Search artifacts in Maven Central by class name", 12 | usageHelpAutoWidth = true) 13 | public class ClassSearchCommand implements Callable { 14 | @CommandLine.Parameters( 15 | arity = "1", 16 | description = { 17 | "The class name to search for.", 18 | }) 19 | private String query; 20 | 21 | @CommandLine.Option( 22 | names = {"-f", "--full-name"}, 23 | negatable = true, 24 | arity = "0", 25 | description = "Class name includes package") 26 | private boolean fullName; 27 | 28 | @CommandLine.Option( 29 | names = {"-l", "--limit"}, 30 | description = "Show results", 31 | paramLabel = "") 32 | private Integer limit; 33 | 34 | private final SearchCommandHandler searchCommandHandler; 35 | 36 | @Inject 37 | public ClassSearchCommand(final SearchCommandHandler searchCommandHandler) { 38 | this.searchCommandHandler = searchCommandHandler; 39 | } 40 | 41 | // Visible for testing 42 | ClassSearchCommand(final SearchCommandHandler searchCommandHandler, String query, Integer limit, boolean fullName) { 43 | this(searchCommandHandler); 44 | this.fullName = fullName; 45 | this.limit = limit; 46 | this.query = query; 47 | } 48 | 49 | @Override 50 | public Integer call() { 51 | System.out.printf("Searching for artifacts containing %s...%n", query); 52 | var searchQuery = SearchQuery.classSearch(this.query) 53 | .isFullyQualified(this.fullName) 54 | .withLimit(limit) 55 | .build(); 56 | searchCommandHandler.search(searchQuery, "maven", false); 57 | return 0; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/cli/ClasspathVersionProvider.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.cli; 2 | 3 | import java.util.Properties; 4 | import picocli.CommandLine; 5 | 6 | /** 7 | * {@link CommandLine.IVersionProvider} implementation that returns version information from a 8 | * {@code /mcs.properties} file in the classpath. 9 | */ 10 | public class ClasspathVersionProvider implements CommandLine.IVersionProvider { 11 | @Override 12 | public String[] getVersion() throws Exception { 13 | var properties = new Properties(); 14 | try (var stream = getClass().getResourceAsStream("/mcs.properties")) { 15 | properties.load(stream); 16 | var version = String.format("mcs v%s", properties.getProperty("mcs.version")); 17 | return new String[] {version}; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/cli/Cli.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.cli; 2 | 3 | import jakarta.inject.Inject; 4 | import picocli.CommandLine; 5 | 6 | @CommandLine.Command( 7 | name = "mcs", 8 | subcommands = {SearchCommand.class, ClassSearchCommand.class}, 9 | usageHelpAutoWidth = true, 10 | versionProvider = ClasspathVersionProvider.class) 11 | public class Cli { 12 | 13 | @CommandLine.Option( 14 | names = {"-V", "--version"}, 15 | description = "Show version number", 16 | versionHelp = true) 17 | private boolean showVersion; 18 | 19 | @CommandLine.Option( 20 | names = {"-h", "--help"}, 21 | description = "Display this help message and exits", 22 | scope = CommandLine.ScopeType.INHERIT, 23 | usageHelp = true) 24 | private boolean usageHelpRequested; 25 | 26 | @Inject 27 | public Cli() {} 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/cli/SearchCommand.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.cli; 2 | 3 | import it.mulders.mcs.search.SearchCommandHandler; 4 | import it.mulders.mcs.search.SearchQuery; 5 | import jakarta.inject.Inject; 6 | import java.util.concurrent.Callable; 7 | import picocli.CommandLine; 8 | 9 | @CommandLine.Command( 10 | name = "search", 11 | description = "Search artifacts in Maven Central by coordinates", 12 | usageHelpAutoWidth = true) 13 | public class SearchCommand implements Callable { 14 | @CommandLine.Parameters( 15 | arity = "1..n", 16 | description = { 17 | "What to search for.", 18 | "If the search term contains a colon ( : ), it is considered a literal groupId and artifactId", 19 | "Otherwise, the search term is considered a wildcard search" 20 | }) 21 | private String[] query; 22 | 23 | @CommandLine.Option( 24 | names = {"-l", "--limit"}, 25 | description = "Show results", 26 | paramLabel = "") 27 | private Integer limit; 28 | 29 | @CommandLine.Option( 30 | names = {"-f", "--format"}, 31 | description = 32 | """ 33 | Show result in format 34 | Supported types are: 35 | maven, gradle, gradle-short, gradle-kotlin, sbt, ivy, grape, leiningen, buildr, jbang, gav 36 | """, 37 | paramLabel = "") 38 | private String responseFormat; 39 | 40 | @CommandLine.Option( 41 | names = {"-s", "--show-vulnerabilities"}, 42 | description = "Show reported security vulnerabilities", 43 | paramLabel = "") 44 | private boolean showVulnerabilities; 45 | 46 | private final SearchCommandHandler searchCommandHandler; 47 | 48 | @Inject 49 | public SearchCommand(final SearchCommandHandler searchCommandHandler) { 50 | this.searchCommandHandler = searchCommandHandler; 51 | } 52 | 53 | // Visible for testing 54 | SearchCommand( 55 | SearchCommandHandler searchCommandHandler, 56 | String[] query, 57 | Integer limit, 58 | String responseFormat, 59 | boolean showVulnerabilities) { 60 | this(searchCommandHandler); 61 | this.limit = limit; 62 | this.query = query; 63 | this.responseFormat = responseFormat; 64 | this.showVulnerabilities = showVulnerabilities; 65 | } 66 | 67 | @Override 68 | public Integer call() { 69 | var combinedQuery = String.join(" ", query); 70 | System.out.printf("Searching for %s...%n", combinedQuery); 71 | var searchQuery = 72 | SearchQuery.search(combinedQuery).withLimit(this.limit).build(); 73 | 74 | searchCommandHandler.search(searchQuery, responseFormat, showVulnerabilities); 75 | return 0; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/cli/SystemPropertyLoader.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.cli; 2 | 3 | import jakarta.inject.Inject; 4 | import java.io.IOException; 5 | import java.nio.file.Files; 6 | import java.nio.file.Path; 7 | import java.nio.file.Paths; 8 | import java.util.Properties; 9 | 10 | /** 11 | * Loads additional system properties from a predefined file on disk. 12 | *
13 | * Copies those properties over the existing ones @{{@link System#getProperties()}} so the result 14 | * is a drop-in replacement that could be passed for {@link System#setProperties(Properties)}. 15 | * This class does not modify the System properties itself. 16 | */ 17 | public class SystemPropertyLoader { 18 | private static final Path MCS_PROPERTIES_FILE = Paths.get(System.getProperty("user.home"), ".mcs", "mcs.config"); 19 | 20 | private final Properties properties = new Properties(); 21 | 22 | @Inject 23 | public SystemPropertyLoader() { 24 | this(MCS_PROPERTIES_FILE); 25 | } 26 | 27 | protected SystemPropertyLoader(final Path source) { 28 | properties.putAll(System.getProperties()); 29 | 30 | if (Files.exists(source) && Files.isRegularFile(source)) { 31 | var input = new Properties(); 32 | try (var reader = Files.newBufferedReader(source)) { 33 | input.load(reader); 34 | properties.putAll(input); 35 | } catch (IOException ioe) { 36 | System.err.printf("Failed to load %s: %s%n", source, ioe.getLocalizedMessage()); 37 | } 38 | } 39 | } 40 | 41 | public Properties getProperties() { 42 | return properties; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/common/McsExecutionExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.common; 2 | 3 | import jakarta.inject.Inject; 4 | import picocli.CommandLine; 5 | 6 | public class McsExecutionExceptionHandler implements CommandLine.IExecutionExceptionHandler { 7 | @Inject 8 | public McsExecutionExceptionHandler() {} 9 | 10 | @Override 11 | public int handleExecutionException(Exception ex, CommandLine commandLine, CommandLine.ParseResult parseResult) { 12 | var message = 13 | ex instanceof McsRuntimeException ? ex.getCause().getLocalizedMessage() : ex.getLocalizedMessage(); 14 | System.err.printf("MCS ran into an error: %s%n", message); 15 | System.err.printf("%n"); 16 | System.err.printf( 17 | "If the error persist, please consider reporting the issue at https://github.com/mthmulders/mcs/issues/new%n"); 18 | System.err.printf("%n"); 19 | return -1; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/common/McsRuntimeException.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.common; 2 | 3 | public class McsRuntimeException extends RuntimeException { 4 | public McsRuntimeException(Throwable cause) { 5 | super(cause); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/common/Result.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.common; 2 | 3 | import java.util.NoSuchElementException; 4 | import java.util.function.Consumer; 5 | import java.util.function.Function; 6 | 7 | public sealed interface Result permits Result.Success, Result.Failure { 8 | record Success(T value) implements Result { 9 | @Override 10 | public Result map(final Function mapping) { 11 | try { 12 | return new Success<>(mapping.apply(this.value)); 13 | } catch (final Throwable throwable) { 14 | return new Failure<>(throwable); 15 | } 16 | } 17 | 18 | @Override 19 | public void ifPresent(final Consumer consumer) { 20 | consumer.accept(this.value); 21 | } 22 | 23 | @Override 24 | public void ifPresentOrElse(Consumer successConsumer, Consumer failureConsumer) { 25 | successConsumer.accept(value); 26 | } 27 | 28 | @Override 29 | public Throwable cause() { 30 | throw new NoSuchElementException("success: " + this.value); 31 | } 32 | } 33 | 34 | record Failure(Throwable cause) implements Result { 35 | @Override 36 | public Result map(final Function mapping) { 37 | return (Failure) this; 38 | } 39 | 40 | @Override 41 | public void ifPresent(final Consumer consumer) {} 42 | 43 | @Override 44 | public void ifPresentOrElse(Consumer successConsumer, Consumer failureConsumer) { 45 | failureConsumer.accept(cause); 46 | } 47 | 48 | @Override 49 | public T value() { 50 | throw new NoSuchElementException("failure: " + this.cause.getLocalizedMessage()); 51 | } 52 | } 53 | 54 | Result map(final Function mapping); 55 | 56 | void ifPresent(final Consumer consumer); 57 | 58 | void ifPresentOrElse(final Consumer successConsumer, final Consumer failureConsumer); 59 | 60 | T value(); 61 | 62 | Throwable cause(); 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/common/SearchResponseBodyHandler.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.common; 2 | 3 | import com.fasterxml.jackson.core.JsonParseException; 4 | import com.fasterxml.jackson.jr.ob.JSON; 5 | import com.fasterxml.jackson.jr.ob.JSONObjectException; 6 | import it.mulders.mcs.search.SearchResponse; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.net.http.HttpResponse; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | public class SearchResponseBodyHandler implements HttpResponse.BodyHandler> { 14 | @Override 15 | public HttpResponse.BodySubscriber> apply(final HttpResponse.ResponseInfo responseInfo) { 16 | return asObject(); 17 | } 18 | 19 | static HttpResponse.BodySubscriber> asObject() { 20 | var upstream = HttpResponse.BodySubscribers.ofInputStream(); 21 | 22 | return HttpResponse.BodySubscribers.mapping(upstream, SearchResponseBodyHandler::toSearchResponse); 23 | } 24 | 25 | static Result toSearchResponse(final InputStream inputStream) { 26 | try (final InputStream input = inputStream) { 27 | var map = JSON.std.mapFrom(input); 28 | return new Result.Success<>(constructSearchResponse(map)); 29 | } catch (final JsonParseException | JSONObjectException joe) { 30 | return new Result.Failure<>( 31 | new IllegalStateException( 32 | """ 33 | Error parsing the search result. This may be a temporary failure from search.maven.org. 34 | It can also be caused by requesting a large number of results. If that is the case, try lowering the -l/--limit parameter. 35 | If the problem persists, please open a conversation at 36 | 37 | https://github.com/mthmulders/mcs/discussions 38 | 39 | Make sure to at least provide your invocation of mcs and the version of mcs you're using. 40 | """)); 41 | } catch (final IOException ioe) { 42 | return new Result.Failure<>( 43 | new IllegalStateException("Error processing response: %s%n".formatted(ioe.getLocalizedMessage()))); 44 | } 45 | } 46 | 47 | static SearchResponse constructSearchResponse(final Map input) { 48 | return new SearchResponse(null, constructResponse((Map) input.get("response"))); 49 | } 50 | 51 | private static SearchResponse.Response constructResponse(final Map input) { 52 | return new SearchResponse.Response( 53 | (int) input.get("numFound"), (int) input.get("start"), constructDocs((List>) 54 | input.get("docs"))); 55 | } 56 | 57 | private static SearchResponse.Response.Doc[] constructDocs(List> input) { 58 | return input.stream().map(SearchResponseBodyHandler::constructDoc).toArray(SearchResponse.Response.Doc[]::new); 59 | } 60 | 61 | private static SearchResponse.Response.Doc constructDoc(final Map input) { 62 | return new SearchResponse.Response.Doc( 63 | (String) input.get("id"), 64 | (String) input.get("g"), 65 | (String) input.get("a"), 66 | (String) input.get("v"), 67 | (String) input.get("latestVersion"), 68 | (String) input.get("p"), 69 | (long) input.get("timestamp")); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/dagger/Application.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.dagger; 2 | 3 | import dagger.Component; 4 | import it.mulders.mcs.cli.SearchCommand; 5 | import it.mulders.mcs.cli.SystemPropertyLoader; 6 | import picocli.CommandLine; 7 | 8 | @Component(modules = {CommandLineModule.class, OutputModule.class, SearchModule.class}) 9 | public interface Application { 10 | CommandLine commandLine(); 11 | 12 | SystemPropertyLoader systemPropertyLoader(); 13 | 14 | SearchCommand searchCommand(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/dagger/CommandLineModule.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.dagger; 2 | 3 | import dagger.Binds; 4 | import dagger.Module; 5 | import dagger.Provides; 6 | import it.mulders.mcs.cli.Cli; 7 | import it.mulders.mcs.common.McsExecutionExceptionHandler; 8 | import picocli.CommandLine; 9 | import picocli.CommandLine.IExecutionExceptionHandler; 10 | 11 | @Module 12 | public interface CommandLineModule { 13 | // @Provides static Cli provideCli(SearchCommandHandler searchCommandHandler) { 14 | // return new Cli(); 15 | // } 16 | 17 | @Provides 18 | static CommandLine provideCommandLine( 19 | final Cli cli, final DaggerFactory factory, final CommandLine.IExecutionExceptionHandler exceptionHandler) { 20 | return new CommandLine(cli, factory).setExecutionExceptionHandler(exceptionHandler); 21 | } 22 | 23 | // @Binds Cli.SearchCommand bindSearchCommand(final Cli.SearchCommand command); 24 | // @Binds Cli.ClassSearchCommand bindClassSearchCommand(final Cli.ClassSearchCommand command); 25 | // @Binds SearchCommandHandler bindSearchCommandHandler(final SearchCommandHandler handler); 26 | @Binds 27 | IExecutionExceptionHandler bindExecutionExceptionHandler(final McsExecutionExceptionHandler handler); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/dagger/DaggerFactory.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.dagger; 2 | 3 | import it.mulders.mcs.cli.ClassSearchCommand; 4 | import it.mulders.mcs.cli.SearchCommand; 5 | import jakarta.inject.Inject; 6 | import jakarta.inject.Provider; 7 | import picocli.CommandLine; 8 | 9 | public class DaggerFactory implements CommandLine.IFactory { 10 | private final Provider classSearchCommandProvider; 11 | private final Provider searchCommandProvider; 12 | private final CommandLine.IFactory defaultFactory = CommandLine.defaultFactory(); 13 | 14 | @Inject 15 | public DaggerFactory( 16 | final Provider classSearchCommandProvider, 17 | final Provider searchCommandProvider) { 18 | this.classSearchCommandProvider = classSearchCommandProvider; 19 | this.searchCommandProvider = searchCommandProvider; 20 | } 21 | 22 | @Override 23 | public K create(Class cls) throws Exception { 24 | return switch (cls.getName()) { 25 | case "it.mulders.mcs.cli.SearchCommand": 26 | yield (K) this.searchCommandProvider.get(); 27 | case "it.mulders.mcs.cli.ClassSearchCommand": 28 | yield (K) this.classSearchCommandProvider.get(); 29 | default: 30 | yield defaultFactory.create(cls); 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/dagger/OutputModule.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.dagger; 2 | 3 | import dagger.Module; 4 | 5 | @Module 6 | public interface OutputModule {} 7 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/dagger/SearchModule.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.dagger; 2 | 3 | import dagger.Module; 4 | import dagger.Provides; 5 | import java.net.http.HttpClient; 6 | 7 | @Module 8 | public interface SearchModule { 9 | @Provides 10 | static HttpClient provideHttpClient() { 11 | return HttpClient.newHttpClient(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/ClassnameQuery.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search; 2 | 3 | import static it.mulders.mcs.search.Constants.DEFAULT_MAX_SEARCH_RESULTS; 4 | import static it.mulders.mcs.search.Constants.DEFAULT_START; 5 | 6 | import java.net.URLEncoder; 7 | import java.nio.charset.StandardCharsets; 8 | 9 | public record ClassnameQuery(String query, boolean fullyQualified, int searchLimit, int start) implements SearchQuery { 10 | @Override 11 | public String toSolrQuery() { 12 | if (fullyQualified) { 13 | return String.format( 14 | "q=fc:%s&start=%d&rows=%d", 15 | URLEncoder.encode(query, StandardCharsets.UTF_8), start(), searchLimit()); 16 | } else { 17 | return String.format( 18 | "q=c:%s&start=%d&rows=%d", 19 | URLEncoder.encode(query, StandardCharsets.UTF_8), start(), searchLimit()); 20 | } 21 | } 22 | 23 | @Override 24 | public ClassnameQuery.Builder toBuilder() { 25 | return new ClassnameQuery.Builder(query()) 26 | .isFullyQualified(fullyQualified()) 27 | .withLimit(searchLimit()) 28 | .withStart(start()); 29 | } 30 | 31 | public static class Builder implements SearchQuery.Builder { 32 | private final String query; 33 | private Integer limit = DEFAULT_MAX_SEARCH_RESULTS; 34 | private Integer start = DEFAULT_START; 35 | private boolean fullyQualified = false; 36 | 37 | public Builder(String query) { 38 | this.query = query; 39 | } 40 | 41 | public Builder isFullyQualified(boolean isFullyQualified) { 42 | this.fullyQualified = isFullyQualified; 43 | return this; 44 | } 45 | 46 | @Override 47 | public Builder withStart(Integer start) { 48 | if (this.start != null) { 49 | this.start = start; 50 | } 51 | return this; 52 | } 53 | 54 | @Override 55 | public Builder withLimit(Integer limit) { 56 | if (limit != null) { 57 | this.limit = limit; 58 | } 59 | return this; 60 | } 61 | 62 | @Override 63 | public SearchQuery build() { 64 | return new ClassnameQuery(query, fullyQualified, limit, start); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/Constants.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search; 2 | 3 | import it.mulders.mcs.search.printer.CoordinatePrinter; 4 | import it.mulders.mcs.search.printer.PomXmlOutput; 5 | 6 | public class Constants { 7 | public static final Integer DEFAULT_MAX_SEARCH_RESULTS = 20; 8 | public static final Integer DEFAULT_START = 0; 9 | public static final Integer MAX_LIMIT = 200; 10 | public static final CoordinatePrinter DEFAULT_PRINTER = new PomXmlOutput(); 11 | 12 | private Constants() {} 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/CoordinateQuery.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search; 2 | 3 | import static it.mulders.mcs.search.Constants.DEFAULT_MAX_SEARCH_RESULTS; 4 | import static it.mulders.mcs.search.Constants.DEFAULT_START; 5 | 6 | import java.net.URLEncoder; 7 | import java.nio.charset.StandardCharsets; 8 | import java.util.LinkedList; 9 | import java.util.List; 10 | 11 | public record CoordinateQuery(String groupId, String artifactId, String version, int searchLimit, int start) 12 | implements SearchQuery { 13 | @Override 14 | public String toSolrQuery() { 15 | final List parts = new LinkedList<>(); 16 | if (!groupId.isBlank()) parts.add("g:%s".formatted(groupId)); 17 | if (!artifactId.isBlank()) parts.add("a:%s".formatted(artifactId)); 18 | if (!version.isBlank()) parts.add("v:%s".formatted(version)); 19 | var query = String.join(" AND ", parts); 20 | 21 | return String.format( 22 | "q=%s&core=gav&start=%d&rows=%d", 23 | URLEncoder.encode(query, StandardCharsets.UTF_8), start(), searchLimit()); 24 | } 25 | 26 | @Override 27 | public CoordinateQuery.Builder toBuilder() { 28 | return new CoordinateQuery.Builder(groupId(), artifactId(), version()) 29 | .withLimit(searchLimit()) 30 | .withStart(start()); 31 | } 32 | 33 | public static class Builder implements SearchQuery.Builder { 34 | private final String groupId; 35 | private final String artifactId; 36 | private final String version; 37 | private Integer limit = DEFAULT_MAX_SEARCH_RESULTS; 38 | private Integer start = DEFAULT_START; 39 | 40 | public Builder(String groupId, String artifactId) { 41 | this(groupId, artifactId, null); 42 | } 43 | 44 | public Builder(String groupId, String artifactId, String version) { 45 | this.groupId = sanitise(groupId); 46 | this.artifactId = sanitise(artifactId); 47 | this.version = sanitise(version); 48 | } 49 | 50 | private String sanitise(String input) { 51 | return input == null ? "" : input; 52 | } 53 | 54 | @Override 55 | public Builder withStart(Integer start) { 56 | if (this.start != null) { 57 | this.start = start; 58 | } 59 | return this; 60 | } 61 | 62 | @Override 63 | public Builder withLimit(Integer limit) { 64 | if (limit != null) { 65 | this.limit = limit; 66 | } 67 | return this; 68 | } 69 | 70 | @Override 71 | public SearchQuery build() { 72 | return new CoordinateQuery(groupId, artifactId, version, limit, start); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/FormatType.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search; 2 | 3 | import it.mulders.mcs.search.printer.BuildrOutput; 4 | import it.mulders.mcs.search.printer.CoordinatePrinter; 5 | import it.mulders.mcs.search.printer.GavOutput; 6 | import it.mulders.mcs.search.printer.GradleGroovyOutput; 7 | import it.mulders.mcs.search.printer.GradleGroovyShortOutput; 8 | import it.mulders.mcs.search.printer.GradleKotlinOutput; 9 | import it.mulders.mcs.search.printer.GrapeOutput; 10 | import it.mulders.mcs.search.printer.IvyXmlOutput; 11 | import it.mulders.mcs.search.printer.JBangOutput; 12 | import it.mulders.mcs.search.printer.LeiningenOutput; 13 | import it.mulders.mcs.search.printer.PomXmlOutput; 14 | import it.mulders.mcs.search.printer.SbtOutput; 15 | import java.util.Arrays; 16 | import java.util.stream.Collectors; 17 | 18 | public enum FormatType { 19 | MAVEN("maven", new PomXmlOutput()), 20 | GRADLE("gradle", new GradleGroovyOutput()), 21 | GRADLE_SHORT("gradle-short", new GradleGroovyShortOutput()), 22 | GRADLE_KOTLIN("gradle-kotlin", new GradleKotlinOutput()), 23 | SBT("sbt", new SbtOutput()), 24 | IVY("ivy", new IvyXmlOutput()), 25 | GRAPE("grape", new GrapeOutput()), 26 | LEININGEN("leiningen", new LeiningenOutput()), 27 | BUILDR("buildr", new BuildrOutput()), 28 | JBANG("jbang", new JBangOutput()), 29 | GAV("gav", new GavOutput()); 30 | 31 | private final String label; 32 | private final CoordinatePrinter printer; 33 | 34 | FormatType(final String outputType, final CoordinatePrinter printer) { 35 | this.label = outputType; 36 | this.printer = printer; 37 | } 38 | 39 | public CoordinatePrinter getPrinter() { 40 | return printer; 41 | } 42 | 43 | static String commaSeparatedLabels() { 44 | return Arrays.stream(values()).map(type -> type.label).collect(Collectors.joining(", ")); 45 | } 46 | 47 | public static CoordinatePrinter providePrinter(final String text) { 48 | if (text == null) { 49 | return Constants.DEFAULT_PRINTER; 50 | } 51 | if (text.isBlank()) { 52 | throw new UnsupportedFormatException( 53 | "Empty format type is not allowed. Use on of %s".formatted(commaSeparatedLabels())); 54 | } 55 | 56 | return Arrays.stream(values()) 57 | .filter(type -> type.label.equals(text.trim())) 58 | .map(FormatType::getPrinter) 59 | .findFirst() 60 | .orElseThrow(() -> new UnsupportedFormatException( 61 | "Format type '%s' is not supported. Use one of %s".formatted(text, commaSeparatedLabels()))); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/SearchClient.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search; 2 | 3 | import it.mulders.mcs.common.Result; 4 | import it.mulders.mcs.common.SearchResponseBodyHandler; 5 | import jakarta.inject.Inject; 6 | import java.io.IOException; 7 | import java.net.ConnectException; 8 | import java.net.URI; 9 | import java.net.http.HttpClient; 10 | import java.net.http.HttpRequest; 11 | 12 | public class SearchClient { 13 | private final String hostname; 14 | private final HttpClient client; 15 | 16 | @Inject 17 | public SearchClient(final HttpClient client) { 18 | this(client, "https://search.maven.org"); 19 | } 20 | 21 | // Visible for testing 22 | SearchClient(final HttpClient client, final String hostname) { 23 | this.client = client; 24 | this.hostname = hostname; 25 | } 26 | 27 | public Result search(final SearchQuery query) { 28 | var uri = String.format("%s/solrsearch/select?%s", hostname, query.toSolrQuery()); 29 | 30 | var request = HttpRequest.newBuilder() 31 | .version(HttpClient.Version.HTTP_1_1) 32 | .uri(URI.create(uri)) 33 | .build(); 34 | 35 | try { 36 | return client.send(request, new SearchResponseBodyHandler()).body(); 37 | } catch (ConnectException e) { 38 | // The JDK HTTP client throws a ConnectException without a message, we can do better. 39 | return new Result.Failure<>(new ConnectException("Can't resolve " + hostname)); 40 | } catch (IOException | InterruptedException e) { 41 | return new Result.Failure<>(e); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/SearchCommandHandler.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search; 2 | 3 | import static it.mulders.mcs.search.Constants.MAX_LIMIT; 4 | 5 | import it.mulders.mcs.common.McsRuntimeException; 6 | import it.mulders.mcs.common.Result; 7 | import it.mulders.mcs.search.printer.DelegatingOutputPrinter; 8 | import it.mulders.mcs.search.printer.OutputFactory; 9 | import it.mulders.mcs.search.vulnerability.ComponentReportClient; 10 | import it.mulders.mcs.search.vulnerability.ComponentReportResponse.ComponentReport; 11 | import jakarta.inject.Inject; 12 | import java.util.stream.Stream; 13 | 14 | public class SearchCommandHandler { 15 | private final OutputFactory outputFactory; 16 | private final SearchClient searchClient; 17 | private final ComponentReportClient reportClient; 18 | 19 | @Inject 20 | public SearchCommandHandler( 21 | final ComponentReportClient reportClient, 22 | final OutputFactory outputFactory, 23 | final SearchClient searchClient) { 24 | this.outputFactory = outputFactory; 25 | this.reportClient = reportClient; 26 | this.searchClient = searchClient; 27 | } 28 | 29 | public void search(final SearchQuery query, final String outputFormat, final boolean reportVulnerabilities) { 30 | performSearch(query) 31 | .map(response -> performAdditionalSearch(query, response)) 32 | .ifPresentOrElse( 33 | response -> { 34 | if (reportVulnerabilities) { 35 | processResponse(query, response); 36 | } 37 | printResponse(query, response, outputFormat, reportVulnerabilities); 38 | }, 39 | failure -> { 40 | throw new McsRuntimeException(failure); 41 | }); 42 | } 43 | 44 | private SearchResponse.Response performAdditionalSearch( 45 | final SearchQuery query, final SearchResponse.Response previousResponse) { 46 | var lastItemFoundIndex = previousResponse.docs().length; 47 | var enoughItemsForUserLimit = lastItemFoundIndex >= query.searchLimit(); 48 | var allItemsReceived = lastItemFoundIndex == previousResponse.numFound(); 49 | if (enoughItemsForUserLimit || allItemsReceived) { 50 | return previousResponse; 51 | } 52 | var remainingItems = query.searchLimit() - lastItemFoundIndex; 53 | var remainingLimit = Math.min(remainingItems, MAX_LIMIT); 54 | var updatedQuery = query.toBuilder() 55 | .withStart(lastItemFoundIndex) 56 | .withLimit(remainingLimit) 57 | .build(); 58 | 59 | return performSearch(updatedQuery) 60 | .map(response -> combineResponses(previousResponse, response)) 61 | .map(response -> performAdditionalSearch(query, response)) 62 | .value(); 63 | } 64 | 65 | private SearchResponse.Response combineResponses( 66 | SearchResponse.Response response1, SearchResponse.Response response2) { 67 | var docs = new SearchResponse.Response.Doc[response1.docs().length + response2.docs().length]; 68 | System.arraycopy(response1.docs(), 0, docs, 0, response1.docs().length); 69 | System.arraycopy(response2.docs(), 0, docs, response1.docs().length, response2.docs().length); 70 | return new SearchResponse.Response(response1.numFound(), response2.start(), docs); 71 | } 72 | 73 | private Result performSearch(final SearchQuery query) { 74 | return searchClient.search(query).map(SearchResponse::response); 75 | } 76 | 77 | private void processResponse(final SearchQuery query, final SearchResponse.Response searchResponse) { 78 | reportClient 79 | .search(searchResponse.docs()) 80 | .ifPresentOrElse( 81 | componentResponse -> 82 | assignComponentReports(componentResponse.componentReports(), searchResponse.docs()), 83 | failure -> { 84 | throw new McsRuntimeException(failure); 85 | }); 86 | } 87 | 88 | private void assignComponentReports( 89 | final ComponentReport[] componentReports, final SearchResponse.Response.Doc[] docs) { 90 | Stream.of(componentReports) 91 | .forEach(componentReport -> reportClient.assignComponentReport(componentReport, docs)); 92 | } 93 | 94 | private void printResponse( 95 | final SearchQuery query, 96 | final SearchResponse.Response response, 97 | final String outputFormat, 98 | final boolean showVulnerabilities) { 99 | var printer = new DelegatingOutputPrinter(outputFactory.findOutputPrinter(outputFormat), showVulnerabilities); 100 | printer.print(query, response, System.out); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/SearchQuery.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search; 2 | 3 | public sealed interface SearchQuery permits CoordinateQuery, ClassnameQuery, WildcardSearchQuery { 4 | int searchLimit(); 5 | 6 | int start(); 7 | 8 | String toSolrQuery(); 9 | 10 | Builder toBuilder(); 11 | 12 | static SearchQuery.Builder search(String query) { 13 | var isCoordinateSearch = query.contains(":"); 14 | if (isCoordinateSearch) { 15 | var parts = query.split(":"); 16 | return switch (parts.length) { 17 | case 1 -> new CoordinateQuery.Builder(parts[0], null); 18 | case 2 -> new CoordinateQuery.Builder(parts[0], parts[1]); 19 | case 3 -> new CoordinateQuery.Builder(parts[0], parts[1], parts[2]); 20 | default -> { 21 | var msg = 22 | """ 23 | Searching a particular artifact requires at least groupId:artifactId and optionally :version 24 | """; 25 | throw new IllegalArgumentException(msg); 26 | } 27 | }; 28 | } else { 29 | return new WildcardSearchQuery.Builder(query); 30 | } 31 | } 32 | 33 | static ClassnameQuery.Builder classSearch(String query) { 34 | return new ClassnameQuery.Builder(query); 35 | } 36 | 37 | interface Builder { 38 | Builder withLimit(final Integer limit); 39 | 40 | Builder withStart(final Integer start); 41 | 42 | SearchQuery build(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/SearchResponse.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search; 2 | 3 | import it.mulders.mcs.search.vulnerability.ComponentReportResponse.ComponentReport; 4 | 5 | public record SearchResponse(Object responseHeader, Response response) { 6 | public SearchResponse(Response response) { 7 | // Convenience for testing 8 | this(null, response); 9 | } 10 | 11 | public record Response(int numFound, int start, Doc[] docs) { 12 | public record Doc( 13 | String id, 14 | String g, 15 | String a, 16 | String v, 17 | String latestVersion, 18 | String p, 19 | long timestamp, 20 | ComponentReport componentReport) { 21 | public Doc(String id, String g, String a, String v, String latestVersion, String p, long timestamp) { 22 | this(id, g, a, v, latestVersion, p, timestamp, null); 23 | } 24 | 25 | public Doc withComponentReport(ComponentReport componentReport) { 26 | return new Doc(id(), g(), a(), v(), latestVersion(), p(), timestamp(), componentReport); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/UnsupportedFormatException.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search; 2 | 3 | public class UnsupportedFormatException extends RuntimeException { 4 | public UnsupportedFormatException(final String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/WildcardSearchQuery.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search; 2 | 3 | import static it.mulders.mcs.search.Constants.DEFAULT_MAX_SEARCH_RESULTS; 4 | import static it.mulders.mcs.search.Constants.DEFAULT_START; 5 | 6 | import java.net.URLEncoder; 7 | import java.nio.charset.StandardCharsets; 8 | 9 | public record WildcardSearchQuery(String term, int searchLimit, int start) implements SearchQuery { 10 | @Override 11 | public String toSolrQuery() { 12 | return String.format( 13 | "q=%s&start=%d&rows=%d", URLEncoder.encode(term, StandardCharsets.UTF_8), start(), searchLimit()); 14 | } 15 | 16 | @Override 17 | public WildcardSearchQuery.Builder toBuilder() { 18 | return new WildcardSearchQuery.Builder(term()).withLimit(searchLimit()).withStart(start()); 19 | } 20 | 21 | public static class Builder implements SearchQuery.Builder { 22 | private final String query; 23 | private Integer limit = DEFAULT_MAX_SEARCH_RESULTS; 24 | private Integer start = DEFAULT_START; 25 | 26 | public Builder(String query) { 27 | this.query = query; 28 | } 29 | 30 | @Override 31 | public Builder withStart(Integer start) { 32 | if (this.start != null) { 33 | this.start = start; 34 | } 35 | return this; 36 | } 37 | 38 | @Override 39 | public Builder withLimit(final Integer limit) { 40 | if (limit != null) { 41 | this.limit = limit; 42 | } 43 | return this; 44 | } 45 | 46 | @Override 47 | public SearchQuery build() { 48 | return new WildcardSearchQuery(query, limit, start); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/printer/BuildrOutput.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | public final class BuildrOutput implements CoordinatePrinter { 4 | 5 | @Override 6 | public String provideCoordinates( 7 | final String group, final String artifact, final String version, String packaging) { 8 | return "'%s:%s:jar:%s'".formatted(group, artifact, version); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/printer/CoordinatePrinter.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | import it.mulders.mcs.search.SearchQuery; 4 | import it.mulders.mcs.search.SearchResponse; 5 | import it.mulders.mcs.search.vulnerability.ComponentReportResponse; 6 | import it.mulders.mcs.search.vulnerability.ComponentReportVulnerabilitySeverity; 7 | import java.io.PrintStream; 8 | import java.util.Arrays; 9 | 10 | public sealed interface CoordinatePrinter extends OutputPrinter 11 | permits BuildrOutput, 12 | GradleGroovyOutput, 13 | GradleGroovyShortOutput, 14 | GradleKotlinOutput, 15 | GrapeOutput, 16 | IvyXmlOutput, 17 | LeiningenOutput, 18 | PomXmlOutput, 19 | SbtOutput, 20 | JBangOutput, 21 | GavOutput { 22 | 23 | String provideCoordinates(final String group, final String artifact, final String version, final String packaging); 24 | 25 | @Override 26 | default void print(final SearchQuery query, final SearchResponse.Response response, final PrintStream stream) { 27 | if (response.numFound() != 1) { 28 | throw new IllegalArgumentException("Search response with more than one result not expected here"); 29 | } 30 | 31 | var doc = response.docs()[0]; 32 | stream.println(); 33 | stream.println(provideCoordinates(doc.g(), doc.a(), first(doc.v(), doc.latestVersion()), doc.p())); 34 | stream.println(); 35 | printVulnerabilities(doc.componentReport(), stream); 36 | } 37 | 38 | private String first(final String... values) { 39 | for (var value : values) { 40 | if (value != null) { 41 | return value; 42 | } 43 | } 44 | return null; 45 | } 46 | 47 | private void printVulnerabilities( 48 | final ComponentReportResponse.ComponentReport componentReport, final PrintStream stream) { 49 | if (componentReport != null) { // will be null if --show-vulnerabilities cli arg is false 50 | if (componentReport.vulnerabilities().length == 0) { 51 | stream.println("No vulnerabilities reported"); 52 | } else { 53 | stream.println("Vulnerabilities:"); 54 | Arrays.stream(componentReport.vulnerabilitiesSortedByCvssScore()) 55 | .forEachOrdered(vul -> stream.println( 56 | vul.id() + " (" + ComponentReportVulnerabilitySeverity.getText(vul.cvssScore()) + ")" 57 | + " - " + vul.reference())); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/printer/DelegatingOutputPrinter.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | import it.mulders.mcs.search.SearchQuery; 4 | import it.mulders.mcs.search.SearchResponse; 5 | import java.io.PrintStream; 6 | 7 | /** 8 | * Output printer that delegates to a different printer, depending on the number of search results. 9 | */ 10 | public class DelegatingOutputPrinter implements OutputPrinter { 11 | private final OutputPrinter noOutput; 12 | private final OutputPrinter coordinateOutput; 13 | private final OutputPrinter tabularSearchOutput; 14 | 15 | public DelegatingOutputPrinter(final OutputPrinter coordinateOutput, final boolean showVulnerabilities) { 16 | this(new NoOutputPrinter(), coordinateOutput, new TabularOutputPrinter(showVulnerabilities)); 17 | } 18 | 19 | // Visible for testing 20 | DelegatingOutputPrinter( 21 | final OutputPrinter noOutput, 22 | final OutputPrinter coordinateOutput, 23 | final OutputPrinter tabularSearchOutput) { 24 | this.noOutput = noOutput; 25 | this.coordinateOutput = coordinateOutput; 26 | this.tabularSearchOutput = tabularSearchOutput; 27 | } 28 | 29 | @Override 30 | public void print(final SearchQuery query, final SearchResponse.Response response, final PrintStream stream) { 31 | switch (response.numFound()) { 32 | case 0 -> noOutput.print(query, response, stream); 33 | case 1 -> coordinateOutput.print(query, response, stream); 34 | default -> tabularSearchOutput.print(query, response, stream); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/printer/GavOutput.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | public final class GavOutput implements CoordinatePrinter { 4 | @Override 5 | public String provideCoordinates( 6 | final String group, final String artifact, final String version, String packaging) { 7 | if ("jar".equals(packaging)) return "%s:%s:%s".formatted(group, artifact, version); 8 | else return "%s:%s:%s@%s".formatted(group, artifact, version, packaging); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/printer/GradleGroovyOutput.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | public final class GradleGroovyOutput implements CoordinatePrinter { 4 | 5 | @Override 6 | public String provideCoordinates( 7 | final String group, final String artifact, final String version, String packaging) { 8 | return "implementation group: '%s', name: '%s', version: '%s'".formatted(group, artifact, version); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/printer/GradleGroovyShortOutput.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | public final class GradleGroovyShortOutput implements CoordinatePrinter { 4 | 5 | @Override 6 | public String provideCoordinates( 7 | final String group, final String artifact, final String version, String packaging) { 8 | return "implementation '%s:%s:%s'".formatted(group, artifact, version); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/printer/GradleKotlinOutput.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | public final class GradleKotlinOutput implements CoordinatePrinter { 4 | 5 | @Override 6 | public String provideCoordinates( 7 | final String group, final String artifact, final String version, String packaging) { 8 | return "implementation(\"%s:%s:%s\")".formatted(group, artifact, version); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/printer/GrapeOutput.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | public final class GrapeOutput implements CoordinatePrinter { 4 | 5 | @Override 6 | public String provideCoordinates( 7 | final String group, final String artifact, final String version, String packaging) { 8 | return """ 9 | @Grapes( 10 | @Grab(group='%s', module='%s', version='%s') 11 | ) 12 | """ 13 | .formatted(group, artifact, version); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/printer/IvyXmlOutput.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | public final class IvyXmlOutput implements CoordinatePrinter { 4 | 5 | @Override 6 | public String provideCoordinates( 7 | final String group, final String artifact, final String version, String packaging) { 8 | return """ 9 | 10 | """ 11 | .formatted(group, artifact, version); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/printer/JBangOutput.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | public final class JBangOutput implements CoordinatePrinter { 4 | @Override 5 | public String provideCoordinates( 6 | final String group, final String artifact, final String version, String packaging) { 7 | if ("jar".equals(packaging)) return "//DEPS %s:%s:%s".formatted(group, artifact, version); 8 | else return "//DEPS %s:%s:%s@%s".formatted(group, artifact, version, packaging); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/printer/LeiningenOutput.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | public final class LeiningenOutput implements CoordinatePrinter { 4 | 5 | @Override 6 | public String provideCoordinates( 7 | final String group, final String artifact, final String version, String packaging) { 8 | return "[%s/%s \"%s\"]".formatted(group, artifact, version); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/printer/NoOutputPrinter.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | import it.mulders.mcs.search.SearchQuery; 4 | import it.mulders.mcs.search.SearchResponse; 5 | import java.io.PrintStream; 6 | 7 | public class NoOutputPrinter implements OutputPrinter { 8 | @Override 9 | public void print(final SearchQuery query, final SearchResponse.Response response, final PrintStream stream) { 10 | if (response.numFound() != 0) { 11 | throw new IllegalArgumentException("Search response with any result not expected here"); 12 | } 13 | 14 | stream.println(); 15 | stream.printf("No results found%n"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/printer/OutputFactory.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | import it.mulders.mcs.search.FormatType; 4 | import jakarta.inject.Inject; 5 | 6 | public class OutputFactory { 7 | @Inject 8 | public OutputFactory() {} 9 | 10 | public OutputPrinter findOutputPrinter(final String formatName) { 11 | return FormatType.providePrinter(formatName); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/printer/OutputPrinter.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | import it.mulders.mcs.search.SearchQuery; 4 | import it.mulders.mcs.search.SearchResponse; 5 | import java.io.PrintStream; 6 | 7 | public interface OutputPrinter { 8 | void print(final SearchQuery query, final SearchResponse.Response response, final PrintStream stream); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/printer/PomXmlOutput.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | public final class PomXmlOutput implements CoordinatePrinter { 4 | 5 | @Override 6 | public String provideCoordinates( 7 | final String group, final String artifact, final String version, String packaging) { 8 | String element = "maven-plugin".equals(packaging) ? "plugin" : "dependency"; 9 | return """ 10 | <%4$s> 11 | %s 12 | %s 13 | %s 14 | 15 | """ 16 | .formatted(group, artifact, version, element); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/printer/SbtOutput.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | public final class SbtOutput implements CoordinatePrinter { 4 | 5 | @Override 6 | public String provideCoordinates( 7 | final String group, final String artifact, final String version, String packaging) { 8 | return """ 9 | libraryDependencies += "%s" %% "%s" %% "%s" 10 | """ 11 | .formatted(group, artifact, version); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/printer/TabularOutputPrinter.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | import it.mulders.mcs.search.SearchQuery; 4 | import it.mulders.mcs.search.SearchResponse; 5 | import it.mulders.mcs.search.vulnerability.ComponentReportResponse.ComponentReport; 6 | import it.mulders.mcs.search.vulnerability.ComponentReportResponse.ComponentReport.ComponentReportVulnerability; 7 | import it.mulders.mcs.search.vulnerability.ComponentReportVulnerabilitySeverity; 8 | import java.io.PrintStream; 9 | import java.time.Instant; 10 | import java.time.ZoneId; 11 | import java.time.format.DateTimeFormatter; 12 | import java.util.ArrayList; 13 | import java.util.Arrays; 14 | import java.util.LinkedHashMap; 15 | import java.util.Map; 16 | import java.util.function.Function; 17 | import java.util.stream.Collectors; 18 | import picocli.CommandLine; 19 | import picocli.CommandLine.Help; 20 | import picocli.CommandLine.Help.Ansi; 21 | import picocli.CommandLine.Help.Column; 22 | import picocli.CommandLine.Help.Column.Overflow; 23 | 24 | public class TabularOutputPrinter implements OutputPrinter { 25 | private static final DateTimeFormatter DATE_TIME_FORMATTER = 26 | DateTimeFormatter.ofPattern("dd MMM yyyy 'at' HH:mm (zzz)"); 27 | private static final int INDENT = 2; 28 | private static final int SPACING = 3; 29 | 30 | private final boolean showVulnerabilities; 31 | 32 | public TabularOutputPrinter() { 33 | this(false); 34 | } 35 | 36 | public TabularOutputPrinter(final boolean showVulnerabilities) { 37 | this.showVulnerabilities = showVulnerabilities; 38 | } 39 | 40 | private String header(final SearchQuery query, final SearchResponse.Response response) { 41 | var numFound = response.numFound(); 42 | var additionalMessage = 43 | numFound > query.searchLimit() ? String.format(" (showing %d)", response.docs().length) : ""; 44 | return String.format("Found @|bold %d|@ results%s%n", response.numFound(), additionalMessage); 45 | } 46 | 47 | public void print(final SearchQuery query, final SearchResponse.Response response, final PrintStream stream) { 48 | stream.println(CommandLine.Help.Ansi.AUTO.string(header(query, response))); 49 | 50 | var colorScheme = Help.defaultColorScheme(Ansi.AUTO); 51 | 52 | var table = CommandLine.Help.TextTable.forColumns(colorScheme, constructColumns(response)); 53 | 54 | if (showVulnerabilities) { 55 | table.addRowValues("Coordinates", "Last updated", "Vulnerabilities"); 56 | table.addRowValues("===========", "============", "==============="); 57 | } else { 58 | table.addRowValues("Coordinates", "Last updated"); 59 | table.addRowValues("===========", "============"); 60 | } 61 | 62 | Arrays.stream(response.docs()).forEach(doc -> printRow(table, doc)); 63 | 64 | stream.println(table); 65 | } 66 | 67 | private Column[] constructColumns(final SearchResponse.Response response) { 68 | var cols = new ArrayList(); 69 | cols.add(new CommandLine.Help.Column( 70 | calculateCoordinateColumnWidth(response.docs()) + SPACING, INDENT, Overflow.SPAN)); 71 | cols.add(new CommandLine.Help.Column(30, INDENT, Overflow.WRAP)); 72 | if (showVulnerabilities) { 73 | cols.add(new CommandLine.Help.Column(50, INDENT, Overflow.SPAN)); 74 | } 75 | return cols.toArray(Column[]::new); 76 | } 77 | 78 | private int calculateCoordinateColumnWidth(final SearchResponse.Response.Doc[] results) { 79 | return Arrays.stream(results) 80 | .map(this::displayEntry) 81 | .mapToInt(String::length) 82 | .max() 83 | .orElseThrow(() -> new IllegalStateException("Used TabularOutputPrinter without any output")); 84 | } 85 | 86 | private void printRow(final Help.TextTable table, final SearchResponse.Response.Doc doc) { 87 | var lastUpdated = 88 | DATE_TIME_FORMATTER.format(Instant.ofEpochMilli(doc.timestamp()).atZone(ZoneId.systemDefault())); 89 | 90 | var entry = displayEntry(doc); 91 | 92 | if (!showVulnerabilities) { 93 | table.addRowValues(entry, lastUpdated); 94 | } else { 95 | var vulnerabilityText = getVulnerabilityText(doc.componentReport()); 96 | table.addRowValues(entry, lastUpdated, vulnerabilityText); 97 | } 98 | } 99 | 100 | private String getVulnerabilityText(ComponentReport componentReport) { 101 | if (componentReport == null || componentReport.vulnerabilities().length == 0) { 102 | return "-"; 103 | } 104 | 105 | ComponentReportVulnerability[] sorted = componentReport.vulnerabilitiesSortedByCvssScore(); 106 | 107 | Map counts = Arrays.stream(sorted) 108 | .map(vulnerability -> ComponentReportVulnerabilitySeverity.getText(vulnerability.cvssScore())) 109 | .collect(Collectors.groupingBy(Function.identity(), LinkedHashMap::new, Collectors.counting())); 110 | 111 | return counts.entrySet().stream() 112 | .map(entry -> entry.getValue() + " " + entry.getKey()) 113 | .collect(Collectors.joining(", ")); 114 | } 115 | 116 | private String displayEntry(final SearchResponse.Response.Doc doc) { 117 | if (doc.latestVersion() != null) { 118 | return doc.id() + ":" + doc.latestVersion(); 119 | } else { 120 | return doc.id(); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/vulnerability/ComponentReportClient.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.vulnerability; 2 | 3 | import com.github.packageurl.MalformedPackageURLException; 4 | import com.github.packageurl.PackageURL; 5 | import it.mulders.mcs.common.McsRuntimeException; 6 | import it.mulders.mcs.common.Result; 7 | import it.mulders.mcs.search.SearchResponse; 8 | import jakarta.inject.Inject; 9 | import java.io.IOException; 10 | import java.net.URI; 11 | import java.net.http.HttpClient; 12 | import java.net.http.HttpRequest; 13 | import java.util.Base64; 14 | import java.util.List; 15 | import java.util.Objects; 16 | import java.util.stream.Collectors; 17 | import java.util.stream.Stream; 18 | 19 | public class ComponentReportClient { 20 | private static final String MISSING_OSS_INDEX_CREDENTIALS_WARNING = 21 | """ 22 | You've requested to show reported security vulnerabilities, but you haven't configured a username and password 23 | for the Sonatype OSS Index. See https://ossindex.sonatype.org for details on how this may impact your usage."""; 24 | 25 | private final String hostname; 26 | private final HttpClient client; 27 | 28 | @Inject 29 | public ComponentReportClient(final HttpClient client) { 30 | this(client, "https://ossindex.sonatype.org"); 31 | } 32 | 33 | // Visible for testing 34 | ComponentReportClient(final HttpClient client, final String hostname) { 35 | this.client = client; 36 | this.hostname = hostname; 37 | } 38 | 39 | public Result search(final SearchResponse.Response.Doc[] docs) { 40 | return search(Stream.of(docs).map(this::toPackageUrls).toList()); 41 | } 42 | 43 | public Result search(final List coordinates) { 44 | var purls = coordinates.stream().map(purl -> "\"" + purl + "\"").collect(Collectors.joining(",")); 45 | var json = "{\"coordinates\" : [%s]}".formatted(purls); 46 | 47 | var builder = HttpRequest.newBuilder() 48 | .version(HttpClient.Version.HTTP_1_1) 49 | .POST(HttpRequest.BodyPublishers.ofString(json)) 50 | .header("Content-Type", "application/json"); 51 | 52 | var username = System.getProperty("ossindex.username", ""); 53 | var password = System.getProperty("ossindex.password", ""); 54 | 55 | if (!username.isEmpty() || !password.isEmpty()) { 56 | builder.uri(URI.create(String.format("%s/api/v3/authorized/component-report", hostname))); 57 | builder.header("Authorization", getBasicAuthenticationHeader(username, password)); 58 | } else { 59 | builder.uri(URI.create(String.format("%s/api/v3/component-report", hostname))); 60 | System.out.println(MISSING_OSS_INDEX_CREDENTIALS_WARNING); 61 | } 62 | 63 | try { 64 | return client.send(builder.build(), new ComponentReportResponseBodyHandler()) 65 | .body(); 66 | } catch (IOException | InterruptedException e) { 67 | return new Result.Failure<>(e); 68 | } 69 | } 70 | 71 | private String getBasicAuthenticationHeader(final String username, final String password) { 72 | String valueToEncode = username + ":" + password; 73 | return "Basic " + Base64.getEncoder().encodeToString(valueToEncode.getBytes()); 74 | } 75 | 76 | public String toPackageUrls(final SearchResponse.Response.Doc doc) { 77 | try { 78 | return new PackageURL( 79 | "maven", doc.g(), doc.a(), doc.v() != null ? doc.v() : doc.latestVersion(), null, null) 80 | .canonicalize(); 81 | } catch (MalformedPackageURLException e) { 82 | throw new McsRuntimeException(e); 83 | } 84 | } 85 | 86 | public void assignComponentReport( 87 | final ComponentReportResponse.ComponentReport componentReport, final SearchResponse.Response.Doc[] docs) { 88 | for (int i = 0; i < docs.length; i++) { 89 | try { 90 | PackageURL packageURL = new PackageURL(componentReport.coordinates()); 91 | SearchResponse.Response.Doc doc = docs[i]; 92 | if (matches(packageURL, doc)) { 93 | doc = doc.withComponentReport(componentReport); 94 | docs[i] = doc; 95 | break; 96 | } 97 | } catch (MalformedPackageURLException e) { 98 | throw new McsRuntimeException(e); 99 | } 100 | } 101 | } 102 | 103 | private boolean matches(final PackageURL packageURL, final SearchResponse.Response.Doc doc) { 104 | return Objects.equals(packageURL.getNamespace(), doc.g()) 105 | && Objects.equals(packageURL.getName(), doc.a()) 106 | && Objects.equals(packageURL.getVersion(), doc.v() != null ? doc.v() : doc.latestVersion()); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/vulnerability/ComponentReportResponse.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.vulnerability; 2 | 3 | import java.util.Comparator; 4 | import java.util.stream.Stream; 5 | 6 | public record ComponentReportResponse(ComponentReport[] componentReports) { 7 | public record ComponentReport( 8 | String coordinates, String reference, ComponentReportVulnerability[] vulnerabilities) { 9 | public ComponentReportVulnerability[] vulnerabilitiesSortedByCvssScore() { 10 | return Stream.of(vulnerabilities) 11 | .sorted(Comparator.comparingDouble(ComponentReportVulnerability::cvssScore) 12 | .reversed()) 13 | .toArray(ComponentReportVulnerability[]::new); 14 | } 15 | 16 | public record ComponentReportVulnerability(String id, String title, Double cvssScore, String reference) {} 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/vulnerability/ComponentReportResponseBodyHandler.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.vulnerability; 2 | 3 | import com.fasterxml.jackson.core.JsonParseException; 4 | import com.fasterxml.jackson.jr.ob.JSON; 5 | import com.fasterxml.jackson.jr.ob.JSONObjectException; 6 | import it.mulders.mcs.common.Result; 7 | import it.mulders.mcs.search.vulnerability.ComponentReportResponse.ComponentReport; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.net.http.HttpResponse; 11 | import java.net.http.HttpResponse.BodySubscriber; 12 | import java.net.http.HttpResponse.ResponseInfo; 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | public class ComponentReportResponseBodyHandler implements HttpResponse.BodyHandler> { 17 | @Override 18 | public BodySubscriber> apply(final ResponseInfo responseInfo) { 19 | return asObject(); 20 | } 21 | 22 | static HttpResponse.BodySubscriber> asObject() { 23 | var upstream = HttpResponse.BodySubscribers.ofInputStream(); 24 | 25 | return HttpResponse.BodySubscribers.mapping( 26 | upstream, ComponentReportResponseBodyHandler::toComponentReportResponse); 27 | } 28 | 29 | static Result toComponentReportResponse(final InputStream inputStream) { 30 | try (final InputStream input = inputStream) { 31 | var list = JSON.std.listFrom(input); 32 | return new Result.Success<>(constructComponentReportResponse(list)); 33 | } catch (final JsonParseException | JSONObjectException joe) { 34 | return new Result.Failure<>( 35 | new IllegalStateException( 36 | """ 37 | 38 | Error parsing vulnerabilities for supplied component. This may be a temporary failure from ossindex.sonatype.org. 39 | If the problem persists, please open a conversation at 40 | 41 | https://github.com/mthmulders/mcs/discussions 42 | 43 | Make sure to at least provide your invocation of mcs and the version of mcs you're using. 44 | """)); 45 | } catch (final IOException ioe) { 46 | return new Result.Failure<>( 47 | new IllegalStateException("Error processing response: %s%n".formatted(ioe.getLocalizedMessage()))); 48 | } 49 | } 50 | 51 | static ComponentReportResponse constructComponentReportResponse(final List input) { 52 | return new ComponentReportResponse(input.stream() 53 | .map(ComponentReportResponseBodyHandler::constructComponentReport) 54 | .toArray(ComponentReportResponse.ComponentReport[]::new)); 55 | } 56 | 57 | private static ComponentReportResponse.ComponentReport constructComponentReport(final Object input) { 58 | Map map = (Map) input; 59 | 60 | return new ComponentReport( 61 | (String) map.get("coordinates"), 62 | (String) map.get("reference"), 63 | constructComponentReportVulnerabilities((List>) map.get("vulnerabilities"))); 64 | } 65 | 66 | private static ComponentReportResponse.ComponentReport.ComponentReportVulnerability[] 67 | constructComponentReportVulnerabilities(List> input) { 68 | return input.stream() 69 | .map(ComponentReportResponseBodyHandler::constructComponentReportVulnerability) 70 | .toArray(ComponentReportResponse.ComponentReport.ComponentReportVulnerability[]::new); 71 | } 72 | 73 | private static ComponentReportResponse.ComponentReport.ComponentReportVulnerability 74 | constructComponentReportVulnerability(final Map input) { 75 | return new ComponentReportResponse.ComponentReport.ComponentReportVulnerability( 76 | (String) input.get("id"), (String) input.get("title"), (Double) input.get("cvssScore"), (String) 77 | input.get("reference")); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/it/mulders/mcs/search/vulnerability/ComponentReportVulnerabilitySeverity.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.vulnerability; 2 | 3 | /** 4 | * Based on NVD Vulnerability Severity Ratings. Refer to NVD site. 5 | */ 6 | public enum ComponentReportVulnerabilitySeverity { 7 | CRITICAL("Critical", 9.0), 8 | HIGH("High", 7.0), 9 | MEDIUM("Medium", 4.0), 10 | LOW("Low", 0.1), 11 | NONE("None", 0.0); 12 | 13 | ComponentReportVulnerabilitySeverity(String text, Double score) { 14 | this.text = text; 15 | this.score = score; 16 | } 17 | 18 | private final String text; 19 | private final Double score; 20 | 21 | public static String getText(Double score) { 22 | if (score.compareTo(CRITICAL.score) >= 0) { 23 | return CRITICAL.text; 24 | } else if (score.compareTo(HIGH.score) >= 0) { 25 | return HIGH.text; 26 | } else if (score.compareTo(MEDIUM.score) >= 0) { 27 | return MEDIUM.text; 28 | } else if (score.compareTo(LOW.score) >= 0) { 29 | return LOW.text; 30 | } else { 31 | return NONE.text; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/resources/mcs.properties: -------------------------------------------------------------------------------- 1 | mcs.version=${project.version} -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/AppIT.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs; 2 | 3 | import static com.github.stefanbirkner.systemlambda.SystemLambda.tapSystemOut; 4 | import static com.github.stefanbirkner.systemlambda.SystemLambda.withEnvironmentVariable; 5 | import static java.util.Arrays.asList; 6 | 7 | import java.util.List; 8 | import org.assertj.core.api.WithAssertions; 9 | import org.junit.jupiter.api.*; 10 | import org.junitpioneer.jupiter.StdIo; 11 | import org.junitpioneer.jupiter.StdOut; 12 | 13 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 14 | class AppIT implements WithAssertions { 15 | @Nested 16 | class TechnicalIT { 17 | @BeforeEach 18 | void clearProxyProperties() { 19 | System.clearProperty("http.proxyHost"); 20 | System.clearProperty("http.proxyPort"); 21 | System.clearProperty("https.proxyHost"); 22 | System.clearProperty("https.proxyPort"); 23 | } 24 | 25 | @Test 26 | void should_show_version() throws Exception { 27 | var output = tapSystemOut(() -> App.doMain("-V")); 28 | assertThat(output).contains("mcs v"); 29 | } 30 | 31 | @Test 32 | void should_exit_cleanly() { 33 | assertThat(App.doMain("-V")).isEqualTo(0); 34 | } 35 | 36 | @Test 37 | void should_exit_nonzero_on_wrong_invocation() { 38 | assertThat(App.doMain("--does-not-exist")).isNotEqualTo(0); 39 | } 40 | 41 | @Test 42 | void runs_without_search_command_specified() { 43 | // On Github Actions, MCS sometimes fails to read the whole response and fail with 44 | // "chunked transfer encoding, state: READING_LENGTH". 45 | // Make sure the output is as small as possible by searching only one versioned artifact. 46 | assertThat(App.doMain("info.picocli:picocli:4.7.7")).isEqualTo(0); 47 | } 48 | 49 | @Test 50 | void should_not_set_proxy_system_properties_when_no_env_variable_is_present() throws Exception { 51 | List values = withEnvironmentVariable("HTTP_PROXY", null) 52 | .and("HTTPS_PROXY", null) 53 | .execute(() -> { 54 | App.doMain("info.picocli:picocli"); 55 | 56 | return asList( 57 | System.getProperty("http.proxyHost"), 58 | System.getProperty("http.proxyPort"), 59 | System.getProperty("https.proxyHost"), 60 | System.getProperty("https.proxyPort")); 61 | }); 62 | 63 | assertThat(values).isEqualTo(asList(null, null, null, null)); 64 | } 65 | 66 | @Test 67 | void should_set_proxy_system_properties_when_env_variables_are_present() throws Exception { 68 | List values = withEnvironmentVariable("HTTP_PROXY", "http://http.proxy.example.com:8080") 69 | .and("HTTPS_PROXY", "http://https.proxy.example.com:8484") 70 | .execute(() -> { 71 | App.doMain("info.picocli:picocli"); 72 | 73 | return asList( 74 | System.getProperty("http.proxyHost"), 75 | System.getProperty("http.proxyPort"), 76 | System.getProperty("https.proxyHost"), 77 | System.getProperty("https.proxyPort")); 78 | }); 79 | 80 | assertThat(values).isEqualTo(asList("http.proxy.example.com", "8080", "https.proxy.example.com", "8484")); 81 | } 82 | } 83 | 84 | @Nested 85 | class FunctionalIT { 86 | @StdIo 87 | @Test 88 | void should_find_plexus_utils_341(StdOut out) { 89 | App.doMain("search", "org.codehaus.plexus:plexus-utils:3.4.1"); 90 | 91 | var output = out.capturedLines(); 92 | 93 | assertThat(output).anySatisfy(line -> assertThat(line).contains("org.codehaus.plexus")); 94 | assertThat(output).anySatisfy(line -> assertThat(line).contains("plexus-utils")); 95 | assertThat(output).anySatisfy(line -> assertThat(line).contains("3.4.1")); 96 | } 97 | 98 | @StdIo 99 | @Test 100 | void should_find_plexus_utils_341_without_search(StdOut out) { 101 | App.doMain("org.codehaus.plexus:plexus-utils:3.4.1"); 102 | 103 | var output = out.capturedLines(); 104 | 105 | assertThat(output).anySatisfy(line -> assertThat(line).contains("org.codehaus.plexus")); 106 | assertThat(output).anySatisfy(line -> assertThat(line).contains("plexus-utils")); 107 | assertThat(output).anySatisfy(line -> assertThat(line).contains("3.4.1")); 108 | } 109 | 110 | @StdIo 111 | @Test 112 | void should_find_multiple_jreleaser_maven_plugin(StdOut out) { 113 | App.doMain("search", "org.jreleaser:jreleaser-maven-plugin"); 114 | 115 | var output = out.capturedLines(); 116 | 117 | assertThat(output).anySatisfy(line -> assertThat(line).matches("Found (\\d*) results \\(showing 20\\)")); 118 | assertThat(output) 119 | .anySatisfy(line -> assertThat(line).contains("org.jreleaser:jreleaser-maven-plugin:1.16.0")); 120 | } 121 | 122 | @StdIo 123 | @Test 124 | void should_find_many_artifacts_for_JAX_WS_Handler(StdOut out) { 125 | App.doMain("class-search", "-f", "javax.xml.ws.handler.Handler", "-l", "250"); 126 | 127 | var output = out.capturedLines(); 128 | 129 | assertThat(output).hasSizeGreaterThan(250); 130 | assertThat(output).anySatisfy(line -> assertThat(line).contains("jakarta.xml.ws:jakarta.xml.ws-api:2.3.3")); 131 | } 132 | 133 | @StdIo 134 | @Test 135 | void should_find_artifacts_for_Clocky_class(StdOut out) { 136 | App.doMain("class-search", "AdvanceableTime"); 137 | 138 | var output = out.capturedLines(); 139 | 140 | assertThat(output).anySatisfy(line -> assertThat(line).contains("it.mulders.clocky:clocky:0.4")); 141 | assertThat(output).anySatisfy(line -> assertThat(line).contains("it.mulders.clocky:clocky:0.4.1")); 142 | } 143 | 144 | @StdIo 145 | @Test 146 | void should_find_artifacts_for_Clocky_full_class_name(StdOut out) { 147 | App.doMain("class-search", "-f", "it.mulders.clocky.AdvanceableTime"); 148 | 149 | var output = out.capturedLines(); 150 | 151 | assertThat(output).anySatisfy(line -> assertThat(line).contains("it.mulders.clocky:clocky:0.4")); 152 | assertThat(output).anySatisfy(line -> assertThat(line).contains("it.mulders.clocky:clocky:0.4.1")); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/AppTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs; 2 | 3 | import it.mulders.mcs.cli.Cli; 4 | import it.mulders.mcs.cli.MockitoFactory; 5 | import org.assertj.core.api.WithAssertions; 6 | import org.junit.jupiter.api.DisplayNameGeneration; 7 | import org.junit.jupiter.api.DisplayNameGenerator; 8 | import org.junit.jupiter.api.Nested; 9 | import org.junit.jupiter.api.Test; 10 | import picocli.CommandLine; 11 | 12 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 13 | class AppTest implements WithAssertions { 14 | @Nested 15 | class PrependSearchCommandToArgs { 16 | @Test 17 | void should_prepend_search_to_command_line_args() { 18 | assertThat(App.prependSearchCommandToArgs("info.picocli:picocli")) 19 | .isEqualTo(new String[] {"search", "info.picocli:picocli"}); 20 | assertThat(App.prependSearchCommandToArgs("-h")).isEqualTo(new String[] {"search", "-h"}); 21 | assertThat(App.prependSearchCommandToArgs()).isEqualTo(new String[] {"search"}); 22 | } 23 | } 24 | 25 | @Nested 26 | class IsInvocationWithoutSearchCommand { 27 | private final Cli cli = new Cli(); 28 | private final CommandLine program = new CommandLine(cli, MockitoFactory.INSTANCE); 29 | 30 | @Test 31 | void should_detect_when_search_command_is_not_present() { 32 | assertThat(App.isInvocationWithoutSearchCommand(program, "info.picocli:picocli")) 33 | .isTrue(); 34 | assertThat(App.isInvocationWithoutSearchCommand(program, "info.picocli", "picocli")) 35 | .isTrue(); 36 | assertThat(App.isInvocationWithoutSearchCommand(program, "info.picocli", "picocli", "4.7.5")) 37 | .isTrue(); 38 | } 39 | 40 | @Test 41 | void should_detect_when_search_command_is_present() { 42 | assertThat(App.isInvocationWithoutSearchCommand(program, "search", "info.picocli:picocli")) 43 | .isFalse(); 44 | assertThat(App.isInvocationWithoutSearchCommand(program, "search", "--help")) 45 | .isFalse(); 46 | } 47 | 48 | @Test 49 | void invoking_help_is_not_invoking_search_help() { 50 | assertThat(App.isInvocationWithoutSearchCommand(program, "--help")).isFalse(); 51 | } 52 | 53 | @Test 54 | void invoking_help_is_not_invoking_search_version() { 55 | assertThat(App.isInvocationWithoutSearchCommand(program, "-V")).isFalse(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/cli/ClassSearchCommandTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.cli; 2 | 3 | import static org.mockito.ArgumentMatchers.eq; 4 | import static org.mockito.Mockito.mock; 5 | import static org.mockito.Mockito.verify; 6 | 7 | import it.mulders.mcs.search.Constants; 8 | import it.mulders.mcs.search.SearchCommandHandler; 9 | import it.mulders.mcs.search.SearchQuery; 10 | import org.assertj.core.api.WithAssertions; 11 | import org.junit.jupiter.api.DisplayNameGeneration; 12 | import org.junit.jupiter.api.DisplayNameGenerator; 13 | import org.junit.jupiter.api.Test; 14 | import org.mockito.ArgumentCaptor; 15 | 16 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 17 | class ClassSearchCommandTest implements WithAssertions { 18 | private final SearchCommandHandler searchCommandHandler = mock(SearchCommandHandler.class); 19 | 20 | @Test 21 | void delegates_to_handler() { 22 | // Arrange 23 | var command = new ClassSearchCommand(searchCommandHandler, "test", null, false); 24 | 25 | // Act 26 | command.call(); 27 | 28 | // Assert 29 | var query = SearchQuery.classSearch("test").build(); 30 | verifyHandlerInvocation("maven", false, query); 31 | } 32 | 33 | @Test 34 | void accepts_full_name_parameter() { 35 | // Arrange 36 | var command = new ClassSearchCommand(searchCommandHandler, "test", null, true); 37 | 38 | // Act 39 | command.call(); 40 | 41 | // Assert 42 | var query = SearchQuery.classSearch("test") 43 | .isFullyQualified(true) 44 | .withLimit(Constants.DEFAULT_MAX_SEARCH_RESULTS) 45 | .build(); 46 | verifyHandlerInvocation("maven", false, query); 47 | } 48 | 49 | private void verifyHandlerInvocation(String outputFormat, boolean reportVulnerabilities, SearchQuery query) { 50 | var captor = ArgumentCaptor.forClass(SearchQuery.class); 51 | verify(searchCommandHandler).search(captor.capture(), eq(outputFormat), eq(reportVulnerabilities)); 52 | assertThat(captor.getValue()).isEqualTo(query); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/cli/ClasspathVersionProviderTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.cli; 2 | 3 | import org.assertj.core.api.WithAssertions; 4 | import org.junit.jupiter.api.DisplayNameGeneration; 5 | import org.junit.jupiter.api.DisplayNameGenerator; 6 | import org.junit.jupiter.api.Test; 7 | import picocli.CommandLine; 8 | 9 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 10 | class ClasspathVersionProviderTest implements WithAssertions { 11 | private CommandLine.IVersionProvider versionProvider = new ClasspathVersionProvider(); 12 | 13 | @Test 14 | void should_read_version_from_classpath() throws Exception { 15 | // setup is done in src/text/resources/mcs.properties 16 | assertThat(versionProvider.getVersion()).containsOnly("mcs vUnknown"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/cli/CliTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.cli; 2 | 3 | import static org.mockito.Mockito.mock; 4 | 5 | import it.mulders.mcs.search.SearchCommandHandler; 6 | import org.assertj.core.api.InstanceOfAssertFactories; 7 | import org.assertj.core.api.WithAssertions; 8 | import org.junit.jupiter.api.DisplayNameGeneration; 9 | import org.junit.jupiter.api.DisplayNameGenerator; 10 | import org.junit.jupiter.api.Test; 11 | import picocli.CommandLine; 12 | 13 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 14 | class CliTest implements WithAssertions { 15 | private final SearchCommandHandler searchCommandHandler = mock(SearchCommandHandler.class); 16 | private final SearchCommand searchCommand = new SearchCommand(searchCommandHandler); 17 | private final ClassSearchCommand classSearchCommand = new ClassSearchCommand(searchCommandHandler); 18 | 19 | private final CommandLine.IFactory commandLineFactory = new CommandLine.IFactory() { 20 | @Override 21 | public K create(Class cls) throws Exception { 22 | if (SearchCommand.class.equals(cls)) { 23 | return (K) searchCommand; 24 | } else if (ClassSearchCommand.class.equals(cls)) { 25 | return (K) classSearchCommand; 26 | } else { 27 | return mock(cls); 28 | } 29 | } 30 | }; 31 | private final CommandLine program = new CommandLine(new Cli(), commandLineFactory); 32 | 33 | @Test 34 | void should_invoke_search_command() { 35 | // Arrange 36 | 37 | // Act 38 | program.execute("search", "plexus-utils"); 39 | 40 | // Assert 41 | assertThat(searchCommand) 42 | .extracting("query", InstanceOfAssertFactories.ARRAY) 43 | .isEqualTo(new String[] {"plexus-utils"}); 44 | assertThat(searchCommand).extracting("responseFormat").isNull(); 45 | assertThat(classSearchCommand).extracting("limit").isNull(); 46 | } 47 | 48 | @Test 49 | void should_invoke_search_command_with_limit() { 50 | // Arrange 51 | 52 | // Act 53 | program.execute("search", "-l", "3", "plexus-utils"); 54 | 55 | // Assert 56 | assertThat(searchCommand) 57 | .extracting("limit", InstanceOfAssertFactories.INTEGER) 58 | .isEqualTo(3); 59 | } 60 | 61 | @Test 62 | void should_invoke_search_command_with_format() { 63 | // Arrange 64 | 65 | // Act 66 | program.execute("search", "-f", "gradle", "plexus-utils"); 67 | 68 | // Assert 69 | assertThat(searchCommand) 70 | .extracting("responseFormat", InstanceOfAssertFactories.STRING) 71 | .isEqualTo("gradle"); 72 | } 73 | 74 | @Test 75 | void should_invoke_class_search_command() { 76 | // Arrange 77 | 78 | // Act 79 | program.execute("class-search", "WithAssertions"); 80 | 81 | // Assert 82 | assertThat(classSearchCommand) 83 | .extracting("query", InstanceOfAssertFactories.STRING) 84 | .isEqualTo("WithAssertions"); 85 | assertThat(classSearchCommand) 86 | .extracting("fullName", InstanceOfAssertFactories.BOOLEAN) 87 | .isFalse(); 88 | assertThat(classSearchCommand).extracting("limit").isNull(); 89 | } 90 | 91 | @Test 92 | void should_invoke_class_search_command_with_limit() { 93 | // Arrange 94 | 95 | // Act 96 | program.execute("class-search", "-l", "3", "WithAssertions"); 97 | 98 | // Assert 99 | assertThat(classSearchCommand) 100 | .extracting("query", InstanceOfAssertFactories.STRING) 101 | .isEqualTo("WithAssertions"); 102 | assertThat(classSearchCommand) 103 | .extracting("fullName", InstanceOfAssertFactories.BOOLEAN) 104 | .isFalse(); 105 | assertThat(classSearchCommand) 106 | .extracting("limit", InstanceOfAssertFactories.INTEGER) 107 | .isEqualTo(3); 108 | } 109 | 110 | @Test 111 | void should_invoke_class_search_command_with_full_classname() { 112 | // Arrange 113 | 114 | // Act 115 | program.execute("class-search", "-f", "org.assertj.core.api.WithAssertions"); 116 | 117 | // Assert 118 | assertThat(classSearchCommand) 119 | .extracting("query", InstanceOfAssertFactories.STRING) 120 | .isEqualTo("org.assertj.core.api.WithAssertions"); 121 | assertThat(classSearchCommand) 122 | .extracting("fullName", InstanceOfAssertFactories.BOOLEAN) 123 | .isTrue(); 124 | } 125 | // 126 | } 127 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/cli/DaggerFactoryTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.cli; 2 | 3 | import it.mulders.mcs.dagger.DaggerFactory; 4 | import jakarta.inject.Provider; 5 | import org.assertj.core.api.WithAssertions; 6 | import org.junit.jupiter.api.DisplayNameGeneration; 7 | import org.junit.jupiter.api.DisplayNameGenerator; 8 | import org.junit.jupiter.api.Test; 9 | 10 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 11 | class DaggerFactoryTest implements WithAssertions { 12 | private final SearchCommand searchCommand = new SearchCommand(null); 13 | private final ClassSearchCommand classSearchCommand = new ClassSearchCommand(null); 14 | private final Provider searchCommandProvider = () -> searchCommand; 15 | private final Provider classSearchCommandProvider = () -> classSearchCommand; 16 | private final DaggerFactory factory = new DaggerFactory(classSearchCommandProvider, searchCommandProvider); 17 | 18 | @Test 19 | void can_construct_ClassSearchCommand_instance() throws Exception { 20 | assertThat(factory.create(ClassSearchCommand.class)).isEqualTo(classSearchCommand); 21 | } 22 | 23 | @Test 24 | void can_construct_SearchCommand_instance() throws Exception { 25 | assertThat(factory.create(SearchCommand.class)).isEqualTo(searchCommand); 26 | } 27 | 28 | @Test 29 | void can_construct_arbitrary_other_class() throws Exception { 30 | assertThat(factory.create(Dummy.class)).isNotNull(); 31 | } 32 | 33 | static class Dummy {} 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/cli/MockitoFactory.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.cli; 2 | 3 | import org.mockito.Mockito; 4 | import picocli.CommandLine; 5 | 6 | public class MockitoFactory implements CommandLine.IFactory { 7 | public static final CommandLine.IFactory INSTANCE = new MockitoFactory(); 8 | 9 | @Override 10 | public K create(Class cls) throws Exception { 11 | return Mockito.mock(cls); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/cli/SearchCommandTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.cli; 2 | 3 | import static org.mockito.ArgumentMatchers.eq; 4 | import static org.mockito.Mockito.mock; 5 | import static org.mockito.Mockito.verify; 6 | 7 | import it.mulders.mcs.search.SearchCommandHandler; 8 | import it.mulders.mcs.search.SearchQuery; 9 | import org.assertj.core.api.WithAssertions; 10 | import org.junit.jupiter.api.DisplayNameGeneration; 11 | import org.junit.jupiter.api.DisplayNameGenerator; 12 | import org.junit.jupiter.api.Test; 13 | import org.mockito.ArgumentCaptor; 14 | 15 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 16 | class SearchCommandTest implements WithAssertions { 17 | private final SearchCommandHandler searchCommandHandler = mock(SearchCommandHandler.class); 18 | 19 | @Test 20 | void delegates_to_handler() { 21 | // Arrange 22 | var command = new SearchCommand(searchCommandHandler, new String[] {"test"}, null, "maven", false); 23 | 24 | // Act 25 | command.call(); 26 | 27 | // Assert 28 | var query = SearchQuery.search("test").build(); 29 | verifyHandlerInvocation("maven", false, query); 30 | } 31 | 32 | @Test 33 | void accepts_space_separated_terms() { 34 | // Arrange 35 | var command = new SearchCommand(searchCommandHandler, new String[] {"jakarta", "rs"}, null, "maven", false); 36 | 37 | // Act 38 | command.call(); 39 | 40 | // Assert 41 | var query = SearchQuery.search("jakarta rs").build(); 42 | verifyHandlerInvocation("maven", false, query); 43 | } 44 | 45 | @Test 46 | void accepts_limit_results_parameter() { 47 | // Arrange 48 | var command = new SearchCommand(searchCommandHandler, new String[] {"test"}, 3, "maven", false); 49 | 50 | // Act 51 | command.call(); 52 | 53 | // Assert 54 | var query = SearchQuery.search("test").withLimit(3).build(); 55 | verifyHandlerInvocation("maven", false, query); 56 | } 57 | 58 | @Test 59 | void accepts_output_type_parameter() { 60 | // Arrange 61 | var command = new SearchCommand(searchCommandHandler, new String[] {"test"}, null, "gradle-short", false); 62 | 63 | // Act 64 | command.call(); 65 | 66 | // Assert 67 | var query = SearchQuery.search("test").build(); 68 | verifyHandlerInvocation("gradle-short", false, query); 69 | } 70 | 71 | @Test 72 | void accepts_show_vulnerabilities_parameter() { 73 | // Arrange 74 | var command = new SearchCommand(searchCommandHandler, new String[] {"test"}, null, "maven", true); 75 | 76 | // Act 77 | command.call(); 78 | 79 | // Assert 80 | var query = SearchQuery.search("test").build(); 81 | verifyHandlerInvocation("maven", true, query); 82 | } 83 | 84 | private void verifyHandlerInvocation(String outputFormat, boolean reportVulnerabilities, SearchQuery query) { 85 | var captor = ArgumentCaptor.forClass(SearchQuery.class); 86 | verify(searchCommandHandler).search(captor.capture(), eq(outputFormat), eq(reportVulnerabilities)); 87 | assertThat(captor.getValue()).isEqualTo(query); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/cli/SystemPropertyLoaderTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.cli; 2 | 3 | import java.nio.file.Path; 4 | import java.nio.file.Paths; 5 | import org.assertj.core.api.WithAssertions; 6 | import org.junit.jupiter.api.DisplayNameGeneration; 7 | import org.junit.jupiter.api.DisplayNameGenerator; 8 | import org.junit.jupiter.api.Test; 9 | 10 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 11 | class SystemPropertyLoaderTest implements WithAssertions { 12 | private static final Path SAMPLE = Paths.get("src", "test", "resources", "sample-mcs.config"); 13 | 14 | @Test 15 | void should_load_if_file_exists() { 16 | var loader = new SystemPropertyLoader(SAMPLE); 17 | 18 | assertThat(loader.getProperties()).containsEntry("example.a", "foo"); 19 | } 20 | 21 | @Test 22 | void should_not_fail_if_file_does_not_exist() { 23 | var loader = new SystemPropertyLoader(Paths.get("src", "test", "resources", "non-existing-mcs.config")); 24 | 25 | assertThat(loader.getProperties().isEmpty()).isFalse(); 26 | } 27 | 28 | @Test 29 | void should_delegate_to_System_properties() { 30 | var loader = new SystemPropertyLoader(SAMPLE); 31 | 32 | // user.dir is not overridden in the sample configuration file 33 | assertThat(loader.getProperties()).containsKey("user.dir"); 34 | } 35 | 36 | @Test 37 | void should_override_System_properties() { 38 | var loader = new SystemPropertyLoader(SAMPLE); 39 | 40 | // user.home is recklessly overridden in the sample configuration file 41 | assertThat(loader.getProperties()).containsEntry("user.home", "whatever"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/common/McsExecutionExceptionHandlerTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.common; 2 | 3 | import static com.github.stefanbirkner.systemlambda.SystemLambda.tapSystemErr; 4 | 5 | import javax.net.ssl.SSLHandshakeException; 6 | import org.assertj.core.api.WithAssertions; 7 | import org.junit.jupiter.api.DisplayNameGeneration; 8 | import org.junit.jupiter.api.DisplayNameGenerator; 9 | import org.junit.jupiter.api.Test; 10 | import picocli.CommandLine; 11 | 12 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 13 | class McsExecutionExceptionHandlerTest implements WithAssertions { 14 | private final CommandLine.IExecutionExceptionHandler handler = new McsExecutionExceptionHandler(); 15 | 16 | @Test 17 | void should_unwrap_mcs_runtime_exception() throws Exception { 18 | var message = "PKIX path building failed"; 19 | var exception = new McsRuntimeException(new SSLHandshakeException(message)); 20 | var result = tapSystemErr(() -> handler.handleExecutionException(exception, null, null)); 21 | assertThat(result).contains(message); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/common/ResultTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.common; 2 | 3 | import java.util.NoSuchElementException; 4 | import java.util.concurrent.atomic.AtomicReference; 5 | import org.assertj.core.api.WithAssertions; 6 | import org.junit.jupiter.api.DisplayNameGeneration; 7 | import org.junit.jupiter.api.DisplayNameGenerator; 8 | import org.junit.jupiter.api.Nested; 9 | import org.junit.jupiter.api.Test; 10 | 11 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 12 | class ResultTest implements WithAssertions { 13 | @Nested 14 | class Success { 15 | @Test 16 | void ifPresent() { 17 | // Arrange 18 | var x = new AtomicReference<>(false); 19 | var result = new Result.Success<>("foo"); 20 | 21 | // Act 22 | result.ifPresent(r -> { 23 | assertThat(r).isEqualTo("foo"); 24 | x.set(true); 25 | }); 26 | 27 | // Assert 28 | assertThat(x).withFailMessage("expect consumer to be called").hasValue(true); 29 | } 30 | 31 | @Test 32 | void map() { 33 | // Arrange 34 | var input = new Result.Success<>("foo"); 35 | 36 | // Act 37 | var result = input.map(String::toUpperCase); 38 | 39 | // Assert 40 | assertThat(result.value()).isEqualTo("FOO"); 41 | } 42 | 43 | @Test 44 | void map_consumer_throws_exception() { 45 | // Arrange 46 | var input = new Result.Success<>("foo"); 47 | 48 | // Act 49 | var result = input.map(a -> { 50 | throw new NullPointerException(); 51 | }); 52 | 53 | // Assert 54 | assertThatThrownBy(result::value).isInstanceOf(NoSuchElementException.class); 55 | assertThat(result.cause()).isInstanceOf(NullPointerException.class); 56 | } 57 | 58 | @Test 59 | void cause() { 60 | // Arrange 61 | var input = new Result.Success<>("foo"); 62 | 63 | // Act 64 | 65 | // Assert 66 | assertThatThrownBy(input::cause).isInstanceOf(NoSuchElementException.class); 67 | } 68 | } 69 | 70 | @Nested 71 | class Failure { 72 | @Test 73 | void ifPresent() { 74 | // Arrange 75 | var x = new AtomicReference<>(false); 76 | var result = new Result.Failure<>(new Exception()); 77 | 78 | // Act 79 | result.ifPresent(r -> { 80 | assertThat(r).isEqualTo("foo"); 81 | x.set(true); 82 | }); 83 | 84 | // Assert 85 | assertThat(x).withFailMessage("expect consumer not to be called").hasValue(false); 86 | } 87 | 88 | @Test 89 | void map() { 90 | // Arrange 91 | var input = new Result.Failure(new Exception()); 92 | 93 | // Act 94 | var result = input.map(String::toUpperCase); 95 | 96 | // Assert 97 | assertThatThrownBy(result::value).isInstanceOf(NoSuchElementException.class); 98 | } 99 | 100 | @Test 101 | void cause() { 102 | // Arrange 103 | var input = new Result.Failure<>(new Exception() {}); 104 | 105 | // Act 106 | 107 | // Assert 108 | assertThat(input.cause()).isInstanceOf(Exception.class); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/common/SearchResponseBodyHandlerTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.common; 2 | 3 | import it.mulders.mcs.search.SearchResponse; 4 | import org.assertj.core.api.WithAssertions; 5 | import org.junit.jupiter.api.DisplayNameGeneration; 6 | import org.junit.jupiter.api.DisplayNameGenerator; 7 | import org.junit.jupiter.api.Test; 8 | 9 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 10 | class SearchResponseBodyHandlerTest implements WithAssertions { 11 | /** 12 | * This is the response we get when the user uses the "wildcard" search - 13 | * not looking for an exact coordinate, but looking for something like "plexus-utils". 14 | */ 15 | @Test 16 | void parse_wildcard_search_response() { 17 | // Arrange 18 | var input = getClass().getResourceAsStream("/wildcard-search-response.json"); 19 | 20 | // Act 21 | var result = SearchResponseBodyHandler.toSearchResponse(input); 22 | 23 | // Assert 24 | assertThat(result).isInstanceOf(Result.Success.class); 25 | var response = result.value(); 26 | assertThat(response.response()).isNotNull(); 27 | assertThat(response.response().numFound()).isEqualTo(2); 28 | assertThat(response.response().start()).isEqualTo(0); 29 | assertThat(response.response().docs()).hasSize(2); 30 | 31 | assertThat(response.response().docs()) 32 | .contains(new SearchResponse.Response.Doc( 33 | "org.codehaus.plexus:plexus-utils", 34 | "org.codehaus.plexus", 35 | "plexus-utils", 36 | null, 37 | "3.4.1", 38 | "jar", 39 | 1630022910000L)); 40 | 41 | assertThat(response.response().docs()) 42 | .contains(new SearchResponse.Response.Doc( 43 | "plexus:plexus-utils", "plexus", "plexus-utils", null, "1.0.3", "jar", 1131487245000L)); 44 | } 45 | 46 | /** 47 | * This is the response we get when the user searches for a coordinate including a version - 48 | * e.g. "org.codehaus.plexus:plexus-utils:3.4.1". 49 | */ 50 | @Test 51 | void parse_coordinates_with_version_search_response() { 52 | // Arrange 53 | var input = getClass().getResourceAsStream("/group-artifact-version-search.json"); 54 | 55 | // Act 56 | var result = SearchResponseBodyHandler.toSearchResponse(input); 57 | 58 | // Assert 59 | assertThat(result).isInstanceOf(Result.Success.class); 60 | var response = result.value(); 61 | assertThat(response.response()).isNotNull(); 62 | assertThat(response.response().numFound()).isEqualTo(1); 63 | assertThat(response.response().start()).isEqualTo(0); 64 | assertThat(response.response().docs()).hasSize(1); 65 | 66 | assertThat(response.response().docs()) 67 | .contains(new SearchResponse.Response.Doc( 68 | "org.codehaus.plexus:plexus-utils:3.4.1", 69 | "org.codehaus.plexus", 70 | "plexus-utils", 71 | "3.4.1", 72 | null, 73 | "jar", 74 | 1630022910000L)); 75 | } 76 | 77 | /** 78 | * This is the response we get when the user searches for a coordinate without a version - 79 | * e.g. "org.codehaus.plexus:plexus-utils". 80 | */ 81 | @Test 82 | void parse_coordinates_without_version_search_response() { 83 | // Arrange 84 | var input = getClass().getResourceAsStream("/group-artifact-search.json"); 85 | 86 | // Act 87 | var result = SearchResponseBodyHandler.toSearchResponse(input); 88 | 89 | // Assert 90 | assertThat(result).isInstanceOf(Result.Success.class); 91 | var response = result.value(); 92 | assertThat(response.response()).isNotNull(); 93 | assertThat(response.response().numFound()).isEqualTo(1); 94 | assertThat(response.response().start()).isEqualTo(0); 95 | assertThat(response.response().docs()).hasSize(1); 96 | 97 | assertThat(response.response().docs()) 98 | .contains(new SearchResponse.Response.Doc( 99 | "org.codehaus.plexus:plexus-utils", 100 | "org.codehaus.plexus", 101 | "plexus-utils", 102 | null, 103 | "3.4.1", 104 | "jar", 105 | 1630022910000L)); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/search/ClassnameQueryTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search; 2 | 3 | import static org.assertj.core.api.AssertionsForClassTypes.assertThat; 4 | 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.DisplayNameGeneration; 7 | import org.junit.jupiter.api.DisplayNameGenerator; 8 | import org.junit.jupiter.api.Nested; 9 | import org.junit.jupiter.api.Test; 10 | 11 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 12 | public class ClassnameQueryTest { 13 | @Nested 14 | @DisplayName("builder") 15 | class SearchQueryBuilderTest { 16 | 17 | @Test 18 | void default_class_search_is_classname_query() { 19 | SearchQuery query = SearchQuery.classSearch("test").build(); 20 | assertThat(query).isInstanceOf(ClassnameQuery.class); 21 | } 22 | 23 | @Test 24 | void can_create_classname_query() { 25 | SearchQuery query = 26 | SearchQuery.classSearch("test").isFullyQualified(false).build(); 27 | assertThat(query).isInstanceOf(ClassnameQuery.class); 28 | } 29 | } 30 | 31 | @Nested 32 | @DisplayName("toSolrQuery") 33 | class ToSolrQueryTest { 34 | @Test 35 | void solr_query_should_contain_limit() { 36 | var query = SearchQuery.classSearch("foo").withLimit(5).build(); 37 | 38 | var solrQuery = query.toSolrQuery(); 39 | 40 | assertThat(solrQuery).contains("rows=5"); 41 | } 42 | 43 | @Test 44 | void solr_query_should_contain_class_name() { 45 | var query = SearchQuery.classSearch("foo").build(); 46 | 47 | var solrQuery = query.toSolrQuery(); 48 | 49 | assertThat(solrQuery).contains("q=c:foo"); 50 | } 51 | 52 | @Test 53 | void solr_query_should_contain_class_name_with_package() { 54 | var query = SearchQuery.classSearch("foo").isFullyQualified(true).build(); 55 | 56 | var solrQuery = query.toSolrQuery(); 57 | 58 | assertThat(solrQuery).contains("q=fc:foo"); 59 | } 60 | 61 | @Test 62 | void solr_query_should_contain_start() { 63 | var query = SearchQuery.classSearch("foo").build(); 64 | 65 | var solrQuery = query.toSolrQuery(); 66 | 67 | assertThat(solrQuery).contains("start=0"); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/search/CoordinateQueryTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search; 2 | 3 | import static it.mulders.mcs.search.Constants.DEFAULT_MAX_SEARCH_RESULTS; 4 | import static it.mulders.mcs.search.Constants.DEFAULT_START; 5 | 6 | import java.net.URLEncoder; 7 | import java.nio.charset.StandardCharsets; 8 | import org.assertj.core.api.WithAssertions; 9 | import org.junit.jupiter.api.DisplayName; 10 | import org.junit.jupiter.api.DisplayNameGeneration; 11 | import org.junit.jupiter.api.DisplayNameGenerator; 12 | import org.junit.jupiter.api.Nested; 13 | import org.junit.jupiter.params.ParameterizedTest; 14 | import org.junit.jupiter.params.provider.ValueSource; 15 | 16 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 17 | class CoordinateQueryTest implements WithAssertions { 18 | 19 | @Nested 20 | @DisplayName("toSolrQuery") 21 | class ToSolrQueryTest { 22 | @ParameterizedTest 23 | @ValueSource(strings = {"g:foo", "a:bar", "v:1.0"}) 24 | void solr_query_should_contain_basic_parameters(String parameter) { 25 | var query = createQuery(DEFAULT_MAX_SEARCH_RESULTS, DEFAULT_START); 26 | 27 | var solrQuery = query.toSolrQuery(); 28 | 29 | assertThat(solrQuery).contains(URLEncoder.encode(parameter, StandardCharsets.UTF_8)); 30 | } 31 | 32 | @ParameterizedTest 33 | @ValueSource(strings = {"start=3", "rows=5"}) 34 | void solr_query_should_contain_configuring_parameters(String parameter) { 35 | var query = createQuery(5, 3); 36 | 37 | var solrQuery = query.toSolrQuery(); 38 | 39 | assertThat(solrQuery).contains(parameter); 40 | } 41 | 42 | private CoordinateQuery createQuery(Integer maxSearchResults, Integer start) { 43 | return new CoordinateQuery("foo", "bar", "1.0", maxSearchResults, start); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/search/FormatTypeTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 5 | 6 | import it.mulders.mcs.search.printer.BuildrOutput; 7 | import it.mulders.mcs.search.printer.CoordinatePrinter; 8 | import it.mulders.mcs.search.printer.GradleGroovyOutput; 9 | import it.mulders.mcs.search.printer.GradleGroovyShortOutput; 10 | import it.mulders.mcs.search.printer.GradleKotlinOutput; 11 | import it.mulders.mcs.search.printer.GrapeOutput; 12 | import it.mulders.mcs.search.printer.IvyXmlOutput; 13 | import it.mulders.mcs.search.printer.LeiningenOutput; 14 | import it.mulders.mcs.search.printer.PomXmlOutput; 15 | import it.mulders.mcs.search.printer.SbtOutput; 16 | import java.util.stream.Stream; 17 | import org.junit.jupiter.api.DisplayNameGeneration; 18 | import org.junit.jupiter.api.DisplayNameGenerator; 19 | import org.junit.jupiter.api.Test; 20 | import org.junit.jupiter.params.ParameterizedTest; 21 | import org.junit.jupiter.params.provider.Arguments; 22 | import org.junit.jupiter.params.provider.MethodSource; 23 | import org.junit.jupiter.params.provider.NullSource; 24 | import org.junit.jupiter.params.provider.ValueSource; 25 | 26 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 27 | class FormatTypeTest { 28 | 29 | @ParameterizedTest 30 | @NullSource 31 | void return_default_printer_when_format_type_is_null(String parameter) { 32 | assertThat(FormatType.providePrinter(parameter)).isEqualTo(Constants.DEFAULT_PRINTER); 33 | } 34 | 35 | @ParameterizedTest 36 | @ValueSource(strings = {" ", "nuget"}) 37 | void throw_exception_when_format_type_is_blank_or_unknown(String parameter) { 38 | assertThatThrownBy(() -> FormatType.providePrinter(parameter)).isInstanceOf(UnsupportedFormatException.class); 39 | } 40 | 41 | @Test 42 | void return_expected_printer_when_format_type_contains_leading_and_trailing_white_spaces() { 43 | CoordinatePrinter printer = FormatType.providePrinter(" gradle "); 44 | assertThat(printer.getClass()).isEqualTo(GradleGroovyOutput.class); 45 | } 46 | 47 | @ParameterizedTest 48 | @MethodSource("formatPrinters") 49 | void return_expected_printer_when_format_type_is_valid( 50 | String actual, Class expected) { 51 | CoordinatePrinter printer = FormatType.providePrinter(actual); 52 | assertThat(printer.getClass()).isEqualTo(expected); 53 | } 54 | 55 | private static Stream formatPrinters() { 56 | return Stream.of( 57 | Arguments.of("maven", PomXmlOutput.class), 58 | Arguments.of("gradle", GradleGroovyOutput.class), 59 | Arguments.of("gradle-short", GradleGroovyShortOutput.class), 60 | Arguments.of("gradle-kotlin", GradleKotlinOutput.class), 61 | Arguments.of("sbt", SbtOutput.class), 62 | Arguments.of("ivy", IvyXmlOutput.class), 63 | Arguments.of("grape", GrapeOutput.class), 64 | Arguments.of("leiningen", LeiningenOutput.class), 65 | Arguments.of("buildr", BuildrOutput.class)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/search/SearchClientIT.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search; 2 | 3 | import static com.github.tomakehurst.wiremock.client.WireMock.badRequest; 4 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 5 | import static com.github.tomakehurst.wiremock.client.WireMock.ok; 6 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; 7 | import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; 8 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 9 | 10 | import com.github.tomakehurst.wiremock.core.Options; 11 | import com.github.tomakehurst.wiremock.junit5.WireMockExtension; 12 | import it.mulders.mcs.common.Result; 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.net.ConnectException; 16 | import java.net.http.HttpClient; 17 | import java.nio.charset.StandardCharsets; 18 | import java.util.Arrays; 19 | import org.assertj.core.api.WithAssertions; 20 | import org.junit.jupiter.api.DisplayName; 21 | import org.junit.jupiter.api.DisplayNameGeneration; 22 | import org.junit.jupiter.api.DisplayNameGenerator; 23 | import org.junit.jupiter.api.Nested; 24 | import org.junit.jupiter.api.Test; 25 | import org.junit.jupiter.api.extension.RegisterExtension; 26 | 27 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 28 | class SearchClientIT implements WithAssertions { 29 | private final HttpClient httpClient = HttpClient.newHttpClient(); 30 | 31 | @RegisterExtension 32 | static WireMockExtension wiremock = WireMockExtension.newInstance() 33 | .options(wireMockConfig() 34 | .bindAddress("localhost") 35 | .dynamicPort() 36 | .useChunkedTransferEncoding(Options.ChunkedEncodingPolicy.NEVER)) 37 | .configureStaticDsl(true) 38 | .build(); 39 | 40 | String getResourceAsString(final String resourceName) { 41 | try (final InputStream input = getClass().getResourceAsStream(resourceName)) { 42 | byte[] bytes = input != null ? input.readAllBytes() : new byte[] {}; 43 | return new String(bytes, StandardCharsets.UTF_8); 44 | } catch (final IOException ioe) { 45 | return fail("Can't load resource %s", resourceName, ioe); 46 | } 47 | } 48 | 49 | @Nested 50 | @DisplayName("Wildcard search") 51 | class WildcardSearchTest { 52 | @Test 53 | void should_parse_response() { 54 | // Arrange 55 | var wmRuntimeInfo = wiremock.getRuntimeInfo(); 56 | stubFor(get(urlPathMatching("/solrsearch/select*")) 57 | .willReturn(ok(getResourceAsString("/wildcard-search-response.json")))); 58 | 59 | // Act 60 | var result = new SearchClient(httpClient, wmRuntimeInfo.getHttpBaseUrl()) 61 | .search(new WildcardSearchQuery( 62 | "plexus-utils", Constants.DEFAULT_MAX_SEARCH_RESULTS, Constants.DEFAULT_START)); 63 | 64 | // Assert 65 | assertThat(result.value()).isNotNull(); 66 | assertThat(result.value().response().numFound()).isEqualTo(2); 67 | 68 | var ids = Arrays.stream(result.value().response().docs()) 69 | .map(SearchResponse.Response.Doc::id) 70 | .toArray(String[]::new); 71 | assertThat(ids).containsOnly("plexus:plexus-utils", "org.codehaus.plexus:plexus-utils"); 72 | } 73 | } 74 | 75 | @DisplayName("Singular search") 76 | @Nested 77 | class SingularSearchTest { 78 | @Test 79 | void should_parse_response_groupId_artifactId() { 80 | // Arrange 81 | var wmRuntimeInfo = wiremock.getRuntimeInfo(); 82 | stubFor(get(urlPathMatching("/solrsearch/select*")) 83 | .willReturn(ok(getResourceAsString("/group-artifact-search.json")))); 84 | 85 | // Act 86 | var result = new SearchClient(httpClient, wmRuntimeInfo.getHttpBaseUrl()) 87 | .search(SearchQuery.search("org.codehaus.plexus:plexus-utils") 88 | .build()); 89 | 90 | // Assert 91 | assertThat(result.value()).isNotNull(); 92 | assertThat(result.value().response().numFound()).isEqualTo(1); 93 | 94 | var ids = Arrays.stream(result.value().response().docs()) 95 | .map(SearchResponse.Response.Doc::id) 96 | .toArray(String[]::new); 97 | assertThat(ids).containsOnly("org.codehaus.plexus:plexus-utils"); 98 | } 99 | 100 | @Test 101 | void should_parse_response_groupId_artifactId_version() { 102 | // Arrange 103 | var wmRuntimeInfo = wiremock.getRuntimeInfo(); 104 | stubFor(get(urlPathMatching("/solrsearch/select*")) 105 | .willReturn(ok(getResourceAsString("/group-artifact-version-search.json")))); 106 | 107 | // Act 108 | var result = new SearchClient(httpClient, wmRuntimeInfo.getHttpBaseUrl()) 109 | .search(SearchQuery.search("org.codehaus.plexus:plexus-utils:3.4.1") 110 | .build()); 111 | 112 | // Assert 113 | assertThat(result.value()).isNotNull(); 114 | assertThat(result.value().response().numFound()).isEqualTo(1); 115 | 116 | var ids = Arrays.stream(result.value().response().docs()) 117 | .map(SearchResponse.Response.Doc::id) 118 | .toArray(String[]::new); 119 | assertThat(ids).containsOnly("org.codehaus.plexus:plexus-utils:3.4.1"); 120 | } 121 | } 122 | 123 | @DisplayName("Error handling") 124 | @Nested 125 | class ErrorHandlingTest { 126 | @Test 127 | void should_gracefully_handle_4xx_response() { 128 | // Arrange 129 | var wmRuntimeInfo = wiremock.getRuntimeInfo(); 130 | wiremock.stubFor(get(urlPathMatching("/solrsearch/select*")) 131 | .willReturn(badRequest().withBody("Solr returned 400, msg: "))); 132 | 133 | // Act 134 | var result = new SearchClient(httpClient, wmRuntimeInfo.getHttpBaseUrl()) 135 | .search(SearchQuery.search("org.codehaus.plexus:plexus-utils") 136 | .build()); 137 | 138 | // Assert 139 | assertThat(result).isInstanceOf(Result.Failure.class); 140 | assertThat(result.cause()).isInstanceOf(IllegalStateException.class); 141 | assertThat(result.cause()).hasMessageContaining("https://github.com/mthmulders/mcs/discussions"); 142 | } 143 | 144 | @Test 145 | void should_gracefully_handle_connection_failure() { 146 | // Very unlikely there's an HTTP server running there... 147 | var result = new SearchClient(httpClient, "http://localhost:21") 148 | .search(new WildcardSearchQuery( 149 | "plexus-utils", Constants.DEFAULT_MAX_SEARCH_RESULTS, Constants.DEFAULT_START)); 150 | 151 | assertThat(result).isInstanceOf(Result.Failure.class); 152 | assertThat(result.cause()).isInstanceOf(ConnectException.class); 153 | assertThat(result.cause().getLocalizedMessage()).contains("localhost:21"); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/search/SearchCommandHandlerTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search; 2 | 3 | import static org.mockito.Mockito.any; 4 | import static org.mockito.Mockito.eq; 5 | import static org.mockito.Mockito.mock; 6 | import static org.mockito.Mockito.verify; 7 | import static org.mockito.Mockito.when; 8 | 9 | import it.mulders.mcs.common.Result; 10 | import it.mulders.mcs.search.printer.OutputFactory; 11 | import it.mulders.mcs.search.printer.OutputPrinter; 12 | import it.mulders.mcs.search.vulnerability.ComponentReportClient; 13 | import javax.net.ssl.SSLHandshakeException; 14 | import org.assertj.core.api.WithAssertions; 15 | import org.junit.jupiter.api.DisplayName; 16 | import org.junit.jupiter.api.DisplayNameGeneration; 17 | import org.junit.jupiter.api.DisplayNameGenerator; 18 | import org.junit.jupiter.api.Nested; 19 | import org.junit.jupiter.api.Test; 20 | 21 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 22 | class SearchCommandHandlerTest implements WithAssertions { 23 | private final ComponentReportClient componentReportClient = mock(ComponentReportClient.class); 24 | private final OutputPrinter outputPrinter = mock(OutputPrinter.class); 25 | private final OutputFactory outputFactory = new OutputFactory() { 26 | @Override 27 | public OutputPrinter findOutputPrinter(String formatName) { 28 | return outputPrinter; 29 | } 30 | }; 31 | 32 | private final SearchResponse.Response wildcardResponse = 33 | new SearchResponse.Response(0, 0, new SearchResponse.Response.Doc[] { 34 | new SearchResponse.Response.Doc( 35 | "foo:bar", "foo", "bar", "1.0", "1.0", "jar", System.currentTimeMillis()) 36 | }); 37 | private final SearchResponse.Response twoPartCoordinateResponse = 38 | new SearchResponse.Response(2, 0, new SearchResponse.Response.Doc[] { 39 | new SearchResponse.Response.Doc( 40 | "foo:bar", "foo", "bar", "1.0", "2.0", "jar", System.currentTimeMillis()), 41 | new SearchResponse.Response.Doc( 42 | "foo:bar", "foo", "bar", "2.0", "2.0", "jar", System.currentTimeMillis()) 43 | }); 44 | private final SearchResponse.Response threePartCoordinateResponse = 45 | new SearchResponse.Response(3, 0, new SearchResponse.Response.Doc[] { 46 | new SearchResponse.Response.Doc( 47 | "foo:bar", "foo", "bar", "1.0", "3.0", "jar", System.currentTimeMillis()), 48 | new SearchResponse.Response.Doc( 49 | "foo:bar", "foo", "bar", "2.0", "3.0", "jar", System.currentTimeMillis()), 50 | new SearchResponse.Response.Doc( 51 | "foo:bar", "foo", "bar", "3.0", "3.0", "jar", System.currentTimeMillis()), 52 | }); 53 | private final SearchResponse.Response singleArtifactResponse = 54 | new SearchResponse.Response(1, 0, new SearchResponse.Response.Doc[] { 55 | new SearchResponse.Response.Doc( 56 | "org.codehaus.plexus:plexus-utils:3.4.1", 57 | "org.codehaus.plexus", 58 | "plexus-utils", 59 | "3.4.1", 60 | "3.4.1", 61 | "jar", 62 | System.currentTimeMillis()), 63 | }); 64 | private final SearchResponse.Response multipleArtifactResponse = 65 | new SearchResponse.Response(2, 0, new SearchResponse.Response.Doc[] { 66 | new SearchResponse.Response.Doc( 67 | "org.codehaus.plexus:plexus-utils:3.4.0", 68 | "org.codehaus.plexus", 69 | "plexus-utils", 70 | "3.4.0", 71 | "3.4.1", 72 | "jar", 73 | System.currentTimeMillis()), 74 | new SearchResponse.Response.Doc( 75 | "org.codehaus.plexus:plexus-utils:3.4.1", 76 | "org.codehaus.plexus", 77 | "plexus-utils", 78 | "3.4.1", 79 | "3.4.1", 80 | "jar", 81 | System.currentTimeMillis()) 82 | }); 83 | 84 | private final SearchClient searchClient = mock(SearchClient.class); 85 | ; 86 | 87 | private final SearchCommandHandler handler = 88 | new SearchCommandHandler(componentReportClient, outputFactory, searchClient); 89 | 90 | @Nested 91 | @DisplayName("Wildcard search") 92 | class WildcardSearchTest { 93 | @Test 94 | void should_invoke_search_client() { 95 | // Arrange 96 | when(searchClient.search(any())) 97 | .thenReturn(new Result.Success<>(new SearchResponse(singleArtifactResponse))); 98 | 99 | // Act 100 | handler.search(SearchQuery.search("plexus-utils").build(), "maven", false); 101 | 102 | // Assert 103 | verify(outputPrinter).print(any(WildcardSearchQuery.class), eq(singleArtifactResponse), any()); 104 | } 105 | 106 | @Test 107 | void should_propagate_tls_exception_to_runtime_exception() { 108 | // Arrange 109 | var result = new Result.Failure( 110 | new SSLHandshakeException( 111 | "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target")); 112 | when(searchClient.search(any())).thenReturn(result); 113 | 114 | assertThatThrownBy( 115 | () -> handler.search(SearchQuery.search("tls-error").build(), "maven", false)) 116 | .isInstanceOf(RuntimeException.class); 117 | } 118 | } 119 | 120 | @Nested 121 | @DisplayName("Coordinate search") 122 | class CoordinateSearchTest { 123 | @Test 124 | void should_invoke_search_client_with_groupId_and_artifactId() { 125 | // Arrange 126 | when(searchClient.search(any())) 127 | .thenReturn(new Result.Success<>(new SearchResponse(singleArtifactResponse))); 128 | var handler = new SearchCommandHandler(componentReportClient, outputFactory, searchClient); 129 | var query = SearchQuery.search("org.codehaus.plexus:plexus-utils").build(); 130 | 131 | // Act 132 | handler.search(query, "maven", false); 133 | 134 | // Assert 135 | verify(outputPrinter).print(eq(query), eq(singleArtifactResponse), any()); 136 | } 137 | 138 | @Test 139 | void should_invoke_search_client_with_groupId_and_artifactId_and_version() { 140 | // Arrange 141 | when(searchClient.search(any())) 142 | .thenReturn(new Result.Success<>(new SearchResponse(singleArtifactResponse))); 143 | var handler = new SearchCommandHandler(componentReportClient, outputFactory, searchClient); 144 | 145 | // Act 146 | handler.search( 147 | SearchQuery.search("org.codehaus.plexus:plexus-utils").build(), "maven", false); 148 | 149 | // Assert 150 | verify(outputPrinter).print(any(CoordinateQuery.class), eq(singleArtifactResponse), any()); 151 | } 152 | 153 | @Test 154 | void should_propagate_tls_exception_to_runtime_exception() { 155 | assertThatThrownBy(() -> handler.search( 156 | SearchQuery.search("org.codehaus.plexus:tls-error:3.4.1") 157 | .build(), 158 | "maven", 159 | false)) 160 | .isInstanceOf(RuntimeException.class); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/search/SearchQueryTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search; 2 | 3 | import java.net.URLEncoder; 4 | import java.nio.charset.StandardCharsets; 5 | import org.assertj.core.api.WithAssertions; 6 | import org.junit.jupiter.api.DisplayNameGeneration; 7 | import org.junit.jupiter.api.DisplayNameGenerator; 8 | import org.junit.jupiter.api.Nested; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.params.ParameterizedTest; 11 | import org.junit.jupiter.params.provider.CsvSource; 12 | 13 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 14 | class SearchQueryTest implements WithAssertions { 15 | @Nested 16 | class CoordinateQueryTest { 17 | @Test 18 | void should_build_query_with_groupId_and_artifactId() { 19 | var query = SearchQuery.search("org.codehaus.plexus:plexus-utils").build(); 20 | assertThat(query).isInstanceOf(CoordinateQuery.class).satisfies(q -> { 21 | assertThat(((CoordinateQuery) q).groupId()).isEqualTo("org.codehaus.plexus"); 22 | assertThat(((CoordinateQuery) q).artifactId()).isEqualTo("plexus-utils"); 23 | assertThat(((CoordinateQuery) q).version()).isNullOrEmpty(); 24 | }); 25 | } 26 | 27 | @Test 28 | void should_build_query_with_groupId_and_artifactId_and_version() { 29 | var query = 30 | SearchQuery.search("org.codehaus.plexus:plexus-utils:3.4.1").build(); 31 | assertThat(query).isInstanceOf(CoordinateQuery.class).satisfies(q -> { 32 | assertThat(((CoordinateQuery) q).groupId()).isEqualTo("org.codehaus.plexus"); 33 | assertThat(((CoordinateQuery) q).artifactId()).isEqualTo("plexus-utils"); 34 | assertThat(((CoordinateQuery) q).version()).isEqualTo("3.4.1"); 35 | }); 36 | } 37 | 38 | @Test 39 | void should_build_query_with_only_groupId() { 40 | var query = SearchQuery.search("org.codehaus.plexus:").build(); 41 | assertThat(query).isInstanceOf(CoordinateQuery.class).satisfies(q -> { 42 | assertThat(((CoordinateQuery) q).groupId()).isEqualTo("org.codehaus.plexus"); 43 | assertThat(((CoordinateQuery) q).artifactId()).isNullOrEmpty(); 44 | assertThat(((CoordinateQuery) q).version()).isNullOrEmpty(); 45 | }); 46 | } 47 | 48 | @Test 49 | void should_build_query_with_only_artifactId() { 50 | var query = SearchQuery.search(":plexus-utils").build(); 51 | assertThat(query).isInstanceOf(CoordinateQuery.class).satisfies(q -> { 52 | assertThat(((CoordinateQuery) q).groupId()).isNullOrEmpty(); 53 | assertThat(((CoordinateQuery) q).artifactId()).isEqualTo("plexus-utils"); 54 | assertThat(((CoordinateQuery) q).version()).isNullOrEmpty(); 55 | }); 56 | } 57 | 58 | @Test 59 | void should_reject_invalid_input() { 60 | assertThatThrownBy(() -> SearchQuery.search("foo:bar:baz:qux").build()) 61 | .isInstanceOf(IllegalArgumentException.class); 62 | } 63 | 64 | @ParameterizedTest 65 | @CsvSource( 66 | textBlock = 67 | """ 68 | org.codehaus.plexus:plexus-utils,g:org.codehaus.plexus AND a:plexus-utils 69 | org.codehaus.plexus:plexus-utils:3.4.1,g:org.codehaus.plexus AND a:plexus-utils AND v:3.4.1 70 | org.codehaus.plexus:,g:org.codehaus.plexus 71 | :plexus-utils,a:plexus-utils 72 | """) 73 | void should_construct_valid_solr_query(String input, String solrQuery) { 74 | var result = SearchQuery.search(input).build().toSolrQuery(); 75 | 76 | assertThat(result).contains("q=" + URLEncoder.encode(solrQuery, StandardCharsets.UTF_8)); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/search/WildcardSearchQueryTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search; 2 | 3 | import org.assertj.core.api.WithAssertions; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Nested; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class WildcardSearchQueryTest implements WithAssertions { 9 | @Nested 10 | @DisplayName("toSolrQuery") 11 | class ToSolrQueryTest { 12 | @Test 13 | void solr_query_should_contain_limit() { 14 | var query = new WildcardSearchQuery("foo", 5, Constants.DEFAULT_START); 15 | 16 | var solrQuery = query.toSolrQuery(); 17 | 18 | assertThat(solrQuery).contains("rows=5"); 19 | } 20 | 21 | @Test 22 | void solr_query_should_contain_search_term() { 23 | var query = new WildcardSearchQuery("foo", Constants.DEFAULT_MAX_SEARCH_RESULTS, Constants.DEFAULT_START); 24 | 25 | var solrQuery = query.toSolrQuery(); 26 | 27 | assertThat(solrQuery).contains("q=foo"); 28 | } 29 | 30 | @Test 31 | void solr_query_should_contain_start() { 32 | var query = new WildcardSearchQuery("foo", Constants.DEFAULT_MAX_SEARCH_RESULTS, 3); 33 | 34 | var solrQuery = query.toSolrQuery(); 35 | 36 | assertThat(solrQuery).contains("start=3"); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/search/printer/CoordinatePrinterTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | import it.mulders.mcs.search.SearchQuery; 4 | import it.mulders.mcs.search.SearchResponse; 5 | import it.mulders.mcs.search.vulnerability.ComponentReportResponse.ComponentReport; 6 | import it.mulders.mcs.search.vulnerability.ComponentReportResponse.ComponentReport.ComponentReportVulnerability; 7 | import java.io.ByteArrayOutputStream; 8 | import java.io.PrintStream; 9 | import java.util.stream.Stream; 10 | import org.assertj.core.api.WithAssertions; 11 | import org.junit.jupiter.api.DisplayName; 12 | import org.junit.jupiter.api.Nested; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.params.ParameterizedTest; 15 | import org.junit.jupiter.params.provider.Arguments; 16 | import org.junit.jupiter.params.provider.MethodSource; 17 | 18 | class CoordinatePrinterTest implements WithAssertions { 19 | 20 | private static final SearchQuery QUERY = 21 | SearchQuery.search("org.codehaus.plexus:plexus-utils").build(); 22 | private static final SearchResponse.Response PLUGIN_RESPONSE = 23 | new SearchResponse.Response(1, 0, new SearchResponse.Response.Doc[] { 24 | new SearchResponse.Response.Doc( 25 | "org.apache.maven.plugins:maven-jar-plugin:3.3.0", 26 | "org.apache.maven.plugins", 27 | "maven-jar-plugin", 28 | "3.3.0", 29 | null, 30 | "maven-plugin", 31 | 1630022910000L) 32 | }); 33 | private static final SearchResponse.Response RESPONSE = 34 | new SearchResponse.Response(1, 0, new SearchResponse.Response.Doc[] { 35 | new SearchResponse.Response.Doc( 36 | "org.codehaus.plexus:plexus-utils:3.4.1", 37 | "org.codehaus.plexus", 38 | "plexus-utils", 39 | "3.4.1", 40 | null, 41 | "jar", 42 | 1630022910000L) 43 | }); 44 | private static final String POM_XML_DEPENDENCY_OUTPUT = 45 | """ 46 | 47 | org.codehaus.plexus 48 | plexus-utils 49 | 3.4.1 50 | 51 | """; 52 | private static final String POM_XML_PLUGIN_OUTPUT = 53 | """ 54 | 55 | org.apache.maven.plugins 56 | maven-jar-plugin 57 | 3.3.0 58 | 59 | """; 60 | private static final String GRADLE_GROOVY_OUTPUT = 61 | "implementation group: 'org.codehaus.plexus', name: 'plexus-utils', version: '3.4.1'"; 62 | private static final String GRADLE_GROOVY_SHORT_OUTPUT = "implementation 'org.codehaus.plexus:plexus-utils:3.4.1'"; 63 | private static final String GRADLE_KOTLIN_OUTPUT = "implementation(\"org.codehaus.plexus:plexus-utils:3.4.1\")"; 64 | private static final String SBT_OUTPUT = 65 | """ 66 | libraryDependencies += "org.codehaus.plexus" % "plexus-utils" % "3.4.1" 67 | """; 68 | private static final String IVY_XML_OUTPUT = 69 | """ 70 | 71 | """; 72 | private static final String GRAPE_OUTPUT = 73 | """ 74 | @Grapes( 75 | @Grab(group='org.codehaus.plexus', module='plexus-utils', version='3.4.1') 76 | ) 77 | """; 78 | private static final String LEININGEN_OUTPUT = "[org.codehaus.plexus/plexus-utils \"3.4.1\"]"; 79 | private static final String BUILDR_OUTPUT = "'org.codehaus.plexus:plexus-utils:jar:3.4.1'"; 80 | private static final String JBANG_OUTPUT = "//DEPS org.codehaus.plexus:plexus-utils:3.4.1"; 81 | private static final String GAV_OUTPUT = "org.codehaus.plexus:plexus-utils:3.4.1"; 82 | 83 | private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 84 | 85 | private static Stream coordinatePrinters() { 86 | return Stream.of( 87 | Arguments.of(new PomXmlOutput(), POM_XML_DEPENDENCY_OUTPUT, RESPONSE), 88 | Arguments.of(new PomXmlOutput(), POM_XML_PLUGIN_OUTPUT, PLUGIN_RESPONSE), 89 | Arguments.of(new GradleGroovyOutput(), GRADLE_GROOVY_OUTPUT, RESPONSE), 90 | Arguments.of(new GradleGroovyShortOutput(), GRADLE_GROOVY_SHORT_OUTPUT, RESPONSE), 91 | Arguments.of(new GradleKotlinOutput(), GRADLE_KOTLIN_OUTPUT, RESPONSE), 92 | Arguments.of(new SbtOutput(), SBT_OUTPUT, RESPONSE), 93 | Arguments.of(new IvyXmlOutput(), IVY_XML_OUTPUT, RESPONSE), 94 | Arguments.of(new GrapeOutput(), GRAPE_OUTPUT, RESPONSE), 95 | Arguments.of(new LeiningenOutput(), LEININGEN_OUTPUT, RESPONSE), 96 | Arguments.of(new BuildrOutput(), BUILDR_OUTPUT, RESPONSE), 97 | Arguments.of(new JBangOutput(), JBANG_OUTPUT, RESPONSE), 98 | Arguments.of(new GavOutput(), GAV_OUTPUT, RESPONSE)); 99 | } 100 | 101 | @ParameterizedTest 102 | @MethodSource("coordinatePrinters") 103 | void should_print_snippet(CoordinatePrinter printer, String expected, SearchResponse.Response response) { 104 | printer.print(QUERY, response, new PrintStream(buffer)); 105 | var xml = buffer.toString(); 106 | 107 | assertThat(xml).isEqualToIgnoringWhitespace(expected); 108 | } 109 | 110 | @Nested 111 | @DisplayName("CoordinatePrinter with vulnerabilities") 112 | class CoordinatePrinterWithMultipleVulnerabilitiesTest { 113 | private static final SearchQuery QUERY_DEPENDENCY_WITH_VULNERABILITIES = 114 | SearchQuery.search("org.apache.shiro:shiro-web").build(); 115 | private static final SearchResponse.Response RESPONSE_WITH_VULNERABILITIES = 116 | new SearchResponse.Response(1, 0, new SearchResponse.Response.Doc[] { 117 | new SearchResponse.Response.Doc( 118 | "org.apache.shiro:shiro-web:1.9.0", 119 | "org.apache.shiro", 120 | "shiro-web", 121 | "1.9.0", 122 | null, 123 | "jar", 124 | 1630022910000L, 125 | new ComponentReport( 126 | "pkg:maven/org.apache.shiro/shiro-web@1.9.0", 127 | "https://ossindex.sonatype.org/component/pkg:maven/org.apache.shiro/shiro-web@1.9.0", 128 | new ComponentReportVulnerability[] { 129 | new ComponentReportVulnerability( 130 | "CVE-2023-34478", 131 | "CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')", 132 | 9.8, 133 | "https://ossindex.sonatype.org/vulnerability/CVE-2023-34478?component-type=maven&component-name=org.apache.shiro%2Fshiro-web"), 134 | new ComponentReportVulnerability( 135 | "CVE-2022-40664", 136 | "CWE-287: Improper Authentication", 137 | 9.8, 138 | "https://ossindex.sonatype.org/vulnerability/CVE-2022-40664?component-type=maven&component-name=org.apache.shiro%2Fshiro-web") 139 | })) 140 | }); 141 | private static final String POM_XML_DEPENDENCY_OUTPUT_WITH_VULNERABILITIES = 142 | """ 143 | 144 | org.apache.shiro 145 | shiro-web 146 | 1.9.0 147 | 148 | 149 | Vulnerabilities: 150 | CVE-2023-34478 (Critical) - https://ossindex.sonatype.org/vulnerability/CVE-2023-34478?component-type=maven&component-name=org.apache.shiro%2Fshiro-web 151 | CVE-2022-40664 (Critical) - https://ossindex.sonatype.org/vulnerability/CVE-2022-40664?component-type=maven&component-name=org.apache.shiro%2Fshiro-web 152 | """; 153 | 154 | @Test 155 | void should_print_vulnerability_text() { 156 | CoordinatePrinter printer = new PomXmlOutput(); 157 | printer.print( 158 | QUERY_DEPENDENCY_WITH_VULNERABILITIES, RESPONSE_WITH_VULNERABILITIES, new PrintStream(buffer)); 159 | var xml = buffer.toString(); 160 | assertThat(xml).isEqualToIgnoringWhitespace(POM_XML_DEPENDENCY_OUTPUT_WITH_VULNERABILITIES); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/search/printer/DelegatingOutputPrinterTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | import static org.mockito.Mockito.any; 4 | import static org.mockito.Mockito.eq; 5 | import static org.mockito.Mockito.mock; 6 | import static org.mockito.Mockito.never; 7 | import static org.mockito.Mockito.verify; 8 | 9 | import it.mulders.mcs.search.SearchQuery; 10 | import it.mulders.mcs.search.SearchResponse; 11 | import java.io.PrintStream; 12 | import org.apache.commons.io.output.NullOutputStream; 13 | import org.assertj.core.api.WithAssertions; 14 | import org.junit.jupiter.api.DisplayNameGeneration; 15 | import org.junit.jupiter.api.DisplayNameGenerator; 16 | import org.junit.jupiter.api.Test; 17 | 18 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 19 | class DelegatingOutputPrinterTest implements WithAssertions { 20 | private final OutputPrinter noOutput = mock(OutputPrinter.class); 21 | private final OutputPrinter coordinateOutput = mock(OutputPrinter.class); 22 | private final OutputPrinter tabularSearchOutput = mock(OutputPrinter.class); 23 | 24 | private final DelegatingOutputPrinter printer = 25 | new DelegatingOutputPrinter(noOutput, coordinateOutput, tabularSearchOutput); 26 | 27 | private final SearchQuery query = 28 | SearchQuery.search("org.codehaus.plexus:plexus-utils").build(); 29 | private final PrintStream outputStream = new PrintStream(NullOutputStream.nullOutputStream()); 30 | 31 | @Test 32 | void no_results_delegate() { 33 | printer.print(query, responseWithResult(0), outputStream); 34 | verify(noOutput).print(eq(query), any(), eq(outputStream)); 35 | verify(coordinateOutput, never()).print(any(), any(), any()); 36 | verify(tabularSearchOutput, never()).print(any(), any(), any()); 37 | } 38 | 39 | @Test 40 | void one_result_delegate() { 41 | printer.print(query, responseWithResult(1), outputStream); 42 | verify(noOutput, never()).print(any(), any(), any()); 43 | verify(coordinateOutput).print(eq(query), any(), eq(outputStream)); 44 | verify(tabularSearchOutput, never()).print(any(), any(), any()); 45 | } 46 | 47 | @Test 48 | void multiple_results_delegate() { 49 | printer.print(query, responseWithResult(2), outputStream); 50 | verify(noOutput, never()).print(any(), any(), any()); 51 | verify(coordinateOutput, never()).print(any(), any(), any()); 52 | verify(tabularSearchOutput).print(eq(query), any(), eq(outputStream)); 53 | } 54 | 55 | private SearchResponse.Response responseWithResult(int count) { 56 | return new SearchResponse.Response(count, 0, new SearchResponse.Response.Doc[0]); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/search/printer/NoOutputPrinterTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | import it.mulders.mcs.search.SearchResponse; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.PrintStream; 6 | import java.nio.charset.StandardCharsets; 7 | import org.assertj.core.api.WithAssertions; 8 | import org.junit.jupiter.api.DisplayNameGeneration; 9 | import org.junit.jupiter.api.DisplayNameGenerator; 10 | import org.junit.jupiter.api.Test; 11 | 12 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 13 | class NoOutputPrinterTest implements WithAssertions { 14 | private final OutputPrinter printer = new NoOutputPrinter(); 15 | 16 | private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 17 | 18 | @Test 19 | void should_fail_with_non_empty_result() { 20 | assertThatThrownBy(() -> printer.print(null, responseWithResult(1), new PrintStream(outputStream))) 21 | .isInstanceOf(IllegalArgumentException.class); 22 | assertThatThrownBy(() -> printer.print(null, responseWithResult(2), new PrintStream(outputStream))) 23 | .isInstanceOf(IllegalArgumentException.class); 24 | } 25 | 26 | @Test 27 | void should_print_message_with_empty_result() { 28 | printer.print(null, responseWithResult(0), new PrintStream(outputStream)); 29 | assertThat(outputStream.toString(StandardCharsets.UTF_8)).contains("No results found"); 30 | } 31 | 32 | private SearchResponse.Response responseWithResult(int count) { 33 | return new SearchResponse.Response(count, 0, new SearchResponse.Response.Doc[0]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/search/printer/TabularOutputPrinterTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.printer; 2 | 3 | import it.mulders.mcs.search.SearchQuery; 4 | import it.mulders.mcs.search.SearchResponse; 5 | import it.mulders.mcs.search.vulnerability.ComponentReportResponse.ComponentReport; 6 | import it.mulders.mcs.search.vulnerability.ComponentReportResponse.ComponentReport.ComponentReportVulnerability; 7 | import java.io.ByteArrayOutputStream; 8 | import java.io.PrintStream; 9 | import java.time.Instant; 10 | import java.time.ZoneId; 11 | import java.time.format.DateTimeFormatter; 12 | import org.assertj.core.api.WithAssertions; 13 | import org.junit.jupiter.api.DisplayNameGeneration; 14 | import org.junit.jupiter.api.DisplayNameGenerator; 15 | import org.junit.jupiter.api.Test; 16 | 17 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 18 | class TabularOutputPrinterTest implements WithAssertions { 19 | private final TabularOutputPrinter output = new TabularOutputPrinter(); 20 | private final SearchQuery query = 21 | SearchQuery.search("org.codehaus.plexus:plexus-utils").build(); 22 | 23 | @Test 24 | void should_print_gav() { 25 | // Arrange 26 | var response = new SearchResponse.Response(1, 0, new SearchResponse.Response.Doc[] { 27 | new SearchResponse.Response.Doc( 28 | "org.codehaus.plexus:plexus-utils", 29 | "org.codehaus.plexus", 30 | "plexus-utils", 31 | null, 32 | "3.4.1", 33 | "jar", 34 | 1630022910000L) 35 | }); 36 | var buffer = new ByteArrayOutputStream(); 37 | 38 | // Act 39 | output.print(query, response, new PrintStream(buffer)); 40 | 41 | // Assert 42 | var table = buffer.toString(); 43 | assertThat(table).contains("org.codehaus.plexus:plexus-utils:3.4.1"); 44 | } 45 | 46 | @Test 47 | void should_print_last_updated() { 48 | // Arrange 49 | var response = new SearchResponse.Response(1, 0, new SearchResponse.Response.Doc[] { 50 | new SearchResponse.Response.Doc( 51 | "org.codehaus.plexus:plexus-utils", 52 | "org.codehaus.plexus", 53 | "plexus-utils", 54 | null, 55 | "3.4.1", 56 | "jar", 57 | 1630022910000L) 58 | }); 59 | var buffer = new ByteArrayOutputStream(); 60 | 61 | // Act 62 | output.print(query, response, new PrintStream(buffer)); 63 | 64 | // Assert 65 | var table = buffer.toString(); 66 | var lastUpdated = DateTimeFormatter.ofPattern("dd MMM yyyy 'at' HH:mm (zzz)") 67 | .format(Instant.ofEpochMilli(1630022910000L).atZone(ZoneId.systemDefault())); 68 | assertThat(table).contains(lastUpdated); 69 | } 70 | 71 | @Test 72 | void should_mention_number_of_results() { 73 | // Arrange 74 | var response = new SearchResponse.Response(1, 0, new SearchResponse.Response.Doc[] { 75 | new SearchResponse.Response.Doc( 76 | "org.codehaus.plexus:plexus-utils", 77 | "org.codehaus.plexus", 78 | "plexus-utils", 79 | null, 80 | "3.4.1", 81 | "jar", 82 | 1630022910000L) 83 | }); 84 | var buffer = new ByteArrayOutputStream(); 85 | 86 | // Act 87 | output.print(query, response, new PrintStream(buffer)); 88 | 89 | // Assert 90 | var table = buffer.toString(); 91 | assertThat(table).contains("Found 1 results"); 92 | } 93 | 94 | @Test 95 | void should_not_mention_latest_version_when_not_present() { 96 | // Arrange 97 | var response = new SearchResponse.Response(4, 0, new SearchResponse.Response.Doc[] { 98 | new SearchResponse.Response.Doc( 99 | "org.codehaus.plexus:plexus-utils", 100 | "org.codehaus.plexus", 101 | "plexus-utils", 102 | null, 103 | null, 104 | "jar", 105 | 1630022910000L), 106 | new SearchResponse.Response.Doc( 107 | "org.codehaus.plexus:plexus-archiver", 108 | "org.codehaus.plexus", 109 | "plexus-archiver", 110 | null, 111 | null, 112 | "jar", 113 | 1630022910000L) 114 | }); 115 | var buffer = new ByteArrayOutputStream(); 116 | 117 | // Act 118 | var query = SearchQuery.search("org.codehaus.plexus:plexus-utils") 119 | .withLimit(2) 120 | .build(); 121 | output.print(query, response, new PrintStream(buffer)); 122 | 123 | // Assert 124 | var table = buffer.toString(); 125 | assertThat(table).doesNotContain("null"); 126 | } 127 | 128 | @Test 129 | void should_mention_when_number_of_results_is_larger_than_the_search_limit() { 130 | // Arrange 131 | var response = new SearchResponse.Response(4, 0, new SearchResponse.Response.Doc[] { 132 | new SearchResponse.Response.Doc( 133 | "org.codehaus.plexus:plexus-utils", 134 | "org.codehaus.plexus", 135 | "plexus-utils", 136 | null, 137 | "3.4.1", 138 | "jar", 139 | 1630022910000L), 140 | new SearchResponse.Response.Doc( 141 | "org.codehaus.plexus:plexus-archiver", 142 | "org.codehaus.plexus", 143 | "plexus-archiver", 144 | null, 145 | "4.2.7", 146 | "jar", 147 | 1630022910000L) 148 | }); 149 | var buffer = new ByteArrayOutputStream(); 150 | 151 | // Act 152 | var query = SearchQuery.search("org.codehaus.plexus:plexus-utils") 153 | .withLimit(2) 154 | .build(); 155 | output.print(query, response, new PrintStream(buffer)); 156 | 157 | // Assert 158 | var table = buffer.toString(); 159 | assertThat(table).contains("showing 2"); 160 | } 161 | 162 | @Test 163 | void should_not_mention_when_number_of_results_is_equal_to_the_search_limit() { 164 | // Arrange 165 | var response = new SearchResponse.Response(2, 0, new SearchResponse.Response.Doc[] { 166 | new SearchResponse.Response.Doc( 167 | "org.codehaus.plexus:plexus-utils", 168 | "org.codehaus.plexus", 169 | "plexus-utils", 170 | null, 171 | "3.4.1", 172 | "jar", 173 | 1630022910000L), 174 | new SearchResponse.Response.Doc( 175 | "org.codehaus.plexus:plexus-archiver", 176 | "org.codehaus.plexus", 177 | "plexus-archiver", 178 | null, 179 | "4.2.7", 180 | "jar", 181 | 1630022910000L) 182 | }); 183 | var buffer = new ByteArrayOutputStream(); 184 | 185 | // Act 186 | var query = SearchQuery.search("org.codehaus.plexus:plexus-utils") 187 | .withLimit(2) 188 | .build(); 189 | output.print(query, response, new PrintStream(buffer)); 190 | 191 | // Assert 192 | var table = buffer.toString(); 193 | assertThat(table).doesNotContain("showing 1"); 194 | } 195 | 196 | @Test 197 | void should_not_mention_when_number_of_results_is_smaller_than_the_search_limit() { 198 | // Arrange 199 | var response = new SearchResponse.Response(1, 0, new SearchResponse.Response.Doc[] { 200 | new SearchResponse.Response.Doc( 201 | "org.codehaus.plexus:plexus-utils", 202 | "org.codehaus.plexus", 203 | "plexus-utils", 204 | null, 205 | "3.4.1", 206 | "jar", 207 | 1630022910000L) 208 | }); 209 | var buffer = new ByteArrayOutputStream(); 210 | 211 | // Act 212 | var query = SearchQuery.search("org.codehaus.plexus:plexus-utils") 213 | .withLimit(2) 214 | .build(); 215 | output.print(query, response, new PrintStream(buffer)); 216 | 217 | // Assert 218 | var table = buffer.toString(); 219 | assertThat(table).doesNotContain("showing 1"); 220 | } 221 | 222 | @Test 223 | void should_print_vulnerability_text() { 224 | // Arrange 225 | var output = new TabularOutputPrinter(true); 226 | var query = SearchQuery.search("org.apache.shiro:shiro-web").build(); 227 | 228 | var response = new SearchResponse.Response(1, 0, new SearchResponse.Response.Doc[] { 229 | new SearchResponse.Response.Doc( 230 | "org.apache.shiro:shiro-web:1.10.0", 231 | "org.apache.shiro", 232 | "shiro-web", 233 | "1.10.0", 234 | null, 235 | "jar", 236 | 1630022910000L, 237 | new ComponentReport( 238 | "pkg:maven/org.apache.shiro/shiro-web@1.10.0", 239 | "https://ossindex.sonatype.org/component/pkg:maven/org.apache.shiro/shiro-web@1.10.0", 240 | new ComponentReportVulnerability[] { 241 | new ComponentReportVulnerability( 242 | "CVE-2023-34478", 243 | "CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')", 244 | 9.8, 245 | "https://ossindex.sonatype.org/vulnerability/CVE-2023-34478?component-type=maven&component-name=org.apache.shiro%2Fshiro-web"), 246 | new ComponentReportVulnerability( 247 | "CVE-2020-13933", 248 | "[CVE-2020-13933] CWE-287: Improper Authentication", 249 | 7.5, 250 | "https://ossindex.sonatype.org/vulnerability/CVE-2020-13933?component-type=maven&component-name=org.apache.shiro%2Fshiro-web") 251 | })) 252 | }); 253 | 254 | // Act 255 | var buffer = new ByteArrayOutputStream(); 256 | output.print(query, response, new PrintStream(buffer)); 257 | 258 | // Assert 259 | var table = buffer.toString(); 260 | assertThat(table).contains("Vulnerabilities"); 261 | assertThat(table).contains("1 Critical, 1 High"); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/search/vulnerability/ComponentReportClientIT.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.vulnerability; 2 | 3 | import static com.github.tomakehurst.wiremock.client.WireMock.badRequest; 4 | import static com.github.tomakehurst.wiremock.client.WireMock.ok; 5 | import static com.github.tomakehurst.wiremock.client.WireMock.post; 6 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; 7 | import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; 8 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 9 | 10 | import com.github.tomakehurst.wiremock.core.Options; 11 | import com.github.tomakehurst.wiremock.junit5.WireMockExtension; 12 | import it.mulders.mcs.common.Result; 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.net.ConnectException; 16 | import java.net.http.HttpClient; 17 | import java.nio.charset.StandardCharsets; 18 | import java.util.Arrays; 19 | import java.util.List; 20 | import org.assertj.core.api.WithAssertions; 21 | import org.junit.jupiter.api.DisplayName; 22 | import org.junit.jupiter.api.DisplayNameGeneration; 23 | import org.junit.jupiter.api.DisplayNameGenerator; 24 | import org.junit.jupiter.api.Nested; 25 | import org.junit.jupiter.api.Test; 26 | import org.junit.jupiter.api.extension.RegisterExtension; 27 | import org.junitpioneer.jupiter.SetSystemProperty; 28 | import org.junitpioneer.jupiter.SetSystemProperty.SetSystemProperties; 29 | 30 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 31 | class ComponentReportClientIT implements WithAssertions { 32 | private final HttpClient httpClient = HttpClient.newHttpClient(); 33 | 34 | @RegisterExtension 35 | static WireMockExtension wiremock = WireMockExtension.newInstance() 36 | .options(wireMockConfig() 37 | .bindAddress("localhost") 38 | .dynamicPort() 39 | .useChunkedTransferEncoding(Options.ChunkedEncodingPolicy.NEVER)) 40 | .configureStaticDsl(true) 41 | .build(); 42 | 43 | String getResourceAsString(final String resourceName) { 44 | try (final InputStream input = getClass().getResourceAsStream(resourceName)) { 45 | byte[] bytes = input != null ? input.readAllBytes() : new byte[] {}; 46 | return new String(bytes, StandardCharsets.UTF_8); 47 | 48 | } catch (final IOException ioe) { 49 | return fail("Can't load resource %s", resourceName, ioe); 50 | } 51 | } 52 | 53 | @Nested 54 | @DisplayName("ComponentReport with vulnerabilities") 55 | class ComponentReportWithVulnerabilitiesTest { 56 | @Test 57 | @SetSystemProperties({ 58 | @SetSystemProperty(key = "ossindex.username", value = "user"), 59 | @SetSystemProperty(key = "ossindex.password", value = "pass") 60 | }) 61 | void should_parse_response() { 62 | // Arrange 63 | var wmRuntimeInfo = wiremock.getRuntimeInfo(); 64 | stubFor(post(urlPathMatching("/api/v3/authorized/component-report")) 65 | .withBasicAuth("user", "pass") 66 | .willReturn(ok(getResourceAsString("/vulnerabilities-component-report-response.json")))); 67 | 68 | // Act 69 | var result = new ComponentReportClient(httpClient, wmRuntimeInfo.getHttpBaseUrl()) 70 | .search(List.of("pkg:maven/org.apache.shiro/shiro-web@1.9.0")); 71 | 72 | // Assert 73 | assertThat(result.value()).isNotNull(); 74 | assertThat(result.value().componentReports()).hasSize(1); 75 | assertThat(result.value().componentReports()[0].vulnerabilities()).hasSize(2); 76 | 77 | var ids = Arrays.stream(result.value().componentReports()[0].vulnerabilities()) 78 | .map(ComponentReportResponse.ComponentReport.ComponentReportVulnerability::id) 79 | .toArray(String[]::new); 80 | assertThat(ids).containsOnly("CVE-2022-40664", "CVE-2023-34478"); 81 | } 82 | } 83 | 84 | @Nested 85 | @DisplayName("ComponentReport with no vulnerabilities") 86 | class ComponentReportWithNoVulnerabilitiesTest { 87 | @Test 88 | void should_parse_response() { 89 | // Arrange 90 | var wmRuntimeInfo = wiremock.getRuntimeInfo(); 91 | stubFor(post(urlPathMatching("/api/v3/component-report")) 92 | .willReturn(ok(getResourceAsString("/no-vulnerabilities-component-report-response.json")))); 93 | 94 | // Act 95 | var result = new ComponentReportClient(httpClient, wmRuntimeInfo.getHttpBaseUrl()) 96 | .search(List.of("pkg:maven/org.codehaus.plexus/plexus-utils@3.4.1")); 97 | 98 | // Assert 99 | assertThat(result.value()).isNotNull(); 100 | assertThat(result.value().componentReports()).hasSize(1); 101 | assertThat(result.value().componentReports()[0].vulnerabilities()).isEmpty(); 102 | } 103 | } 104 | 105 | @DisplayName("Error handling") 106 | @Nested 107 | class ErrorHandlingTest { 108 | @Test 109 | void should_gracefully_handle_4xx_response() { 110 | // Arrange 111 | var wmRuntimeInfo = wiremock.getRuntimeInfo(); 112 | stubFor(post(urlPathMatching("/api/v3/component-report")) 113 | .willReturn(badRequest().withBody("Ossindex returned 400, msg: "))); 114 | 115 | // Act 116 | var result = new ComponentReportClient(httpClient, wmRuntimeInfo.getHttpBaseUrl()) 117 | .search(List.of("pkg:maven/org.codehaus.plexus/plexus-utils@3.4.1")); 118 | 119 | // Assert 120 | assertThat(result).isInstanceOf(Result.Failure.class); 121 | assertThat(result.cause()).isInstanceOf(IllegalStateException.class); 122 | assertThat(result.cause()).hasMessageContaining("https://github.com/mthmulders/mcs/discussions"); 123 | } 124 | 125 | @Test 126 | void should_gracefully_handle_connection_failure() { 127 | // Very unlikely there's an HTTP server running there... 128 | var result = new ComponentReportClient(httpClient, "http://localhost:21") 129 | .search(List.of("pkg:maven/org.codehaus.plexus/plexus-utils@3.4.1")); 130 | 131 | assertThat(result).isInstanceOf(Result.Failure.class); 132 | assertThat(result.cause()).isInstanceOf(ConnectException.class); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/search/vulnerability/ComponentReportResponseBodyHandlerTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.vulnerability; 2 | 3 | import it.mulders.mcs.common.Result; 4 | import it.mulders.mcs.search.vulnerability.ComponentReportResponse.ComponentReport.ComponentReportVulnerability; 5 | import org.assertj.core.api.WithAssertions; 6 | import org.junit.jupiter.api.DisplayNameGeneration; 7 | import org.junit.jupiter.api.DisplayNameGenerator; 8 | import org.junit.jupiter.api.Test; 9 | 10 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 11 | class ComponentReportResponseBodyHandlerTest implements WithAssertions { 12 | @Test 13 | void parse_component_report_with_vulnerabilities() { 14 | // Arrange 15 | var input = getClass().getResourceAsStream("/vulnerabilities-component-report-response.json"); 16 | 17 | // Act 18 | var result = ComponentReportResponseBodyHandler.toComponentReportResponse(input); 19 | 20 | // Assert 21 | assertThat(result).isInstanceOf(Result.Success.class); 22 | var response = result.value(); 23 | assertThat(response.componentReports()).hasSize(1); 24 | assertThat(response.componentReports()[0].coordinates()) 25 | .isEqualTo("pkg:maven/org.apache.shiro/shiro-web@1.9.0"); 26 | assertThat(response.componentReports()[0].reference()) 27 | .isEqualTo("https://ossindex.sonatype.org/component/pkg:maven/org.apache.shiro/shiro-web@1.9.0"); 28 | 29 | assertThat(response.componentReports()[0].vulnerabilities()).hasSize(2); 30 | assertThat(response.componentReports()[0].vulnerabilities()) 31 | .contains( 32 | new ComponentReportVulnerability( 33 | "CVE-2022-40664", 34 | "[CVE-2022-40664] CWE-287: Improper Authentication", 35 | 9.8, 36 | "https://ossindex.sonatype.org/vulnerability/CVE-2022-40664?component-type=maven&component-name=org.apache.shiro%2Fshiro-web")); 37 | assertThat(response.componentReports()[0].vulnerabilities()) 38 | .contains( 39 | new ComponentReportVulnerability( 40 | "CVE-2023-34478", 41 | "[CVE-2023-34478] CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')", 42 | 9.8, 43 | "https://ossindex.sonatype.org/vulnerability/CVE-2023-34478?component-type=maven&component-name=org.apache.shiro%2Fshiro-web")); 44 | } 45 | 46 | @Test 47 | void parse_component_report_with_no_vulnerabilities() { 48 | // Arrange 49 | var input = getClass().getResourceAsStream("/no-vulnerabilities-component-report-response.json"); 50 | 51 | // Act 52 | var result = ComponentReportResponseBodyHandler.toComponentReportResponse(input); 53 | 54 | // Assert 55 | assertThat(result).isInstanceOf(Result.Success.class); 56 | var response = result.value(); 57 | assertThat(response.componentReports()).hasSize(1); 58 | assertThat(response.componentReports()[0].coordinates()) 59 | .isEqualTo("pkg:maven/org.codehaus.plexus/plexus-utils@3.4.1"); 60 | assertThat(response.componentReports()[0].reference()) 61 | .isEqualTo("https://ossindex.sonatype.org/component/pkg:maven/org.codehaus.plexus/plexus-utils@3.4.1"); 62 | assertThat(response.componentReports()[0].vulnerabilities()).isEmpty(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/it/mulders/mcs/search/vulnerability/ComponentReportVulnerabilitySeverityTest.java: -------------------------------------------------------------------------------- 1 | package it.mulders.mcs.search.vulnerability; 2 | 3 | import java.util.stream.Stream; 4 | import org.assertj.core.api.WithAssertions; 5 | import org.junit.jupiter.api.DisplayNameGeneration; 6 | import org.junit.jupiter.api.DisplayNameGenerator; 7 | import org.junit.jupiter.params.ParameterizedTest; 8 | import org.junit.jupiter.params.provider.Arguments; 9 | import org.junit.jupiter.params.provider.MethodSource; 10 | 11 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 12 | class ComponentReportVulnerabilitySeverityTest implements WithAssertions { 13 | 14 | private static Stream scores() { 15 | return Stream.of( 16 | Arguments.of(9.8, "Critical"), 17 | Arguments.of(9.0, "Critical"), 18 | Arguments.of(8.0, "High"), 19 | Arguments.of(7.0, "High"), 20 | Arguments.of(5.0, "Medium"), 21 | Arguments.of(4.0, "Medium"), 22 | Arguments.of(2.0, "Low"), 23 | Arguments.of(0.1, "Low"), 24 | Arguments.of(0.0, "None")); 25 | } 26 | 27 | @ParameterizedTest 28 | @MethodSource("scores") 29 | void should_return_correct_severity_for_score(double score, String expectedSeverity) { 30 | assertThat(ComponentReportVulnerabilitySeverity.getText(score)).isEqualTo(expectedSeverity); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/resources/group-artifact-search.json: -------------------------------------------------------------------------------- 1 | { 2 | "responseHeader": { 3 | "status": 0, 4 | "QTime": 1, 5 | "params": { 6 | "q": "g:org.codehaus.plexus AND a:plexus-utils", 7 | "core": "", 8 | "spellcheck": "true", 9 | "indent": "off", 10 | "fl": "id,g,a,latestVersion,p,ec,repositoryId,text,timestamp,versionCount", 11 | "start": "0", 12 | "sort": "score desc,timestamp desc,g asc,a asc", 13 | "spellcheck.count": "5", 14 | "rows": "20", 15 | "wt": "json", 16 | "version": "2.2" 17 | } 18 | }, 19 | "response": { 20 | "numFound": 1, 21 | "start": 0, 22 | "docs": [ 23 | { 24 | "id": "org.codehaus.plexus:plexus-utils", 25 | "g": "org.codehaus.plexus", 26 | "a": "plexus-utils", 27 | "latestVersion": "3.4.1", 28 | "repositoryId": "central", 29 | "p": "jar", 30 | "timestamp": 1630022910000, 31 | "versionCount": 73, 32 | "text": [ 33 | "org.codehaus.plexus", 34 | "plexus-utils", 35 | "-sources.jar", 36 | "-javadoc.jar", 37 | ".jar", 38 | "-source-release.zip", 39 | ".pom" 40 | ], 41 | "ec": [ 42 | "-sources.jar", 43 | "-javadoc.jar", 44 | ".jar", 45 | "-source-release.zip", 46 | ".pom" 47 | ] 48 | } 49 | ] 50 | }, 51 | "spellcheck": { 52 | "suggestions": [] 53 | } 54 | } -------------------------------------------------------------------------------- /src/test/resources/group-artifact-version-search.json: -------------------------------------------------------------------------------- 1 | { 2 | "responseHeader": { 3 | "status": 0, 4 | "QTime": 0, 5 | "params": { 6 | "q": "g:org.codehaus.plexus AND a:plexus-utils AND v:3.4.1", 7 | "core": "", 8 | "indent": "off", 9 | "fl": "id,g,a,v,p,ec,timestamp,tags", 10 | "start": "0", 11 | "sort": "score desc,timestamp desc,g asc,a asc,v desc", 12 | "rows": "20", 13 | "wt": "json", 14 | "version": "2.2" 15 | } 16 | }, 17 | "response": { 18 | "numFound": 1, 19 | "start": 0, 20 | "docs": [ 21 | { 22 | "id": "org.codehaus.plexus:plexus-utils:3.4.1", 23 | "g": "org.codehaus.plexus", 24 | "a": "plexus-utils", 25 | "v": "3.4.1", 26 | "p": "jar", 27 | "timestamp": 1630022910000, 28 | "ec": [ 29 | "-javadoc.jar", 30 | "-sources.jar", 31 | ".jar", 32 | ".pom", 33 | "-source-release.zip" 34 | ], 35 | "tags": [ 36 | "classes", 37 | "files", 38 | "with", 39 | "more", 40 | "utility", 41 | "command", 42 | "ease", 43 | "various", 44 | "strings", 45 | "lines", 46 | "collection", 47 | "working" 48 | ] 49 | } 50 | ] 51 | } 52 | } -------------------------------------------------------------------------------- /src/test/resources/mcs.properties: -------------------------------------------------------------------------------- 1 | mcs.version=Unknown -------------------------------------------------------------------------------- /src/test/resources/no-vulnerabilities-component-report-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "coordinates": "pkg:maven/org.codehaus.plexus/plexus-utils@3.4.1", 4 | "description": "A collection of various utility classes to ease working with strings, files, command lines, XML and more.", 5 | "reference": "https://ossindex.sonatype.org/component/pkg:maven/org.codehaus.plexus/plexus-utils@3.4.1", 6 | "vulnerabilities": [] 7 | } 8 | ] -------------------------------------------------------------------------------- /src/test/resources/sample-mcs.config: -------------------------------------------------------------------------------- 1 | example.a=foo 2 | example.b=${example.a} bar 3 | example.c=${example.a} ${example.a} baz 4 | user.home=whatever -------------------------------------------------------------------------------- /src/test/resources/vulnerabilities-component-report-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "coordinates": "pkg:maven/org.apache.shiro/shiro-web@1.9.0", 4 | "description": "", 5 | "reference": "https://ossindex.sonatype.org/component/pkg:maven/org.apache.shiro/shiro-web@1.9.0", 6 | "vulnerabilities": [ 7 | { 8 | "id": "CVE-2022-40664", 9 | "displayName": "CVE-2022-40664", 10 | "title": "[CVE-2022-40664] CWE-287: Improper Authentication", 11 | "description": "Apache Shiro before 1.10.0, Authentication Bypass Vulnerability in Shiro when forwarding or including via RequestDispatcher.", 12 | "cvssScore": 9.8, 13 | "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", 14 | "cwe": "CWE-287", 15 | "cve": "CVE-2022-40664", 16 | "reference": "https://ossindex.sonatype.org/vulnerability/CVE-2022-40664?component-type=maven&component-name=org.apache.shiro%2Fshiro-web", 17 | "externalReferences": [ 18 | "http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2022-40664", 19 | "https://shiro.apache.org/blog/2022/10/10/2022/apache-shiro-1101-released.html", 20 | "https://lists.apache.org/thread/loc2ktxng32xpy7lfwxto13k4lvnhjwg", 21 | "http://www.openwall.com/lists/oss-security/2022/10/12/1", 22 | "https://github.com/advisories/GHSA-45x9-q6vj-cqgq" 23 | ] 24 | }, 25 | { 26 | "id": "CVE-2023-34478", 27 | "displayName": "CVE-2023-34478", 28 | "title": "[CVE-2023-34478] CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')", 29 | "description": "Apache Shiro, before 1.12.0 or 2.0.0-alpha-3, may be susceptible to a path traversal attack that results in an authentication bypass when used together with APIs or other web frameworks that route requests based on non-normalized requests.\n\nMitigation: Update to Apache Shiro 1.12.0+ or 2.0.0-alpha-3+\n\n\nSonatype's research suggests that this CVE's details differ from those defined at NVD. See https://ossindex.sonatype.org/vulnerability/CVE-2023-34478 for details", 30 | "cvssScore": 9.8, 31 | "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", 32 | "cwe": "CWE-22", 33 | "cve": "CVE-2023-34478", 34 | "reference": "https://ossindex.sonatype.org/vulnerability/CVE-2023-34478?component-type=maven&component-name=org.apache.shiro%2Fshiro-web", 35 | "externalReferences": [ 36 | "http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2023-34478", 37 | "https://lists.apache.org/thread/jowcs5nd4tz5fxwl1mqkqnvyrwwx59qo" 38 | ] 39 | } 40 | ] 41 | } 42 | ] -------------------------------------------------------------------------------- /src/test/resources/wildcard-search-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "responseHeader": { 3 | "status": 0, 4 | "QTime": 1, 5 | "params": { 6 | "q": "plexus-utils", 7 | "core": "", 8 | "defType": "dismax", 9 | "indent": "off", 10 | "qf": "text^20 g^5 a^10", 11 | "spellcheck": "true", 12 | "fl": "id,g,a,latestVersion,p,ec,repositoryId,text,timestamp,versionCount", 13 | "start": "0", 14 | "sort": "score desc,timestamp desc,g asc,a asc", 15 | "spellcheck.count": "5", 16 | "rows": "20", 17 | "wt": "json", 18 | "version": "2.2" 19 | } 20 | }, 21 | "response": { 22 | "numFound": 2, 23 | "start": 0, 24 | "docs": [ 25 | { 26 | "id": "org.codehaus.plexus:plexus-utils", 27 | "g": "org.codehaus.plexus", 28 | "a": "plexus-utils", 29 | "latestVersion": "3.4.1", 30 | "repositoryId": "central", 31 | "p": "jar", 32 | "timestamp": 1630022910000, 33 | "versionCount": 73, 34 | "text": [ 35 | "org.codehaus.plexus", 36 | "plexus-utils", 37 | "-sources.jar", 38 | "-javadoc.jar", 39 | ".jar", 40 | "-source-release.zip", 41 | ".pom" 42 | ], 43 | "ec": [ 44 | "-sources.jar", 45 | "-javadoc.jar", 46 | ".jar", 47 | "-source-release.zip", 48 | ".pom" 49 | ] 50 | }, 51 | { 52 | "id": "plexus:plexus-utils", 53 | "g": "plexus", 54 | "a": "plexus-utils", 55 | "latestVersion": "1.0.3", 56 | "repositoryId": "central", 57 | "p": "jar", 58 | "timestamp": 1131487245000, 59 | "versionCount": 8, 60 | "text": [ 61 | "plexus", 62 | "plexus-utils", 63 | ".jar", 64 | ".pom" 65 | ], 66 | "ec": [ 67 | ".jar", 68 | ".pom" 69 | ] 70 | } 71 | ] 72 | }, 73 | "spellcheck": { 74 | "suggestions": [] 75 | } 76 | } --------------------------------------------------------------------------------