├── .gitattributes ├── .github ├── FUNDING.yml ├── contributing.md ├── dependabot.yml └── workflows │ ├── gradle.yml │ ├── vulnz-docker-pr.yml │ └── vulnz-docker-release.yml ├── .gitignore ├── .java-version ├── LICENSE.txt ├── README.md ├── buildSrc ├── build.gradle └── src │ └── main │ ├── config │ ├── java.license │ ├── spotbugs.exclude.xml │ └── spotless.eclipseformat.xml │ └── groovy │ ├── vuln.tools.java-application-conventions.gradle │ ├── vuln.tools.java-common-conventions.gradle │ └── vuln.tools.java-library-conventions.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── vulnz ├── Dockerfile ├── README.md ├── build.gradle └── src ├── docker ├── apache │ ├── htdocs │ │ ├── epss.shtml │ │ ├── index.html │ │ ├── kev.shtml │ │ └── nvd.shtml │ └── mirror.conf ├── crontab │ └── mirror ├── scripts │ ├── epss.sh │ ├── kev.sh │ ├── mirror-all.sh │ ├── mirror.sh │ └── validate.sh └── supervisor │ └── supervisord.conf ├── main ├── java │ └── io │ │ └── github │ │ └── jeremylong │ │ └── vulnz │ │ └── cli │ │ ├── Application.java │ │ ├── cache │ │ ├── CacheException.java │ │ ├── CacheProperties.java │ │ └── CacheUpdateException.java │ │ ├── commands │ │ ├── AbstractHelpfulCommand.java │ │ ├── AbstractJsonCommand.java │ │ ├── AbstractNvdCommand.java │ │ ├── CveCommand.java │ │ ├── GHSACommand.java │ │ ├── InstallCommand.java │ │ ├── MainCommand.java │ │ └── TimedCommand.java │ │ ├── model │ │ └── BasicOutput.java │ │ ├── monitoring │ │ ├── CveCounterPerYear.java │ │ └── PrometheusFileWriter.java │ │ ├── services │ │ ├── NvdMirrorService.java │ │ └── NvdService.java │ │ ├── ui │ │ ├── IProgressMonitor.java │ │ ├── JLineAppender.java │ │ ├── JlineShutdownHook.java │ │ └── ProgressMonitor.java │ │ └── util │ │ └── HexUtil.java └── resources │ ├── application.properties │ ├── banner.txt │ └── logback-spring.xml └── test └── java └── io └── github └── jeremylong └── vulnz └── cli ├── NvdApplicationTests.java └── ui └── ProgressMonitorTest.java /.gitattributes: -------------------------------------------------------------------------------- 1 | # Declare files that will always have LF line endings on checkout. 2 | *.sh text eol=lf 3 | *.conf text eol=lf -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: jeremylong 4 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to vuln-tools 2 | 3 | Pretty much everything is on github. Use issues to report issues and enhancement requests. 4 | If contributing code via a PR please use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gradle" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Gradle 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up JDK 17 18 | uses: actions/setup-java@v4 19 | with: 20 | java-version: '17' 21 | distribution: 'temurin' 22 | - name: Run build 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | run: | 26 | ./gradlew build --info 27 | - name: Archive test reports 28 | id: archive-logs 29 | if: always() 30 | uses: actions/upload-artifact@v4 31 | with: 32 | name: test-reports 33 | retention-days: 7 34 | path: | 35 | vulnz/build/reports/tests 36 | -------------------------------------------------------------------------------- /.github/workflows/vulnz-docker-pr.yml: -------------------------------------------------------------------------------- 1 | name: docker pr 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | 7 | env: 8 | IMAGE_FQDN: ghcr.io/jeremylong/open-vulnerability-data-mirror 9 | VERSION: 0.0.0-SNAPSHOT 10 | 11 | jobs: 12 | docker-pr: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - name: Set up QEMU 18 | uses: docker/setup-qemu-action@v3 19 | - name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v3 21 | - name: Set up JDK 17 22 | uses: actions/setup-java@v4 23 | with: 24 | java-version: '17' 25 | distribution: 'temurin' 26 | - name: Run build 27 | run: ./gradlew -x test -Pversion=${{ env.VERSION }} vulnz:build 28 | - name: Build docker image 29 | uses: docker/build-push-action@v6 30 | with: 31 | context: vulnz/ 32 | platforms: linux/amd64 33 | push: false 34 | tags: | 35 | ${{ env.IMAGE_FQDN }}:${{ env.VERSION }} 36 | build-args: | 37 | BUILD_VERSION=${{ env.VERSION }} 38 | -------------------------------------------------------------------------------- /.github/workflows/vulnz-docker-release.yml: -------------------------------------------------------------------------------- 1 | name: docker pr 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | env: 9 | GHCR_IMAGE_FQDN: ghcr.io/jeremylong/open-vulnerability-data-mirror 10 | HUB_IMAGE_FQDN: jeremylong/open-vulnerability-data-mirror 11 | VERSION: ${{ github.ref_name }} 12 | 13 | jobs: 14 | docker-release: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | - name: Set up QEMU 20 | uses: docker/setup-qemu-action@v3 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v3 23 | - name: Login to GHCR 24 | uses: docker/login-action@v3 25 | with: 26 | registry: ghcr.io 27 | username: ${{ github.repository_owner }} 28 | password: ${{ github.token }} 29 | - name: Login to Docker Hub 30 | uses: docker/login-action@v3 31 | with: 32 | username: ${{ secrets.DOCKERHUB_USERNAME }} 33 | password: ${{ secrets.DOCKERHUB_TOKEN }} 34 | - name: Set up JDK 17 35 | uses: actions/setup-java@v4 36 | with: 37 | java-version: '17' 38 | distribution: 'temurin' 39 | - name: Run build 40 | run: ./gradlew -x test -Pversion=${{ env.VERSION }} vulnz:build 41 | - name: Build docker image 42 | uses: docker/build-push-action@v6 43 | with: 44 | context: vulnz/ 45 | platforms: linux/amd64,linux/arm64 46 | push: true 47 | tags: | 48 | ${{ env.GHCR_IMAGE_FQDN }}:${{ env.VERSION }} 49 | ${{ env.GHCR_IMAGE_FQDN }}:latest 50 | ${{ env.HUB_IMAGE_FQDN }}:${{ env.VERSION }} 51 | ${{ env.HUB_IMAGE_FQDN }}:latest 52 | build-args: | 53 | BUILD_VERSION=${{ env.VERSION }} 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | **/build/ 3 | !src/**/build/ 4 | 5 | # Ignore Gradle GUI config 6 | gradle-app.setting 7 | 8 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 9 | !gradle-wrapper.jar 10 | 11 | # Avoid ignore Gradle wrappper properties 12 | !gradle-wrapper.properties 13 | 14 | # Cache of project 15 | .gradletasknamecache 16 | 17 | .DS_Store 18 | .vscode 19 | # Intellij project files 20 | *.iml 21 | *.ipr 22 | *.iws 23 | .idea/ 24 | # Eclipse project files 25 | .classpath 26 | .project 27 | .settings 28 | maven-eclipse.xml 29 | .externalToolBuilders 30 | # Netbeans configuration 31 | nb-configuration.xml 32 | **/nbproject/ 33 | local.properties 34 | data-source/data/ 35 | -------------------------------------------------------------------------------- /.java-version: -------------------------------------------------------------------------------- 1 | 17.0 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # open-vulnerability-cli 2 | 3 | The open-vulnerability-cli is a command line utility that can be used to 4 | query various online vulnerability sources such as the NVD or GHSA. The 5 | CLI and docker images can be used to mirror the NVD (instructions below). 6 | 7 | Note that the CLI is called `vulnz` because open-vulnerability-cli is cumbersome. 8 | `vulnz` is a spring-boot command line utility built with picocli. 9 | 10 | ## Setup 11 | 12 | As of the 8.0.0 release, Java 17 is required; alternatively, you can use the 13 | docker image. The `vulnz` CLI can be downloaded from the releases page. 14 | 15 | The example below does run the setup - which creates both the `vulnz` symlink 16 | (in `/usr/local/bin`) and a completion script. If using zsh, the completion 17 | will be added to `/etc/bash_completion.d` or `/usr/local/etc/bash_completion.d` 18 | (depending on if they exist); see [permanently installing completion](https://picocli.info/autocomplete.html#_installing_completion_scripts_permanently_in_bashzsh) 19 | for more details. 20 | 21 | After running `install` you may need to restart your shell for the completion to work. 22 | 23 | ```bash 24 | ./gradlew vulnz:build 25 | cd vulnz/build/libs 26 | ./vulnz-9.0.0.jar install 27 | vulnz cve --cveId CVE-2021-44228 --prettyPrint 28 | ``` 29 | 30 | Example of using the CLI with an API key stored in [1password](https://1password.com/) using 31 | the `op` CLI (see [getting started with op](https://developer.1password.com/docs/cli/get-started/)): 32 | 33 | ```bash 34 | export NVD_API_KEY=op://vaultname/nvd-api/credential 35 | eval $(op signin) 36 | op run -- vulnz cve --requestCount 40 > cve-complete.json 37 | ``` 38 | 39 | ## Mirroring the NVD CVE Data 40 | 41 | The vulnz cli can create a cache of the NVD CVE data obtained from the API. The 42 | data is stored in `json` files with the data saved in the traditional yearly groupings 43 | starting with 2002 and going to the current year. In addition, a `cache.properties` is 44 | created that contains the `lastModifiedDate` datetime as well as the prefix used for the 45 | generated JSON files (by default `nvdcve-` is used). Additionally, a `modified` JSON file 46 | is created that will hold the CVEs that have been modified in the last 8 days. After running 47 | the below command you will end up with a directory with: 48 | 49 | - `cache.properties` 50 | - `nvdcve-modified.json.gz` 51 | - `nvdcve-modified.meta` 52 | - `nvdcve-2002.json.gz` 53 | - `nvdcve-2002.meta` 54 | - `nvdcve-2003.json.gz` 55 | - `nvdcve-2003.meta` 56 | - ... 57 | - `nvdcve-2025.json.gz` 58 | - `nvdcve-2025.meta` 59 | 60 | ### API Key is used and a 403 or 404 error occurs 61 | 62 | If an API Key is used and you receive a 404 error: 63 | 64 | ``` 65 | ERROR 66 | io.github.jeremylong.openvulnerability.client.nvd.NvdApiException: NVD Returned Status Code: 404 67 | ``` 68 | 69 | There is a good chance that the API Key is set incorrectly or is invalid. To check if the API Key works 70 | the following `curl` command should return JSON: 71 | 72 | ``` 73 | curl -H "Accept: application/json" -H "apiKey: ########-####-####-####-############" -v https://services.nvd.nist.gov/rest/json/cves/2.0\?cpeName\=cpe:2.3:o:microsoft:windows_10:1607:\*:\*:\*:\*:\*:\*:\* 74 | ``` 75 | 76 | If no JSON is returned and you see a 404 error the API Key is invalid and you should request a new one. 77 | 78 | ### Out-of-Memory Errors 79 | 80 | Create the local cache may result in an out-of-memory error. To resolve the 81 | error simply increase the available memory for Java: 82 | 83 | ```bash 84 | export JAVA_OPTS="-Xmx2g" 85 | ``` 86 | 87 | Alternatively, run the CLI using the `-Xmx2g` argument: 88 | 89 | ```bash 90 | java -Xmx2g -jar ./vulnz-9.0.0.jar 91 | ``` 92 | 93 | An option to save memory would be: `-XX:+UseStringDeduplication`: 94 | ```bash 95 | export JAVA_OPTS="-Xmx2g -XX:+UseStringDeduplication" 96 | ``` 97 | 98 | ### Creating the Mirror 99 | 100 | To create a local mirror of the NVD CVE Data you can execute the following command 101 | via a daily schedule to keep the cached data current: 102 | 103 | ```bash 104 | vulnz cve --cache --directory ./cache 105 | ``` 106 | 107 | Alternatively, without using the above install command: 108 | 109 | ```bash 110 | ./vulnz-9.0.0.jar cve --cache --directory ./cache 111 | ``` 112 | 113 | When creating the cache all other arguments to the vulnz cli 114 | will still work except the `--lastModEndDate` and `--lastModStartDate`. 115 | As such, you can create `--prettyPrint` the cache or create a cache 116 | of only "application" CVE using the `--virtualMatchString=cpe:2.3:a`. 117 | 118 | ## Docker image 119 | 120 | ### Configuration 121 | 122 | There are a couple of ENV vars 123 | 124 | - `NVD_API_KEY`: define your API key 125 | - `DELAY`: override the delay - given in milliseconds. If you do not set an API KEY, the delay will be `10000` 126 | - `MAX_RETRY_ARG` Using max retry attempts 127 | - `MAX_RECORDS_PER_PAGE_ARG` Using max records per page 128 | - `METRICS_ENABLE` If is set to `true`, OpenMetrics data for the vulnz cli can be retrieved via the endpoint http://.../metrics 129 | - `METRICS_WRITE_INTERVAL` Sets the update interval for generating metrics, in milliseconds. Default: `5000` 130 | - `METRICS_WRITER_FORMAT` Sets the output format for the metrics. Either `openmetrics` or `prometheus` format. Default: `openmetrics` 131 | - `CACERT` Path to a custom Certificate Authority (CA) certificate file that should be used for secure SSL/TLS connections with curl. Example: `/cacert.pem` 132 | 133 | 134 | ### Run 135 | 136 | ```bash 137 | # replace the NVD_API_KEY with your NVD api key 138 | docker run --name vulnz -e NVD_API_KEY=myapikey jeremylong/open-vulnerability-data-mirror:v9.0.0 139 | 140 | # if you like use a volume 141 | docker run --name vulnz -e NVD_API_KEY=myapikey -v cache:/usr/local/apache2/htdocs jeremylong/open-vulnerability-data-mirror:v9.0.0 142 | 143 | # adjust the memory usage 144 | docker run --name vulnz -e JAVA_OPT=-Xmx2g jeremylong/open-vulnerability-data-mirror:v9.0.0 145 | 146 | # you can also adjust the delay 147 | docker run --name vulnz -e NVD_API_KEY=myapikey -e DELAY=3000 jeremylong/open-vulnerability-data-mirror:v9.0.0 148 | 149 | # mounts the custom Java `cacerts` file from your local machine into the container for secure SSL/TLS connections with java 150 | # and mounts the custom `cafile` from your local machine into the container for secure SSL/TLS connections with curl 151 | docker run --name vulnz -v /path/to/java/cacerts:/etc/ssl/certs/java/cacerts -v /path/to/cacert.pem:/cacert.pem jeremylong/open-vulnerability-data-mirror:v9.0.0 152 | ``` 153 | 154 | If you like, run this to pre-populate the mirror right away 155 | 156 | ```bash 157 | docker exec -u mirror vulnz /mirror.sh 158 | ``` 159 | 160 | ### Build 161 | 162 | Assuming the current version is `9.0.0` 163 | 164 | ```bash 165 | export TARGET_VERSION=9.0.0 166 | ./gradlew vulnz:build -Pversion=$TARGET_VERSION 167 | docker build vulnz/ -t ghcr.io/jeremylong/vulnz:$TARGET_VERSION --build-arg BUILD_VERSION=$TARGET_VERSION 168 | ``` 169 | 170 | ### Release 171 | 172 | ```bash 173 | # checkout the repo 174 | git tag -a 'v9.0.0'' -m 'release 9.0.0' 175 | git push --tags 176 | # this will build vulnz 9.0.0 on publish the docker image tagged 9.0.0 177 | ``` 178 | -------------------------------------------------------------------------------- /buildSrc/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | */ 4 | 5 | plugins { 6 | // Support convention plugins written in Groovy. Convention plugins are build scripts in 'src/main' that automatically become available as plugins in the main build. 7 | id 'groovy-gradle-plugin' 8 | } 9 | 10 | repositories { 11 | // Use the plugin portal to apply community plugins in convention plugins. 12 | gradlePluginPortal() 13 | mavenCentral() 14 | } 15 | 16 | dependencies { 17 | implementation 'com.diffplug.spotless:spotless-plugin-gradle:7.0.3' 18 | implementation 'com.github.spotbugs.snom:spotbugs-gradle-plugin:6.0.18' 19 | } -------------------------------------------------------------------------------- /buildSrc/src/main/config/java.license: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) $today.year Jeremy Long. All Rights Reserved. 16 | */ -------------------------------------------------------------------------------- /buildSrc/src/main/config/spotbugs.exclude.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /buildSrc/src/main/config/spotless.eclipseformat.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 9 | 12 | 15 | 18 | 20 | 23 | 25 | 28 | 31 | 32 | 35 | 38 | 41 | 44 | 46 | 47 | 49 | 52 | 55 | 58 | 61 | 63 | 65 | 66 | 68 | 70 | 71 | 73 | 76 | 79 | 82 | 84 | 86 | 87 | 90 | 93 | 96 | 99 | 101 | 104 | 107 | 109 | 111 | 113 | 116 | 119 | 122 | 125 | 128 | 131 | 134 | 137 | 140 | 141 | 144 | 146 | 148 | 151 | 154 | 157 | 160 | 163 | 164 | 166 | 167 | 170 | 173 | 176 | 179 | 182 | 185 | 188 | 191 | 193 | 196 | 199 | 201 | 204 | 207 | 209 | 211 | 214 | 217 | 220 | 223 | 226 | 229 | 232 | 234 | 237 | 239 | 242 | 244 | 245 | 248 | 251 | 254 | 256 | 259 | 262 | 265 | 268 | 270 | 273 | 276 | 279 | 282 | 285 | 288 | 290 | 292 | 295 | 298 | 300 | 302 | 304 | 307 | 309 | 312 | 315 | 318 | 321 | 324 | 326 | 329 | 331 | 332 | 334 | 337 | 340 | 343 | 346 | 349 | 351 | 354 | 357 | 359 | 362 | 365 | 368 | 370 | 372 | 375 | 376 | 379 | 382 | 385 | 387 | 390 | 392 | 393 | 396 | 399 | 402 | 405 | 408 | 411 | 413 | 415 | 418 | 421 | 424 | 425 | 428 | 431 | 432 | 435 | 438 | 441 | 444 | 446 | 449 | 452 | 455 | 456 | 458 | 461 | 464 | 465 | 468 | 471 | 474 | 477 | 478 | 480 | 483 | 486 | 488 | 491 | 494 | 495 | 498 | 501 | 502 | 503 | 506 | 508 | 511 | 513 | 516 | 519 | 522 | 525 | 528 | 531 | 533 | 535 | 538 | 541 | 544 | 547 | 550 | 553 | 556 | 559 | 561 | 563 | 566 | 569 | 572 | 575 | 578 | 581 | 583 | 586 | 589 | 591 | 593 | 596 | 599 | 602 | 604 | 606 | 609 | 611 | 614 | 617 | 620 | 623 | 625 | 628 | 631 | 633 | 635 | 638 | 640 | 642 | 644 | 647 | 650 | 653 | 654 | 657 | 660 | 663 | 666 | 668 | 671 | 674 | 677 | 680 | 682 | 685 | 687 | 689 | 691 | 694 | 697 | 700 | 703 | 706 | 709 | 712 | 715 | 718 | 721 | 723 | 726 | 729 | 730 | 733 | 736 | 738 | 741 | 742 | 745 | 747 | 748 | 751 | 754 | 755 | 756 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/vuln.tools.java-application-conventions.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | */ 4 | 5 | plugins { 6 | // Apply the common convention plugin for shared build configuration between library and application projects. 7 | id 'vuln.tools.java-common-conventions' 8 | 9 | // Apply the application plugin to add support for building a CLI application in Java. 10 | id 'application' 11 | id 'java-library' 12 | } 13 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/vuln.tools.java-common-conventions.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | */ 4 | 5 | plugins { 6 | id 'java' 7 | id 'maven-publish' 8 | id 'signing' 9 | id 'com.diffplug.spotless' 10 | id 'com.github.spotbugs' 11 | } 12 | 13 | group 'io.github.jeremylong' 14 | 15 | repositories { 16 | mavenCentral() 17 | gradlePluginPortal() 18 | } 19 | 20 | dependencies { 21 | 22 | compileOnly 'com.github.spotbugs:spotbugs-annotations:4.8.6' 23 | 24 | testImplementation platform('org.junit:junit-bom:5.10.3') 25 | testImplementation 'org.junit.jupiter:junit-jupiter' 26 | testRuntimeOnly 'org.junit.platform:junit-platform-launcher' 27 | } 28 | 29 | tasks.named('test') { 30 | // Use JUnit Platform for unit tests. 31 | useJUnitPlatform() 32 | } 33 | 34 | spotless { 35 | java { 36 | targetExclude(fileTree("$buildDir/generated") { include("**/*.java") }) 37 | eclipse().configFile("$rootDir/buildSrc/src/main/config/spotless.eclipseformat.xml") 38 | licenseHeaderFile("$rootDir/buildSrc/src/main/config/java.license").updateYearWithLatest(true) 39 | } 40 | } 41 | 42 | spotbugs { 43 | excludeFilter.set(file("$rootDir/buildSrc/src/main/config/spotbugs.exclude.xml")) 44 | reportsDir = file("$buildDir/spotbugs") 45 | } 46 | compileJava { 47 | sourceCompatibility = JavaVersion.VERSION_17 48 | targetCompatibility = JavaVersion.VERSION_17 49 | options.encoding = 'UTF-8' 50 | } 51 | tasks.withType(AbstractArchiveTask).configureEach { 52 | preserveFileTimestamps = false 53 | reproducibleFileOrder = true 54 | } 55 | javadoc { 56 | failOnError = false 57 | options.encoding("UTF-8") 58 | if (JavaVersion.current().isJava9Compatible()) { 59 | options.addBooleanOption('html5', true) 60 | } 61 | } 62 | java { 63 | withSourcesJar() 64 | withJavadocJar() 65 | } 66 | signing { 67 | sign publishing.publications 68 | } 69 | sourcesJar.dependsOn(compileJava) 70 | sourcesJar.dependsOn(compileTestJava) 71 | javadoc.dependsOn(compileTestJava) 72 | 73 | publishing { 74 | repositories { 75 | maven { 76 | url "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2" 77 | credentials { 78 | username project.findProperty('sonatypeUsername_s01') ?: System.getenv("SONATYPE_USER") 79 | password project.findProperty('sonatypePassword_s01') ?: System.getenv("SONATYPE_PASSWORD") 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/vuln.tools.java-library-conventions.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | */ 4 | 5 | plugins { 6 | // Apply the common convention plugin for shared build configuration between library and application projects. 7 | id 'vuln.tools.java-common-conventions' 8 | 9 | // Apply the java-library plugin for API and implementation separation. 10 | id 'java-library' 11 | } 12 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | version = 9.0.0 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremylong/open-vulnerability-cli/c785f348a577459bde5311d24d4f778da9aeb49a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=a4b4158601f8636cdeeab09bd76afb640030bb5b144aafe261a5e8af027dc612 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | } 6 | } 7 | 8 | rootProject.name = 'open-vulnerability-cli' 9 | include('vulnz') 10 | 11 | 12 | -------------------------------------------------------------------------------- /vulnz/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM httpd:alpine 2 | 3 | ARG BUILD_DATE 4 | ARG BUILD_VERSION 5 | 6 | ARG http_proxy 7 | ARG https_proxy 8 | ARG no_proxy 9 | 10 | LABEL authors="derhecht,stevespringett,jeremylong,eugenmayer" 11 | LABEL maintainer="jeremy.long@gmail.com" 12 | LABEL name="jeremylong/vulnz" 13 | LABEL version=$BUILD_VERSION 14 | LABEL org.label-schema.schema-version="1.0" 15 | LABEL org.label-schema.build-date=$BUILD_DATE 16 | LABEL org.label-schema.name="jeremylong/vulnz" 17 | LABEL org.label-schema.description="Persist the data using the open-vulnerability-store." 18 | LABEL org.label-schema.url="https://github.com/jeremylong/Open-Vulnerability-Project" 19 | LABEL org.label-schema.vcs-url="https://github.com/jeremylong/Open-Vulnerability-Project" 20 | LABEL org.label-schema.vendor="jeremylong" 21 | LABEL org.label-schema.version=$BUILD_VERSION 22 | LABEL org.label-schema.docker.cmd="docker run -it --rm --name mirror -e NVD_API_KEY=YOUR_API_KEY_HERE -p 80:80 jeremylong/vulnz" 23 | 24 | ENV user=mirror 25 | ENV BUILD_VERSION=$BUILD_VERSION 26 | ENV JAVA_OPT="-XX:InitialRAMPercentage=50.0" 27 | 28 | RUN apk update && \ 29 | apk add --no-cache bash openjdk17 dcron nss supervisor tzdata curl && \ 30 | addgroup -S "$user" && \ 31 | adduser -S "$user" -G "$user" && \ 32 | addgroup "$user" www-data && \ 33 | addgroup www-data "$user" && \ 34 | chown -R "$user":"$user" /usr/local/apache2/htdocs && \ 35 | echo "Include conf/mirror.conf" >> /usr/local/apache2/conf/httpd.conf && \ 36 | mkdir -p /var/log/supervisor && \ 37 | rm -v /usr/local/apache2/htdocs/index.html 38 | 39 | COPY ["/src/docker/supervisor/supervisord.conf", "/etc/supervisord.conf"] 40 | COPY ["/src/docker/scripts/mirror.sh", "/mirror.sh"] 41 | COPY ["/src/docker/scripts/epss.sh", "/epss.sh"] 42 | COPY ["/src/docker/scripts/kev.sh", "/kev.sh"] 43 | COPY ["/src/docker/scripts/mirror-all.sh", "/mirror-all.sh"] 44 | COPY ["/src/docker/scripts/validate.sh", "/validate.sh"] 45 | COPY ["/src/docker/crontab/mirror", "/etc/crontabs/mirror"] 46 | COPY ["/src/docker/apache/mirror.conf", "/usr/local/apache2/conf"] 47 | COPY ["/src/docker/apache/htdocs/", "/usr/local/apache2/htdocs/"] 48 | COPY ["/build/libs/vulnz-$BUILD_VERSION.jar", "/usr/local/bin/vulnz"] 49 | 50 | RUN echo "Version: $BUILD_VERSION" > /usr/local/apache2/htdocs/version.shtml && \ 51 | chmod +x /mirror.sh /validate.sh /epss.sh /kev.sh /mirror-all.sh /usr/local/apache2/htdocs/index.html && \ 52 | chown root:root /etc/crontabs/mirror && \ 53 | chown -R mirror:mirror /usr/local/apache2/htdocs && \ 54 | chown mirror:mirror /usr/local/bin/vulnz 55 | 56 | # ensures we can log cron task is into stdout of docker 57 | RUN ln -sf /proc/1/fd/1 /var/log/docker_out.log 58 | 59 | VOLUME /usr/local/apache2/htdocs 60 | WORKDIR /usr/local/apache2/htdocs 61 | EXPOSE 80/tcp 62 | 63 | CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"] 64 | -------------------------------------------------------------------------------- /vulnz/README.md: -------------------------------------------------------------------------------- 1 | # open-vulnerability-cli 2 | 3 | The information you are looking for has moved to the [README.md](../README.md) in 4 | the root of the project. -------------------------------------------------------------------------------- /vulnz/build.gradle: -------------------------------------------------------------------------------- 1 | import org.apache.tools.ant.filters.ReplaceTokens 2 | 3 | plugins { 4 | id 'vuln.tools.java-application-conventions' 5 | id 'org.springframework.boot' version '3.4.2' 6 | id 'io.spring.dependency-management' version '1.1.7' 7 | } 8 | 9 | description = 'A java CLI to call the NVD API' 10 | 11 | ext['httpclient5.version'] = '5.4.2' 12 | ext['httpcore5.version'] = '5.3.3' 13 | ext['httpcore5-h2.version'] = '5.3.3' 14 | ext['snakeyaml.version'] = '2.4' 15 | ext['jackson.version'] = '2.17.1' 16 | ext['commons-lang3.version'] = '3.17.0' 17 | 18 | dependencies { 19 | implementation 'io.github.jeremylong:open-vulnerability-clients:8.0.0' 20 | implementation 'info.picocli:picocli-spring-boot-starter:4.7.7' 21 | constraints { 22 | implementation 'org.springframework.boot:spring-boot-starter:3.4.2' 23 | } 24 | implementation 'com.diogonunes:JColor:5.5.1' 25 | implementation 'org.jline:jline:3.30.4' 26 | implementation 'commons-io:commons-io:2.19.0' 27 | implementation 'com.fasterxml.jackson.core:jackson-databind' 28 | implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' 29 | implementation 'jakarta.persistence:jakarta.persistence-api:3.2.0' 30 | implementation 'jakarta.transaction:jakarta.transaction-api:2.0.1' 31 | implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.2' 32 | implementation 'com.sun.activation:jakarta.activation:2.0.1' 33 | implementation 'io.prometheus:prometheus-metrics-core:1.3.7' 34 | implementation 'io.prometheus:prometheus-metrics-exposition-formats:1.3.7' 35 | implementation 'io.prometheus:prometheus-metrics-instrumentation-jvm:1.3.8' 36 | implementation 'it.unimi.dsi:fastutil:8.5.15' 37 | 38 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 39 | } 40 | repositories { 41 | mavenLocal() 42 | mavenCentral() 43 | } 44 | 45 | jar { 46 | enabled = false 47 | } 48 | bootJar { 49 | launchScript() 50 | } 51 | 52 | application { 53 | mainClass = 'io.github.jeremylong.vulnz.cli.Application' 54 | } 55 | 56 | task generateAutocomplete(type: JavaExec) { 57 | mainClass = 'picocli.AutoComplete' 58 | classpath = sourceSets.main.runtimeClasspath 59 | args = ['--force', '--name', 'vulnz', '--completionScript', "${buildDir}/resources/main/vulnz.completion.sh", 'io.github.jeremylong.vulnz.cli.commands.MainCommand'] 60 | } 61 | tasks.named("classes") { finalizedBy("generateAutocomplete") } 62 | 63 | processResources { 64 | filter ReplaceTokens, tokens: [version: project.version] 65 | } 66 | 67 | publishing { 68 | publications { 69 | maven(MavenPublication) { 70 | artifact tasks.named("bootJar") 71 | from components.java 72 | pom { 73 | url = 'https://github.com/jeremylong/vuln-tools/' 74 | description = project.description 75 | name = project.name 76 | licenses { 77 | license { 78 | name = 'The Apache License, Version 2.0' 79 | url = 'https://github.com/jeremylong/vuln-tools/blob/main/LICENSE.txt' 80 | } 81 | } 82 | developers { 83 | developer { 84 | id = 'jeremy.long' 85 | name = 'Jeremy Long' 86 | } 87 | } 88 | scm { 89 | url = 'https://github.com/jeremylong/vuln-tools' 90 | connection = 'scm:git:https://github.com/jeremylong/vuln-tools.git' 91 | developerConnection = 'scm:git:https://github.com/jeremylong/vuln-tools.git' 92 | } 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /vulnz/src/docker/apache/htdocs/epss.shtml: -------------------------------------------------------------------------------- 1 |
  • Mirror has not completed yet
  • -------------------------------------------------------------------------------- /vulnz/src/docker/apache/htdocs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Open Vulnerability Cache 7 | 96 | 97 | 98 |
    99 | 114 | 115 |
    116 | 117 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 |

    Open Vulnerability Mirror

    130 |
    131 |
    132 | 135 | 141 | 142 | 148 | 149 | 155 | 156 |
    157 | 158 | 159 | -------------------------------------------------------------------------------- /vulnz/src/docker/apache/htdocs/kev.shtml: -------------------------------------------------------------------------------- 1 |
  • KEV mirror has not completed yet
  • -------------------------------------------------------------------------------- /vulnz/src/docker/apache/htdocs/nvd.shtml: -------------------------------------------------------------------------------- 1 |
  • Mirror has not completed yet
  • -------------------------------------------------------------------------------- /vulnz/src/docker/apache/mirror.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration for the httpd mirror 3 | # 4 | LoadModule include_module modules/mod_include.so 5 | 6 | ServerName localhost 7 | 8 | Options +Includes 9 | XBitHack on 10 | AddType text/html .shtml .html 11 | AddOutputFilter INCLUDES .shtml .html 12 | AddHandler server-parsed .shtml .html 13 | -------------------------------------------------------------------------------- /vulnz/src/docker/crontab/mirror: -------------------------------------------------------------------------------- 1 | 0 0 * * * /epss.sh 2>&1 | tee -a /var/log/docker_out.log | tee -a /var/log/cron_epss.log 2 | 0 0 * * * /kev.sh 2>&1 | tee -a /var/log/docker_out.log | tee -a /var/log/cron_kev.log 3 | 0 0 * * * /mirror.sh 2>&1 | tee -a /var/log/docker_out.log | tee -a /var/log/cron_mirror.log 4 | 0 2 * * * /validate.sh 2>&1 | tee -a /var/log/docker_out.log | tee -a /var/log/cron_validate.log -------------------------------------------------------------------------------- /vulnz/src/docker/scripts/epss.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "Mirroring EPSS..." 6 | 7 | LOCKFILE=/tmp/epss.lock 8 | 9 | if [ -f $LOCKFILE ]; then 10 | echo "Lockfile found - another mirror-sync process already running" 11 | else 12 | touch $LOCKFILE 13 | fi 14 | 15 | function remove_lockfile() { 16 | rm -f $LOCKFILE 17 | exit 0 18 | } 19 | trap remove_lockfile SIGHUP SIGINT SIGQUIT SIGABRT SIGALRM SIGTERM SIGTSTP 20 | 21 | CACERT_ARG="" 22 | if [ -n "${CACERT}" ]; then 23 | echo "Using cacert file: $CACERT" 24 | if [ -f "$CACERT" ]; then 25 | echo "File $CACERT exists." 26 | CACERT_ARG="--cacert $CACERT" 27 | else 28 | echo "File $CACERT not found." 29 | fi 30 | fi 31 | 32 | curl -L -sS $CACERT_ARG -o /usr/local/apache2/htdocs/epss_scores-current.csv.gz https://epss.cyentia.com/epss_scores-current.csv.gz 33 | 34 | if [ -f /usr/local/apache2/htdocs/epss_scores-current.csv.gz ]; then 35 | timestamp=$(stat -c "%Y" "/usr/local/apache2/htdocs/epss_scores-current.csv.gz" | xargs -I{} date -d @{} "+%Y-%m-%d %H:%M:%S") 36 | echo "
  • epss_scores-current.csv.gz (Last Modified: $timestamp)
  • " > /usr/local/apache2/htdocs/epss.shtml 37 | fi 38 | 39 | rm -f $LOCKFILE -------------------------------------------------------------------------------- /vulnz/src/docker/scripts/kev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "Mirroring Known Exploited Vulnerabilities..." 6 | 7 | LOCKFILE=/tmp/kev.lock 8 | 9 | if [ -f $LOCKFILE ]; then 10 | echo "Lockfile found - another mirror-sync process already running" 11 | else 12 | touch $LOCKFILE 13 | fi 14 | 15 | function remove_lockfile() { 16 | rm -f $LOCKFILE 17 | exit 0 18 | } 19 | trap remove_lockfile SIGHUP SIGINT SIGQUIT SIGABRT SIGALRM SIGTERM SIGTSTP 20 | 21 | CACERT_ARG="" 22 | if [ -n "${CACERT}" ]; then 23 | echo "Using cacert file: $CACERT" 24 | if [ -f "$CACERT" ]; then 25 | echo "File $CACERT exists." 26 | CACERT_ARG="--cacert $CACERT" 27 | else 28 | echo "File $CACERT not found." 29 | fi 30 | fi 31 | 32 | curl -L -sS $CACERT_ARG -o /usr/local/apache2/htdocs/known_exploited_vulnerabilities.json https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json 33 | 34 | if [ -f /usr/local/apache2/htdocs/known_exploited_vulnerabilities.json ]; then 35 | timestamp=$(stat -c "%Y" "/usr/local/apache2/htdocs/known_exploited_vulnerabilities.json" | xargs -I{} date -d @{} "+%Y-%m-%d %H:%M:%S") 36 | echo "
  • known_exploited_vulnerabilities.json (Last Modified: $timestamp)
  • " > /usr/local/apache2/htdocs/kev.shtml 37 | fi 38 | 39 | rm -f $LOCKFILE -------------------------------------------------------------------------------- /vulnz/src/docker/scripts/mirror-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "****debugging****" 4 | apachectl -M | grep include 5 | 6 | /epss.sh 7 | /kev.sh 8 | /mirror.sh -------------------------------------------------------------------------------- /vulnz/src/docker/scripts/mirror.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "Updating..." 6 | 7 | LOCKFILE=/tmp/vulzn.lock 8 | 9 | if [ -f $LOCKFILE ]; then 10 | echo "Lockfile found - another mirror-sync process already running" 11 | else 12 | touch $LOCKFILE 13 | fi 14 | 15 | DELAY_ARG="" 16 | if [ -z $NVD_API_KEY ]; then 17 | DELAY_ARG="--delay=10000" 18 | else 19 | echo "Using NVD API KEY: ${NVD_API_KEY:0:5}****" 20 | fi 21 | 22 | if [ -n "${DELAY}" ]; then 23 | echo "Overriding delay with ${DELAY}ms" 24 | DELAY_ARG="--delay=$DELAY" 25 | fi 26 | 27 | MAX_RETRY_ARG="" 28 | if [ -n "${MAX_RETRY}" ]; then 29 | echo "Using max retry attempts: $MAX_RETRY" 30 | MAX_RETRY_ARG="--maxRetry=$MAX_RETRY" 31 | fi 32 | 33 | MAX_RECORDS_PER_PAGE_ARG="" 34 | if [ -n "${MAX_RECORDS_PER_PAGE}" ]; then 35 | echo "Using max records per page: $MAX_RECORDS_PER_PAGE" 36 | MAX_RECORDS_PER_PAGE_ARG="--recordsPerPage=$MAX_RECORDS_PER_PAGE" 37 | fi 38 | 39 | MAX_DAYS_OF_YEAR_RANGED_ARG="" 40 | if [ -n "${MAX_DAYS_OF_YEAR_RANGED}" ]; then 41 | echo "Limiting a maximum of $MAX_DAYS_OF_YEAR_RANGED for a year slice" 42 | MAX_DAYS_OF_YEAR_RANGED_ARG="--maxDaysOfYearRange=$MAX_DAYS_OF_YEAR_RANGED" 43 | fi 44 | 45 | FORCE_UPDATE_ARG="" 46 | if [ -n "${FORCE_UPDATE}" ]; then 47 | echo "Forcing full update of years" 48 | FORCE_UPDATE_ARG="--forceUpdate" 49 | fi 50 | 51 | DEBUG_ARG="" 52 | if [ -n "${DEBUG}" ]; then 53 | echo "Enabling debug mode" 54 | DEBUG_ARG="--debug" 55 | fi 56 | 57 | function remove_lockfile() { 58 | rm -f $LOCKFILE 59 | exit 0 60 | } 61 | trap remove_lockfile SIGHUP SIGINT SIGQUIT SIGABRT SIGALRM SIGTERM SIGTSTP 62 | 63 | attempt=1 64 | max_attempts=5 65 | if [ -n "${MAX_MIRROR_RETRIES}" ]; then 66 | echo "Going to try up to $MAX_MIRROR_RETRIES times on a mirror fail" 67 | max_attempts=$MAX_MIRROR_RETRIES 68 | fi 69 | 70 | 71 | set +e 72 | while [ $attempt -le $max_attempts ]; do 73 | java $JAVA_OPT -jar /usr/local/bin/vulnz cve $DELAY_ARG $DEBUG_ARG $MAX_RETRY_ARG $MAX_RECORDS_PER_PAGE_ARG $MAX_DAYS_OF_YEAR_RANGED_ARG $FORCE_UPDATE_ARG --cache --directory /usr/local/apache2/htdocs 74 | exit_code=$? 75 | # 100: indicates an issue downloading the full CVE dataset from the NVD API; we will retry the update immediately to utilize the forced persistent HTTP Response Cache. 76 | if [ $exit_code -ne 100 ]; then 77 | break 78 | fi 79 | echo "Got exit code $exit_code, attempt $attempt of $max_attempts" 80 | attempt=$((attempt + 1)) 81 | 82 | # wait for a factor of 1 minute before retrying the mirror process. This might dodge short outages without retrying to fast 83 | retryDebounce=$((60*$attempt)) 84 | echo "waiting for $retryDebounce seconds before retrying" 85 | sleep "${retryDebounce}s" 86 | done 87 | set -e 88 | 89 | # create the directory listing for the SSI 90 | if [ -f /usr/local/apache2/htdocs/cache.properties ]; then 91 | timestamp=$(stat -c "%Y" "/usr/local/apache2/htdocs/cache.properties" | xargs -I{} date -d @{} "+%Y-%m-%d %H:%M:%S") 92 | echo "
  • cache.properties (Last Modified: $timestamp)
  • " > /usr/local/apache2/htdocs/nvd.shtml 93 | echo "
  • cve-count-per-year.json (Last Modified: $timestamp)
  • " >> /usr/local/apache2/htdocs/nvd.shtml 94 | for file in /usr/local/apache2/htdocs/nvdcve*; do 95 | filename=$(basename "$file") 96 | timestamp=$(stat -c "%Y" "$file" | xargs -I{} date -d @{} "+%Y-%m-%d %H:%M:%S") 97 | echo "
  • $filename (Last Modified: $timestamp)
  • " >> /usr/local/apache2/htdocs/nvd.shtml 98 | done 99 | fi 100 | 101 | rm -f $LOCKFILE -------------------------------------------------------------------------------- /vulnz/src/docker/scripts/validate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Validating the NVD mirror..." 4 | for file in /usr/local/apache2/htdocs/*.json.gz; do 5 | if ! gzip -t "$file"; then 6 | echo "Corrupt gz file detected: $file, clearing mirror and re-running mirror" 7 | rm -f /usr/local/apache2/htdocs/*.json.gz 8 | rm -f /usr/local/apache2/htdocs/*.meta 9 | rm -f /usr/local/apache2/htdocs/cache.properties 10 | rm -f /usr/local/apache2/htdocs/cve-count-per-year.json 11 | supervisorctl start init_mirror 12 | break 13 | fi 14 | done 15 | 16 | -------------------------------------------------------------------------------- /vulnz/src/docker/supervisor/supervisord.conf: -------------------------------------------------------------------------------- 1 | ; supervisor config file 2 | 3 | [unix_http_server] 4 | file=/dev/shm/supervisor.sock ; (the path to the socket file) 5 | chmod=0700 ; sockef file mode (default 0700) 6 | username = dummy 7 | password = dummy 8 | 9 | [supervisord] 10 | logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log) 11 | logfile_maxbytes=5MB 12 | logfile_backups=3 13 | nodaemon=true 14 | pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid) 15 | childlogdir=/var/log/supervisor ; ('AUTO' child log dir, default $TEMP) 16 | loglevel = WARN 17 | # Privileges were not dropped because no user is specified in the config file. If you intend to run as root, you can set user=root in the config file to avoid this message. 18 | user = root 19 | 20 | ; the below section must remain in the config file for RPC 21 | ; (supervisorctl/web interface) to work, additional interfaces may be 22 | ; added by defining them in separate rpcinterface: sections 23 | [rpcinterface:supervisor] 24 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 25 | 26 | [supervisorctl] 27 | serverurl=unix:///dev/shm/supervisor.sock ; use a unix:// URL for a unix socket 28 | username = dummy 29 | password = dummy 30 | 31 | ; The [include] section can just contain the "files" setting. This 32 | ; setting can list multiple files (separated by whitespace or 33 | ; newlines). It can also contain wildcards. The filenames are 34 | ; interpreted as relative to this file. Included files *cannot* 35 | ; include files themselves. 36 | 37 | [include] 38 | files = /etc/supervisor/conf.d/*.conf 39 | 40 | [program:httpd] 41 | priority=1 42 | command=/usr/local/bin/httpd-foreground 43 | stdout_logfile=/dev/fd/1 44 | stdout_logfile_maxbytes=0 45 | redirect_stderr=true 46 | 47 | [program:crond] 48 | priority=3 49 | command=crond -s /var/spool/cron/crontabs -f 50 | stdout_logfile=/dev/fd/1 51 | stdout_logfile_maxbytes=0 52 | redirect_stderr=true 53 | 54 | [program:init_mirror] 55 | priority=2 56 | command=/mirror-all.sh 57 | autorestart=false 58 | stdout_logfile=/dev/fd/1 59 | stdout_logfile_maxbytes=0 60 | redirect_stderr=true 61 | user=mirror 62 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/Application.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2022-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli; 18 | 19 | import io.github.jeremylong.vulnz.cli.commands.MainCommand; 20 | import io.prometheus.metrics.core.metrics.Counter; 21 | import io.prometheus.metrics.model.snapshots.Labels; 22 | import org.springframework.beans.factory.annotation.Value; 23 | import org.springframework.boot.CommandLineRunner; 24 | import org.springframework.boot.ExitCodeGenerator; 25 | import org.springframework.boot.SpringApplication; 26 | import org.springframework.boot.autoconfigure.ImportAutoConfiguration; 27 | import org.springframework.boot.autoconfigure.SpringBootApplication; 28 | import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration; 29 | import org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration; 30 | import org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration; 31 | import org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration; 32 | import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; 33 | import org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration; 34 | import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; 35 | import org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration; 36 | import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; 37 | import org.springframework.scheduling.annotation.EnableScheduling; 38 | import picocli.CommandLine; 39 | import picocli.spring.boot.autoconfigure.PicocliAutoConfiguration; 40 | 41 | /** 42 | * Main entry point for the Open Vulnerability CLI. 43 | */ 44 | @SpringBootApplication() 45 | @EnableScheduling 46 | // speed up spring load time. 47 | @ImportAutoConfiguration(value = {PicocliAutoConfiguration.class}, exclude = { 48 | ConfigurationPropertiesAutoConfiguration.class, ProjectInfoAutoConfiguration.class, 49 | PropertyPlaceholderAutoConfiguration.class, LifecycleAutoConfiguration.class, 50 | ApplicationAvailabilityAutoConfiguration.class, AopAutoConfiguration.class, JacksonAutoConfiguration.class, 51 | SqlInitializationAutoConfiguration.class, TaskExecutionAutoConfiguration.class}) 52 | public class Application implements CommandLineRunner, ExitCodeGenerator { 53 | private final CommandLine.IFactory factory; 54 | private final MainCommand command; 55 | private int exitCode; 56 | 57 | @Value("${application.version:0.0.0}") 58 | private String applicationVersion; 59 | 60 | @Value("${spring.application.name:nvd}") 61 | private String applicationName; 62 | 63 | Application(CommandLine.IFactory factory, MainCommand command) { 64 | this.factory = factory; 65 | this.command = command; 66 | } 67 | 68 | /** 69 | * Main entry point for the Open Vulnerability CLI. 70 | * 71 | * @param args command line arguments 72 | */ 73 | public static void main(String[] args) { 74 | String[] arguments = args; 75 | if (arguments.length == 0) { 76 | arguments = new String[]{"--help"}; 77 | } 78 | System.exit(SpringApplication.exit(SpringApplication.run(Application.class, arguments))); 79 | } 80 | 81 | /** 82 | * Get the exit code. 83 | * 84 | * @return the exit code 85 | */ 86 | @Override 87 | public int getExitCode() { 88 | return exitCode; 89 | } 90 | 91 | /** 92 | * Run the application. 93 | * 94 | * @param args command line arguments 95 | */ 96 | @Override 97 | public void run(String... args) { 98 | Counter.builder().name("application").help("Information about the current project version and name") 99 | .constLabels(Labels.of("version", applicationVersion, "name", applicationName)).register().inc(); 100 | 101 | // add extra line to make output more readable 102 | System.err.println(); 103 | exitCode = new CommandLine(command, factory).setCaseInsensitiveEnumValuesAllowed(true).execute(args); 104 | System.err.println(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/cache/CacheException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2023-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.cache; 18 | 19 | /** 20 | * Exception thrown when a cache operation fails. 21 | */ 22 | public class CacheException extends RuntimeException { 23 | 24 | /** 25 | * Constructs a new exception with the specified detail message. 26 | * 27 | * @param msg the detailed message 28 | */ 29 | public CacheException(String msg) { 30 | super(msg); 31 | } 32 | 33 | /** 34 | * Constructs a new exception with the specified detail message and cause. 35 | * 36 | * @param msg the detailed message 37 | * @param cause the cause 38 | */ 39 | public CacheException(String msg, Throwable cause) { 40 | super(msg, cause); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/cache/CacheProperties.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2023-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.cache; 18 | 19 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 20 | 21 | import java.io.File; 22 | import java.io.FileInputStream; 23 | import java.io.FileOutputStream; 24 | import java.io.IOException; 25 | import java.io.InputStream; 26 | import java.io.OutputStream; 27 | import java.time.ZonedDateTime; 28 | import java.time.format.DateTimeFormatter; 29 | import java.util.Properties; 30 | 31 | /** 32 | * Cache properties. 33 | */ 34 | public class CacheProperties { 35 | /** 36 | * The name of the cache properties file. 37 | */ 38 | private static final String NAME = "cache.properties"; 39 | /** 40 | * The cache property indicating if the cache is new. 41 | */ 42 | public static final String IS_NEW = "cache.is.new"; 43 | /** 44 | * The cache property indicating if an existing cache is used. 45 | */ 46 | public static final String USING_EXISTING_CACHE = "cache.used.existing"; 47 | /** 48 | * The cache property indicating the cache directory. 49 | */ 50 | private Properties properties = new Properties(); 51 | /** 52 | * The cache directory. 53 | */ 54 | private File directory; 55 | 56 | /** 57 | * Constructs a new CacheProperties. 58 | * 59 | * @param dir the cache directory 60 | */ 61 | @SuppressFBWarnings("CT_CONSTRUCTOR_THROW") 62 | public CacheProperties(File dir) { 63 | directory = dir; 64 | if (!directory.isDirectory() && !directory.mkdirs()) { 65 | throw new CacheException("Unable to create cache directory: " + directory); 66 | } 67 | File file = new File(directory, NAME); 68 | if (file.isFile()) { 69 | try (InputStream input = new FileInputStream(file)) { 70 | properties.load(input); 71 | } catch (IOException exception) { 72 | throw new CacheException("Unable to create read properties file: " + directory, exception); 73 | } 74 | } else { 75 | properties.setProperty(IS_NEW, "true"); 76 | } 77 | } 78 | 79 | /** 80 | * Returns true if the cache properties contains the given key. 81 | * 82 | * @param key the key 83 | * @return true if the cache properties contains the given key 84 | */ 85 | public boolean has(String key) { 86 | return properties.containsKey(key); 87 | } 88 | 89 | /** 90 | * Returns the value for the given key. 91 | * 92 | * @param key the key 93 | * @return the value for the given key 94 | */ 95 | public String get(String key) { 96 | return properties.getProperty(key); 97 | } 98 | 99 | /** 100 | * Returns the value for the given key or the default value if the key is not found. 101 | * 102 | * @param key the key 103 | * @param defaultValue the default value 104 | * @return the value for the given key or the default value if the key is not found 105 | */ 106 | public String get(String key, String defaultValue) { 107 | return properties.getProperty(key, defaultValue); 108 | } 109 | 110 | /** 111 | * Returns value for the given key as a timestamp. 112 | * 113 | * @param key the key 114 | * @return the value for the given key as a timestamp 115 | */ 116 | public ZonedDateTime getTimestamp(String key) { 117 | DateTimeFormatter dtf = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ssX"); 118 | if (has(key)) { 119 | String value = get(key); 120 | return ZonedDateTime.parse(value, dtf); 121 | } 122 | return null; 123 | } 124 | 125 | /** 126 | * Sets the value for the given key. 127 | * 128 | * @param key the key 129 | * @param timestamp the value 130 | */ 131 | public void set(String key, ZonedDateTime timestamp) { 132 | DateTimeFormatter dtf = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ssX"); 133 | set(key, dtf.format(timestamp)); 134 | } 135 | 136 | /** 137 | * Sets the value for the given key. 138 | * 139 | * @param key the key 140 | * @param value the value 141 | */ 142 | public void set(String key, String value) { 143 | properties.setProperty(key, value); 144 | } 145 | 146 | /** 147 | * Returns the cache directory. 148 | * 149 | * @return the cache directory 150 | */ 151 | public File getDirectory() { 152 | return directory; 153 | } 154 | 155 | /** 156 | * Saves the cache properties. 157 | * 158 | * @throws CacheException if unable to save the cache properties 159 | */ 160 | public void save() throws CacheException { 161 | File file = new File(directory, NAME); 162 | try (OutputStream output = new FileOutputStream(file, false)) { 163 | properties.store(output, null); 164 | } catch (IOException exception) { 165 | throw new CacheException("Unable to write properties file: " + directory, exception); 166 | } 167 | } 168 | 169 | /** 170 | * Removes the given key. 171 | * 172 | * @param key the key 173 | */ 174 | public void remove(String key) { 175 | if (properties.containsKey(key)) { 176 | properties.remove(key); 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/cache/CacheUpdateException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2023-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.cache; 18 | 19 | /** 20 | * Exception thrown when a cache update fails. 21 | */ 22 | public class CacheUpdateException extends Exception { 23 | 24 | /** 25 | * Constructs a new exception with the specified detail message. 26 | * 27 | * @param msg the detailed message 28 | */ 29 | public CacheUpdateException(String msg) { 30 | super(msg); 31 | } 32 | 33 | /** 34 | * Constructs a new exception with the specified detail message and cause. 35 | * 36 | * @param msg the detailed message 37 | * @param cause the cause 38 | */ 39 | public CacheUpdateException(String msg, Throwable cause) { 40 | super(msg, cause); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/commands/AbstractHelpfulCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2022-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.commands; 18 | 19 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 20 | import picocli.CommandLine; 21 | 22 | import java.util.concurrent.Callable; 23 | 24 | /** 25 | * Abstract base class for commands that provides help. 26 | */ 27 | public abstract class AbstractHelpfulCommand implements Callable { 28 | @CommandLine.Option(names = {"-h", 29 | "--help"}, usageHelp = true, description = "Displays information describing the command options") 30 | @SuppressFBWarnings("URF_UNREAD_FIELD") 31 | private boolean helpRequested = false; 32 | @CommandLine.Option(names = {"--debug"}, description = "Enable debug output") 33 | private boolean debug = false; 34 | 35 | /** 36 | * Returns whether the debug flag is set. 37 | * 38 | * @return true if the debug flag is set 39 | */ 40 | protected boolean isDebug() { 41 | return debug; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/commands/AbstractJsonCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2022-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.commands; 18 | 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | import picocli.CommandLine; 22 | 23 | /** 24 | * Abstract base class for commands that output JSON. 25 | */ 26 | public abstract class AbstractJsonCommand extends TimedCommand { 27 | /** 28 | * Reference to the logger. 29 | */ 30 | private static final Logger LOG = LoggerFactory.getLogger(AbstractJsonCommand.class); 31 | 32 | @CommandLine.Option(names = {"--prettyPrint"}, description = "Pretty print the JSON output") 33 | private boolean prettyPrint = false; 34 | 35 | /** 36 | * Returns whether the pretty print flag is set. 37 | * 38 | * @return true if the pretty print flag is set 39 | */ 40 | protected boolean isPrettyPrint() { 41 | return prettyPrint; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/commands/AbstractNvdCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2022-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.commands; 18 | 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | import picocli.CommandLine; 22 | 23 | /** 24 | * Abstract base class for commands that output JSON. 25 | */ 26 | public abstract class AbstractNvdCommand extends AbstractJsonCommand { 27 | /** 28 | * Reference to the logger. 29 | */ 30 | private static final Logger LOG = LoggerFactory.getLogger(AbstractNvdCommand.class); 31 | @CommandLine.Option(names = { 32 | "--delay"}, description = "The delay in milliseconds between API calls to the NVD - important if pulling a larger data set without an API Key") 33 | private int delay; 34 | @CommandLine.Option(names = { 35 | "--maxRetry"}, description = "The maximum number of retry attempts on 503 and 429 errors from the NVD API") 36 | private int maxRetry; 37 | @CommandLine.Option(names = { 38 | "--pageCount"}, description = "The number of `pages` of data to retrieve from the NVD if more then a single page is returned") 39 | private int pageCount = 0; 40 | @CommandLine.Option(names = { 41 | "--requestCount"}, description = "The number of requests to make to the NVD API in a 30-second rolling window") 42 | private int requestsPer30Seconds = 0; 43 | @CommandLine.Option(names = { 44 | "--recordsPerPage"}, description = "The number of records per pages of data to retrieve from the NVD in a single call") 45 | private int recordsPerPage = 2000; 46 | // yes - this should not be a string, but seriously down the call path the HttpClient 47 | // doesn't support passing a header in as a char[]... 48 | private String apiKey = null; 49 | 50 | /** 51 | * Returns the page count. 52 | * 53 | * @return the page count 54 | */ 55 | protected int getPageCount() { 56 | return pageCount; 57 | } 58 | 59 | /** 60 | * Returns the number of requests to make over a 30-second window. 61 | * 62 | * @return the number of requests to make over a 30-second window 63 | */ 64 | protected int getRequestPer30Seconds() { 65 | return requestsPer30Seconds; 66 | } 67 | 68 | /** 69 | * Returns the number of records per page of data to retrieve from the NVD in a single call. 70 | * 71 | * @return the number of records per page of data to retrieve from the NVD in a single call 72 | */ 73 | protected int getRecordsPerPage() { 74 | return recordsPerPage; 75 | } 76 | 77 | /** 78 | * Returns the delay in milliseconds between API calls to the NVD. 79 | * 80 | * @return the delay in milliseconds between API calls to the NVD 81 | */ 82 | protected int getDelay() { 83 | return delay; 84 | } 85 | 86 | /** 87 | * Returns the maximum number of retry attempts on 503 and 429 errors from the NVD API. 88 | * 89 | * @return the maximum number of retry attempts on 503 and 429 errors from the NVD API 90 | */ 91 | protected int getMaxRetry() { 92 | return maxRetry; 93 | } 94 | 95 | /** 96 | * Returns the NVD API Key if supplied. 97 | * 98 | * @return the NVD API Key if supplied; otherwise null 99 | */ 100 | protected String getApiKey() { 101 | if (apiKey == null && System.getenv("NVD_API_KEY") != null) { 102 | String key = System.getenv("NVD_API_KEY"); 103 | if (key != null && key.startsWith("op://")) { 104 | LOG.warn( 105 | "NVD_API_KEY begins with op://; you are not logged in, did not use the `op run` command, or the environment is setup incorrectly"); 106 | return null; 107 | } else if (key != null && key.trim().isEmpty()) { 108 | LOG.warn( 109 | "NVD_API_KEY environment variable is empty; please set the NVD_API_KEY environment variable to your NVD API Key"); 110 | return null; 111 | } else { 112 | return key; 113 | } 114 | } 115 | return apiKey; 116 | } 117 | 118 | /** 119 | * Sets the NVD API Key. 120 | * 121 | * @param apiKey the NVD API Key 122 | */ 123 | @CommandLine.Option(names = { 124 | "--apikey"}, description = "NVD API Key; it is highly recommend to set the environment variable NVD_API_KEY instead of using the command line option", interactive = true) 125 | public void setApiKey(String apiKey) { 126 | LOG.warn( 127 | "For easier use - consider setting an environment variable NVD_API_KEY.\n\nSee TODO for more information"); 128 | this.apiKey = apiKey; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/commands/CveCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2022-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.commands; 18 | 19 | import ch.qos.logback.classic.Level; 20 | import ch.qos.logback.classic.LoggerContext; 21 | import io.github.jeremylong.openvulnerability.client.nvd.NvdCveClientBuilder; 22 | import io.github.jeremylong.vulnz.cli.services.NvdMirrorService; 23 | import io.github.jeremylong.vulnz.cli.services.NvdService; 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | import org.springframework.stereotype.Component; 27 | import picocli.CommandLine; 28 | 29 | import java.io.File; 30 | import java.time.ZonedDateTime; 31 | 32 | /** 33 | * Command to query the NVD CVE API. 34 | */ 35 | @Component 36 | @CommandLine.Command(name = "cve", description = "Client for the NVD Vulnerability API") 37 | public class CveCommand extends AbstractNvdCommand { 38 | /** 39 | * Reference to the logger. 40 | */ 41 | private static final Logger LOG = LoggerFactory.getLogger(CveCommand.class); 42 | 43 | @CommandLine.ArgGroup(exclusive = true) 44 | ConfigGroup configGroup; 45 | 46 | @CommandLine.ArgGroup(exclusive = false) 47 | PublishedRange publishedRange; 48 | @CommandLine.ArgGroup(exclusive = false) 49 | VirtualMatch virtualMatch; 50 | @CommandLine.Option(names = {"--cpeName"}, description = "") 51 | private String cpeName; 52 | @CommandLine.Option(names = {"--cveId"}, description = "The CVE ID") 53 | private String cveId; 54 | @CommandLine.Option(names = {"--cvssV2Metrics"}, description = "") 55 | private String cvssV2Metrics; 56 | @CommandLine.Option(names = {"--cvssV3Metrics"}, description = "") 57 | private String cvssV3Metrics; 58 | @CommandLine.Option(names = {"--keywordExactMatch"}, description = "") 59 | private String keywordExactMatch; 60 | @CommandLine.Option(names = {"--keywordSearch"}, description = "") 61 | private String keywordSearch; 62 | @CommandLine.Option(names = {"--hasCertAlerts"}, description = "") 63 | private boolean hasCertAlerts; 64 | @CommandLine.Option(names = {"--noRejected"}, defaultValue = "false", description = "") 65 | private boolean noRejected; 66 | @CommandLine.Option(names = {"--hasCertNotes"}, description = "") 67 | private boolean hasCertNotes; 68 | @CommandLine.Option(names = {"--hasKev"}, description = "") 69 | private boolean hasKev; 70 | @CommandLine.Option(names = {"--hasOval"}, description = "") 71 | private boolean hasOval; 72 | @CommandLine.Option(names = {"--isVulnerable"}, description = "") 73 | private boolean isVulnerable; 74 | @CommandLine.Option(names = {"--cvssV2Severity"}, description = "") 75 | private NvdCveClientBuilder.CvssV2Severity cvssV2Severity; 76 | @CommandLine.Option(names = {"--cvssV3Severity"}, description = "") 77 | private NvdCveClientBuilder.CvssV3Severity cvssV3Severity; 78 | @CommandLine.Option(names = {"--interactive"}, description = "Displays a progress bar") 79 | private boolean interactive; 80 | 81 | @Override 82 | public Integer timedCall() throws Exception { 83 | if (isDebug()) { 84 | LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); 85 | loggerContext.getLogger("io.github.jeremylong").setLevel(Level.DEBUG); 86 | } 87 | String apiKey = getApiKey(); 88 | if (apiKey == null || apiKey.isEmpty()) { 89 | LOG.info("NVD_API_KEY not found. Supply an API key for more generous rate limits"); 90 | apiKey = null;// in case it is empty 91 | } else { 92 | LOG.debug("NVD_API_KEY is being used"); 93 | } 94 | NvdCveClientBuilder builder = getNvdCveClientBuilder(apiKey); 95 | 96 | if (configGroup != null && configGroup.cacheSettings != null) { 97 | NvdMirrorService mirrorService = new NvdMirrorService(configGroup.cacheSettings.directory, 98 | configGroup.cacheSettings.prefix, builder, interactive); 99 | return mirrorService.process(apiKey); 100 | } 101 | if (configGroup != null && configGroup.modifiedRange != null 102 | && configGroup.modifiedRange.lastModStartDate != null) { 103 | ZonedDateTime end = configGroup.modifiedRange.lastModEndDate; 104 | if (end == null) { 105 | end = configGroup.modifiedRange.lastModStartDate.plusDays(120); 106 | } 107 | builder.withLastModifiedFilter(configGroup.modifiedRange.lastModStartDate, end); 108 | } 109 | NvdService nvdService = new NvdService(builder, isPrettyPrint(), interactive); 110 | return nvdService.process(); 111 | } 112 | 113 | /** 114 | * Get the cache directory. 115 | * 116 | * @return the cache directory 117 | */ 118 | public File getCacheDirectory() { 119 | if (configGroup != null && configGroup.cacheSettings != null) { 120 | return configGroup.cacheSettings.directory; 121 | } 122 | return null; 123 | } 124 | 125 | private NvdCveClientBuilder getNvdCveClientBuilder(String apiKey) { 126 | NvdCveClientBuilder builder = NvdCveClientBuilder.aNvdCveApi().withApiKey(apiKey); 127 | if (getDelay() > 0) { 128 | builder.withDelay(getDelay()); 129 | } 130 | if (getMaxRetry() > 0) { 131 | builder.withMaxRetryCount(getMaxRetry()); 132 | } 133 | if (cveId != null) { 134 | builder.withFilter(NvdCveClientBuilder.Filter.CVE_ID, cveId); 135 | } 136 | if (cpeName != null) { 137 | builder.withFilter(NvdCveClientBuilder.Filter.CPE_NAME, cpeName); 138 | } 139 | if (cvssV2Metrics != null) { 140 | builder.withFilter(NvdCveClientBuilder.Filter.CVSS_V2_METRICS, cvssV2Metrics); 141 | } 142 | if (cvssV3Metrics != null) { 143 | builder.withFilter(NvdCveClientBuilder.Filter.CVSS_V3_METRICS, cvssV3Metrics); 144 | } 145 | if (keywordExactMatch != null) { 146 | builder.withFilter(NvdCveClientBuilder.Filter.KEYWORD_EXACT_MATCH, keywordExactMatch); 147 | } 148 | if (keywordSearch != null) { 149 | builder.withFilter(NvdCveClientBuilder.Filter.KEYWORD_SEARCH, keywordSearch); 150 | } 151 | if (hasCertAlerts) { 152 | builder.withFilter(NvdCveClientBuilder.BooleanFilter.HAS_CERT_ALERTS); 153 | } 154 | if (noRejected) { 155 | builder.withFilter(NvdCveClientBuilder.BooleanFilter.NO_REJECTED); 156 | } 157 | if (hasCertNotes) { 158 | builder.withFilter(NvdCveClientBuilder.BooleanFilter.HAS_CERT_NOTES); 159 | } 160 | if (hasKev) { 161 | builder.withFilter(NvdCveClientBuilder.BooleanFilter.HAS_KEV); 162 | } 163 | if (hasOval) { 164 | builder.withFilter(NvdCveClientBuilder.BooleanFilter.HAS_OVAL); 165 | } 166 | if (isVulnerable) { 167 | builder.withFilter(NvdCveClientBuilder.BooleanFilter.IS_VULNERABLE); 168 | } 169 | if (cvssV2Severity != null) { 170 | builder.withCvssV2SeverityFilter(cvssV2Severity); 171 | } 172 | if (cvssV3Severity != null) { 173 | builder.withCvssV3SeverityFilter(cvssV3Severity); 174 | } 175 | if (publishedRange != null && publishedRange.pubStartDate != null && publishedRange.pubEndDate != null) { 176 | builder.withPublishedDateFilter(publishedRange.pubStartDate, publishedRange.pubEndDate); 177 | } 178 | 179 | if (virtualMatch != null && virtualMatch.virtualMatchString != null) { 180 | builder.withVirtualMatchString(virtualMatch.virtualMatchString); 181 | if (virtualMatch.matchStart != null && virtualMatch.matchStart.versionStart != null) { 182 | if (virtualMatch.matchStart.versionStartType != null) { 183 | builder.withVersionStart(virtualMatch.matchStart.versionStart, 184 | virtualMatch.matchStart.versionStartType); 185 | } else { 186 | builder.withVersionStart(virtualMatch.matchStart.versionStart); 187 | } 188 | } 189 | 190 | if (virtualMatch.matchEnd != null && virtualMatch.matchEnd.versionEnd != null) { 191 | if (virtualMatch.matchEnd.versionEndType != null) { 192 | builder.withVersionStart(virtualMatch.matchEnd.versionEnd, virtualMatch.matchEnd.versionEndType); 193 | } else { 194 | builder.withVersionStart(virtualMatch.matchEnd.versionEnd); 195 | } 196 | } 197 | } 198 | 199 | int recordCount = getRecordsPerPage(); 200 | if (recordCount > 0 && recordCount <= 2000) { 201 | builder.withResultsPerPage(recordCount); 202 | } 203 | if (getPageCount() > 0) { 204 | builder.withMaxPageCount(getPageCount()); 205 | } 206 | if (getRequestPer30Seconds() > 0) { 207 | builder.withrequestsPerThirtySeconds(getRequestPer30Seconds()); 208 | } 209 | return builder; 210 | } 211 | 212 | static class VirtualMatch { 213 | @CommandLine.Option(names = {"--virtualMatchString"}, required = true, description = "") 214 | private String virtualMatchString; 215 | 216 | @CommandLine.ArgGroup(exclusive = false) 217 | private VirtualMatchStart matchStart; 218 | 219 | @CommandLine.ArgGroup(exclusive = false) 220 | private VirtualMatchEnd matchEnd; 221 | 222 | } 223 | 224 | static class VirtualMatchEnd { 225 | @CommandLine.Option(names = {"--versionEnd"}, required = true, description = "") 226 | private String versionEnd; 227 | 228 | @CommandLine.Option(names = {"--versionEndType"}, description = "INCLUDING or EXCLUDING") 229 | private NvdCveClientBuilder.VersionType versionEndType; 230 | } 231 | 232 | static class VirtualMatchStart { 233 | @CommandLine.Option(names = {"--versionStart"}, required = true, description = "") 234 | private String versionStart; 235 | 236 | @CommandLine.Option(names = {"--versionStartType"}, description = "INCLUDING or EXCLUDING") 237 | private NvdCveClientBuilder.VersionType versionStartType; 238 | } 239 | 240 | static class ModifiedRange { 241 | @CommandLine.Option(names = "--lastModStartDate", required = true, description = "") 242 | ZonedDateTime lastModStartDate; 243 | @CommandLine.Option(names = "--lastModEndDate", description = "") 244 | ZonedDateTime lastModEndDate; 245 | } 246 | 247 | static class PublishedRange { 248 | @CommandLine.Option(names = "--pubStartDate", required = true) 249 | ZonedDateTime pubStartDate; 250 | @CommandLine.Option(names = "--pubEndDate", required = true) 251 | ZonedDateTime pubEndDate; 252 | } 253 | 254 | static class CacheSettings { 255 | @CommandLine.Option(names = "--prefix", required = false, description = "The cache file prefix", defaultValue = "nvdcve-") 256 | public String prefix; 257 | @CommandLine.Option(names = "--cache", required = true, arity = "0") 258 | boolean cache; 259 | @CommandLine.Option(names = "--directory", required = true) 260 | File directory; 261 | } 262 | 263 | static class ConfigGroup { 264 | @CommandLine.ArgGroup(exclusive = false) 265 | CacheSettings cacheSettings; 266 | @CommandLine.ArgGroup(exclusive = false) 267 | ModifiedRange modifiedRange; 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/commands/GHSACommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2022-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.commands; 18 | 19 | import ch.qos.logback.classic.Level; 20 | import ch.qos.logback.classic.LoggerContext; 21 | import com.diogonunes.jcolor.Attribute; 22 | import com.fasterxml.jackson.core.JsonEncoding; 23 | import com.fasterxml.jackson.core.JsonFactory; 24 | import com.fasterxml.jackson.core.JsonGenerator; 25 | import com.fasterxml.jackson.databind.ObjectMapper; 26 | import com.fasterxml.jackson.databind.SerializationFeature; 27 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 28 | import io.github.jeremylong.openvulnerability.client.ghsa.GitHubSecurityAdvisoryClient; 29 | import io.github.jeremylong.openvulnerability.client.ghsa.GitHubSecurityAdvisoryClientBuilder; 30 | import io.github.jeremylong.openvulnerability.client.ghsa.SecurityAdvisory; 31 | import io.github.jeremylong.vulnz.cli.model.BasicOutput; 32 | import io.github.jeremylong.vulnz.cli.ui.IProgressMonitor; 33 | import io.github.jeremylong.vulnz.cli.ui.ProgressMonitor; 34 | import org.slf4j.Logger; 35 | import org.slf4j.LoggerFactory; 36 | import org.springframework.stereotype.Component; 37 | import picocli.CommandLine; 38 | 39 | import java.time.ZonedDateTime; 40 | import java.util.Collection; 41 | import java.util.Objects; 42 | 43 | import static com.diogonunes.jcolor.Ansi.colorize; 44 | 45 | /** 46 | * Command to query the GitHub Security Advisory API. 47 | */ 48 | @Component 49 | @CommandLine.Command(name = "ghsa", description = "Client for the GitHub Security Advisory GraphQL API") 50 | public class GHSACommand extends AbstractJsonCommand { 51 | /** 52 | * Reference to the logger. 53 | */ 54 | private static final Logger LOG = LoggerFactory.getLogger(GHSACommand.class); 55 | 56 | @CommandLine.Option(names = {"--endpoint"}, description = "The GraphQL endpoint of GH or GHE") 57 | private boolean endpoint; 58 | @CommandLine.Option(names = {"--updatedSince"}, description = "The UTC date/time to filter advisories that were " 59 | + "updated since the given date") 60 | private ZonedDateTime updatedSince; 61 | @CommandLine.Option(names = {"--publishedSince"}, description = "The UTC date/time to filter advisories that were " 62 | + "published since the given date") 63 | private ZonedDateTime publishedSince; 64 | @CommandLine.Option(names = {"--classifications"}, description = "The classification of the advisory (\"GENERAL\", " 65 | + "\"MALWARE\")") 66 | private String classifications; 67 | @CommandLine.Option(names = {"--interactive"}, description = "Displays a progress bar") 68 | private boolean interactive; 69 | // yes - this should not be a string, but seriously down the call path the HttpClient 70 | // doesn't support passing a header in as a char[]... 71 | private String apiKey = null; 72 | 73 | /** 74 | * Returns the GitHub API Token Key if supplied. 75 | * 76 | * @return the GitHub API Token Key if supplied; otherwise null 77 | */ 78 | protected String getApiKey() { 79 | if (apiKey == null && System.getenv("GITHUB_TOKEN") != null) { 80 | String token = System.getenv("GITHUB_TOKEN"); 81 | if (token != null && token.startsWith("op://")) { 82 | LOG.warn("GITHUB_TOKEN begins with op://; you are not logged in, did not use the `op run` command, or " 83 | + "the environment is setup incorrectly"); 84 | } else if (token != null && token.trim().isEmpty()) { 85 | LOG.warn("GITHUB_TOKEN environment variable is empty; please set the GITHUB_TOKEN environment variable " 86 | + "to a valid GitHub API token"); 87 | return null; 88 | } else { 89 | return token; 90 | } 91 | } 92 | return apiKey; 93 | } 94 | 95 | /** 96 | * Sets the GitHub API Token Key. 97 | * 98 | * @param apiKey the GitHub API Token Key 99 | */ 100 | @CommandLine.Option(names = {"--apikey"}, description = "API Key; it is highly recommend to set the environment " 101 | + "variable, GITHUB_TOKEN, instead of using the command line option", interactive = true) 102 | public void setApiKey(String apiKey) { 103 | LOG.warn("For easier use - consider setting an environment variable GITHUB_TOKEN."); 104 | this.apiKey = apiKey; 105 | } 106 | 107 | @Override 108 | public Integer timedCall() throws Exception { 109 | if (isDebug()) { 110 | LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); 111 | loggerContext.getLogger("io.github.jeremylong").setLevel(Level.DEBUG); 112 | } 113 | GitHubSecurityAdvisoryClientBuilder builder = GitHubSecurityAdvisoryClientBuilder 114 | .aGitHubSecurityAdvisoryClient().withApiKey(getApiKey()); 115 | if (publishedSince != null) { 116 | builder.withPublishedSinceFilter(publishedSince); 117 | } 118 | if (updatedSince != null) { 119 | builder.withUpdatedSinceFilter(updatedSince); 120 | } 121 | if (classifications != null) { 122 | builder.withClassifications(classifications); 123 | } 124 | 125 | ObjectMapper objectMapper = new ObjectMapper(); 126 | objectMapper.registerModule(new JavaTimeModule()); 127 | objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); 128 | 129 | JsonFactory jfactory = objectMapper.getFactory(); 130 | JsonGenerator jsonOut = jfactory.createGenerator(System.out, JsonEncoding.UTF8); 131 | if (isPrettyPrint()) { 132 | jsonOut.useDefaultPrettyPrinter(); 133 | } 134 | 135 | jsonOut.writeStartObject(); 136 | jsonOut.writeFieldName("advisories"); 137 | jsonOut.writeStartArray(); 138 | BasicOutput output = new BasicOutput(); 139 | try (GitHubSecurityAdvisoryClient api = builder.build(); 140 | IProgressMonitor monitor = new ProgressMonitor(interactive, "GHSA")) { 141 | while (api.hasNext()) { 142 | Collection list = api.next(); 143 | if (list != null) { 144 | output.setSuccess(true); 145 | output.addCount(list.size()); 146 | for (SecurityAdvisory c : list) { 147 | jsonOut.writeObject(c); 148 | } 149 | monitor.updateProgress("GHSA", output.getCount(), api.getTotalAvailable()); 150 | } else { 151 | output.setSuccess(false); 152 | output.setReason(String.format("Received HTTP Status Code: %s", api.getLastStatusCode())); 153 | break; 154 | } 155 | } 156 | output.setLastModifiedDate(api.getLastUpdated()); 157 | jsonOut.writeEndArray(); 158 | jsonOut.writeObjectField("results", output); 159 | jsonOut.writeEndObject(); 160 | jsonOut.close(); 161 | 162 | if (!output.isSuccess()) { 163 | String msg = String.format("%nFAILED: %s", output.getReason()); 164 | LOG.info(colorize(msg, Attribute.RED_TEXT())); 165 | return 2; 166 | } 167 | LOG.info(colorize("\nSUCCESS", Attribute.GREEN_TEXT())); 168 | return 0; 169 | } catch (Exception ex) { 170 | LOG.error("\nERROR", ex); 171 | } 172 | return 1; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/commands/InstallCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2022-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.commands; 18 | 19 | import com.diogonunes.jcolor.Attribute; 20 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | import org.springframework.beans.factory.annotation.Autowired; 24 | import org.springframework.core.io.Resource; 25 | import org.springframework.core.io.ResourceLoader; 26 | import org.springframework.stereotype.Component; 27 | import picocli.CommandLine; 28 | 29 | import java.io.File; 30 | import java.io.InputStream; 31 | import java.nio.channels.Channels; 32 | import java.nio.file.Files; 33 | import java.nio.file.Path; 34 | import java.nio.file.Paths; 35 | import java.nio.file.StandardCopyOption; 36 | 37 | import static com.diogonunes.jcolor.Ansi.colorize; 38 | 39 | /** 40 | * Command to install the vulnz cli. 41 | */ 42 | @Component 43 | @CommandLine.Command(name = "install", description = "Used on mac or unix systems to create the vulnz symlink and add completion to the shell.") 44 | public class InstallCommand extends AbstractHelpfulCommand { 45 | /** 46 | * Reference to the logger. 47 | */ 48 | private static final Logger LOG = LoggerFactory.getLogger(InstallCommand.class); 49 | 50 | @Autowired 51 | private ResourceLoader resourceLoader; 52 | 53 | @Override 54 | @SuppressFBWarnings("DMI_HARDCODED_ABSOLUTE_FILENAME") 55 | public Integer call() throws Exception { 56 | 57 | final Path link; 58 | final File uLocalBin = new File("/usr/local/bin"); 59 | if (uLocalBin.isDirectory()) { 60 | link = Paths.get(uLocalBin.getPath(), "vulnz"); 61 | } else { 62 | link = Paths.get(".", "vulnz"); 63 | } 64 | final File linkFile = link.toFile(); 65 | if (linkFile.isFile() && !linkFile.delete()) { 66 | LOG.warn(colorize("Unable to delete existing link: " + link.toString(), Attribute.RED_TEXT())); 67 | } 68 | final String classResource = this.getClass().getResource(this.getClass().getSimpleName() + ".class").getPath(); 69 | LOG.error("classResource: " + classResource); 70 | final int end = classResource.indexOf("!BOOT-INF/classes/!"); 71 | String bootJar = classResource.substring(0, end); 72 | if (bootJar.startsWith("file:")) { 73 | bootJar = bootJar.substring(5); 74 | } 75 | if (bootJar.startsWith("nested:")) { 76 | bootJar = bootJar.substring(7); 77 | } 78 | final Path target = Paths.get(bootJar); 79 | 80 | Files.createSymbolicLink(link, target); 81 | 82 | LOG.info(colorize("vulnz link created: " + link.toString(), Attribute.GREEN_TEXT())); 83 | 84 | final Resource completion = resourceLoader.getResource("classpath:vulnz.completion.sh"); 85 | if (completion.exists()) { 86 | final File zshlinux = new File("/etc/bash_completion.d"); 87 | final File zshMac = new File("/usr/local/etc/bash_completion.d"); 88 | 89 | final Path destination; 90 | if (zshlinux.isDirectory()) { 91 | destination = Paths.get(zshlinux.getPath(), "vulnz.completion.sh"); 92 | } else if (zshMac.isDirectory()) { 93 | destination = Paths.get(zshMac.getPath(), "vulnz.completion.sh"); 94 | } else { 95 | destination = Paths.get(".", "vulnz.completion.sh"); 96 | } 97 | try (InputStream in = Channels.newInputStream(completion.readableChannel())) { 98 | Files.copy(in, destination, StandardCopyOption.REPLACE_EXISTING); 99 | } 100 | LOG.info(colorize("Created completion script: " + destination.toString(), Attribute.GREEN_TEXT())); 101 | } else { 102 | LOG.error("Unable to setup the completion file: {}", completion); 103 | } 104 | 105 | LOG.info(colorize("Setup complete", Attribute.GREEN_TEXT())); 106 | return 0; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/commands/MainCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2022-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.commands; 18 | 19 | import org.springframework.stereotype.Component; 20 | import picocli.CommandLine; 21 | 22 | /** 23 | * The main command. 24 | */ 25 | @Component 26 | @CommandLine.Command(name = "", subcommands = {CveCommand.class, GHSACommand.class, InstallCommand.class}) 27 | public class MainCommand extends AbstractHelpfulCommand { 28 | @Override 29 | public Integer call() throws Exception { 30 | return 0; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/commands/TimedCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2022-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.commands; 18 | 19 | import com.diogonunes.jcolor.Attribute; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | import static com.diogonunes.jcolor.Ansi.colorize; 24 | 25 | /** 26 | * Abstract base class for commands that times their execution. 27 | */ 28 | public abstract class TimedCommand extends AbstractHelpfulCommand { 29 | /** 30 | * Reference to the logger. 31 | */ 32 | private static final Logger LOG = LoggerFactory.getLogger(TimedCommand.class); 33 | 34 | /** 35 | * Executes the command and times it. 36 | * 37 | * @return the results of the command 38 | * @throws Exception if an error occurs 39 | */ 40 | public abstract Integer timedCall() throws Exception; 41 | 42 | @Override 43 | public Integer call() throws Exception { 44 | final long startTime = System.currentTimeMillis(); 45 | Integer results = timedCall(); 46 | final long endTime = System.currentTimeMillis(); 47 | final long duration = (endTime - startTime) / 1000; 48 | String msg = String.format("Completed in %s seconds", duration); 49 | LOG.info(colorize(msg, Attribute.DIM())); 50 | return results; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/model/BasicOutput.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2022-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.model; 18 | 19 | import com.fasterxml.jackson.annotation.JsonInclude; 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 22 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 23 | 24 | import java.time.ZonedDateTime; 25 | 26 | /** 27 | * Basic output for commands that return a single result. 28 | */ 29 | @JsonInclude(JsonInclude.Include.NON_NULL) 30 | @JsonPropertyOrder({"success", "reason", "lastModifiedDate", "count"}) 31 | public class BasicOutput { 32 | 33 | @JsonProperty("success") 34 | private boolean success; 35 | @JsonProperty("reason") 36 | private String reason; 37 | @JsonProperty("count") 38 | @SuppressFBWarnings("URF_UNREAD_FIELD") 39 | private int count; 40 | @JsonProperty("lastModifiedDate") 41 | @SuppressFBWarnings("URF_UNREAD_FIELD") 42 | private ZonedDateTime lastModifiedDate; 43 | 44 | /** 45 | * Sets the last modified date. 46 | * 47 | * @param lastModifiedDate the last modified date 48 | */ 49 | public void setLastModifiedDate(ZonedDateTime lastModifiedDate) { 50 | this.lastModifiedDate = lastModifiedDate; 51 | } 52 | 53 | /** 54 | * Get the last modified date 55 | * 56 | * @return the last modified date 57 | */ 58 | public ZonedDateTime getLastModifiedDate() { 59 | return lastModifiedDate; 60 | } 61 | 62 | /** 63 | * Get the count 64 | * 65 | * @return the count 66 | */ 67 | public int getCount() { 68 | return count; 69 | } 70 | 71 | /** 72 | * Add to the count 73 | * 74 | * @param size the number to add 75 | */ 76 | public void addCount(int size) { 77 | count += size; 78 | } 79 | 80 | /** 81 | * Set the success flag 82 | * 83 | * @param success the value to set 84 | */ 85 | public void setSuccess(boolean success) { 86 | this.success = success; 87 | } 88 | 89 | /** 90 | * Is the operation successful 91 | * 92 | * @return true if successful 93 | */ 94 | public boolean isSuccess() { 95 | return success; 96 | } 97 | 98 | /** 99 | * Get the reason for failure 100 | * 101 | * @return the reason for failure 102 | */ 103 | public String getReason() { 104 | return reason; 105 | } 106 | 107 | /** 108 | * Set the reason for failure 109 | * 110 | * @param reason the reason for failure 111 | */ 112 | public void setReason(String reason) { 113 | this.reason = reason; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/monitoring/CveCounterPerYear.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2022-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.monitoring; 18 | 19 | import com.fasterxml.jackson.core.type.TypeReference; 20 | import com.fasterxml.jackson.databind.ObjectMapper; 21 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 22 | import io.github.jeremylong.openvulnerability.client.nvd.CveApiJson20; 23 | import io.github.jeremylong.openvulnerability.client.nvd.DefCveItem; 24 | import io.github.jeremylong.vulnz.cli.cache.CacheException; 25 | import io.prometheus.metrics.core.metrics.Gauge; 26 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | import java.io.File; 31 | import java.io.FileInputStream; 32 | import java.io.IOException; 33 | import java.util.Map; 34 | import java.util.TreeMap; 35 | import java.util.zip.GZIPInputStream; 36 | 37 | import static io.github.jeremylong.vulnz.cli.services.NvdMirrorService.MODIFIED; 38 | 39 | /** 40 | * The CveCounterPerYear class is responsible for managing the count of Common Vulnerabilities and Exposures (CVEs) per 41 | * year. This includes initializing, updating, persisting, and providing metrics for the stored counts. 42 | */ 43 | public class CveCounterPerYear { 44 | 45 | private static final Logger LOG = LoggerFactory.getLogger(CveCounterPerYear.class); 46 | 47 | private static final Gauge CVE_COUNTER = Gauge.builder().name("cve_counter").help("Total number of cached cve's") 48 | .register(); 49 | 50 | private static final Gauge CVE_COUNTER_PER_YEAR = Gauge.builder().name("cve_counter_per_year").labelNames("year") 51 | .help("Total number of cached cve's per year").register(); 52 | 53 | private final File cveCountPerYearFile; 54 | 55 | private final ObjectMapper objectMapper; 56 | 57 | private final File directory; 58 | 59 | private final String prefix; 60 | 61 | private Map cveCountPerYear; 62 | 63 | /** 64 | * Constructs a CveCounterPerYear object with the specified directory and prefix. 65 | * 66 | * @param directory the directory where the count data is stored. 67 | * @param prefix the prefix for the count data file. 68 | */ 69 | public CveCounterPerYear(File directory, String prefix) { 70 | this.directory = directory; 71 | this.prefix = prefix; 72 | this.cveCountPerYearFile = new File(directory, "cve-count-per-year.json"); 73 | this.objectMapper = new ObjectMapper(); 74 | this.objectMapper.registerModule(new JavaTimeModule()); 75 | } 76 | 77 | /** 78 | * Initializes the count of Common Vulnerabilities and Exposures (CVEs) per year. If a cached count file exists, it 79 | * loads the count data from the file; otherwise, it calculates the count by processing the provided CVE data and 80 | * updates the Prometheus metrics gauge with the respective counts. 81 | * 82 | * @param cves a map containing CVE data, where the outer map's key represents the year and the inner map's key 83 | * represents individual CVE entries. 84 | */ 85 | public void initCveCountPerYear( 86 | Object2ObjectOpenHashMap> cves) { 87 | if (cveCountPerYearFile.isFile()) { 88 | try (FileInputStream fis = new FileInputStream(cveCountPerYearFile)) { 89 | cveCountPerYear = objectMapper.readValue(fis, new TypeReference<>() { 90 | }); 91 | } catch (IOException exception) { 92 | LOG.warn("Unable to read cve-count-per-year.json", exception); 93 | } 94 | } 95 | if (cveCountPerYear == null) { 96 | cveCountPerYear = new TreeMap<>(); 97 | loadCveCountPerYear(cves); 98 | } 99 | for (String year : cves.keySet()) { 100 | CVE_COUNTER_PER_YEAR.labelValues(year).set(cveCountPerYear.getOrDefault(year, 0)); 101 | } 102 | } 103 | 104 | private void loadCveCountPerYear( 105 | Object2ObjectOpenHashMap> cves) { 106 | for (String year : cves.keySet()) { 107 | final File file = new File(directory, prefix + year + ".json.gz"); 108 | if (file.isFile()) { 109 | try (FileInputStream fis = new FileInputStream(file); GZIPInputStream gzis = new GZIPInputStream(fis)) { 110 | CveApiJson20 data = objectMapper.readValue(gzis, CveApiJson20.class); 111 | cveCountPerYear.put(year, data.getVulnerabilities().size()); 112 | } catch (IOException exception) { 113 | throw new CacheException("Unable to read cached data: " + file, exception); 114 | } 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * Writes the count of Common Vulnerabilities and Exposures (CVEs) per year to a file. 121 | */ 122 | public void writeCveCountPerYear() { 123 | try { 124 | objectMapper.writerWithDefaultPrettyPrinter().writeValue(cveCountPerYearFile, cveCountPerYear); 125 | } catch (IOException e) { 126 | LOG.error("Unable to write year count file {}", e.getMessage()); 127 | } 128 | } 129 | 130 | /** 131 | * Sets the count of CVEs (Common Vulnerabilities and Exposures) for a specific year and updates the corresponding 132 | * metric in the Prometheus gauge. 133 | * 134 | * @param year the year for which the CVE count is being set 135 | * @param cveCount the count of CVEs for the specified year 136 | */ 137 | public void setCveCountPerYear(String year, int cveCount) { 138 | CVE_COUNTER_PER_YEAR.labelValues(year).set(cveCount); 139 | cveCountPerYear.put(year, cveCount); 140 | CVE_COUNTER.set(cveCountPerYear.entrySet().stream().filter(entry -> !MODIFIED.equalsIgnoreCase(entry.getKey())) 141 | .mapToInt(Map.Entry::getValue).sum()); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/monitoring/PrometheusFileWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2022-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.monitoring; 18 | 19 | import io.github.jeremylong.vulnz.cli.commands.CveCommand; 20 | import io.prometheus.metrics.core.metrics.Gauge; 21 | import io.prometheus.metrics.expositionformats.ExpositionFormatWriter; 22 | import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; 23 | import io.prometheus.metrics.expositionformats.PrometheusTextFormatWriter; 24 | import io.prometheus.metrics.instrumentation.jvm.JvmMetrics; 25 | import io.prometheus.metrics.model.registry.PrometheusRegistry; 26 | import jakarta.annotation.PostConstruct; 27 | import jakarta.annotation.PreDestroy; 28 | import org.slf4j.Logger; 29 | import org.slf4j.LoggerFactory; 30 | import org.springframework.beans.factory.annotation.Autowired; 31 | import org.springframework.beans.factory.annotation.Value; 32 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 33 | import org.springframework.scheduling.annotation.Scheduled; 34 | import org.springframework.stereotype.Component; 35 | 36 | import java.io.File; 37 | import java.io.FileOutputStream; 38 | import java.io.IOException; 39 | 40 | /** 41 | * Writes metrics to a file. 42 | */ 43 | @Component 44 | @ConditionalOnProperty(name = "metrics.enable", havingValue = "true") 45 | public class PrometheusFileWriter { 46 | 47 | private static final Logger LOG = LoggerFactory.getLogger(PrometheusFileWriter.class); 48 | 49 | private static final Gauge CVE_SYNC_TIME = Gauge.builder().name("cve_sync_time") 50 | .help("Total running time for the vulnz cli in ms").register(); 51 | 52 | private static final long START_TIME = System.currentTimeMillis(); 53 | 54 | @Autowired 55 | private CveCommand command; 56 | 57 | @Value("${metrics.writer.format:openmetrics}") 58 | private String metricsFormat; 59 | 60 | /** 61 | * Initialise the JVM metrics. 62 | */ 63 | @PostConstruct 64 | public void init() { 65 | JvmMetrics.builder().register(); 66 | } 67 | 68 | /** 69 | * End collecting metrics. 70 | */ 71 | @PreDestroy 72 | public void destroy() { 73 | CVE_SYNC_TIME.set((double) System.currentTimeMillis() - START_TIME); 74 | writeFile(); 75 | } 76 | 77 | /** 78 | * Write the metrics to a file. 79 | */ 80 | @Scheduled(fixedRateString = "${metrics.write.interval:5000}") 81 | public void writeFile() { 82 | File directory = command.getCacheDirectory(); 83 | if (directory != null) { 84 | final PrometheusRegistry defaultRegistry = PrometheusRegistry.defaultRegistry; 85 | try (FileOutputStream out = new FileOutputStream(new File(directory, "metrics"))) { 86 | createWriter().write(out, defaultRegistry.scrape()); 87 | } catch (IOException e) { 88 | LOG.error("Error writing metrics", e); 89 | } 90 | } 91 | } 92 | 93 | private ExpositionFormatWriter createWriter() { 94 | if ("prometheus".equalsIgnoreCase(metricsFormat)) { 95 | return new PrometheusTextFormatWriter(true); 96 | } else { 97 | return new OpenMetricsTextFormatWriter(true, true); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/services/NvdMirrorService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.services; 18 | 19 | import com.fasterxml.jackson.databind.ObjectMapper; 20 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 21 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 22 | import io.github.jeremylong.openvulnerability.client.nvd.CveApiJson20; 23 | import io.github.jeremylong.openvulnerability.client.nvd.DefCveItem; 24 | import io.github.jeremylong.openvulnerability.client.nvd.NvdCveClient; 25 | import io.github.jeremylong.openvulnerability.client.nvd.NvdCveClientBuilder; 26 | import io.github.jeremylong.vulnz.cli.cache.CacheException; 27 | import io.github.jeremylong.vulnz.cli.cache.CacheProperties; 28 | import io.github.jeremylong.vulnz.cli.cache.CacheUpdateException; 29 | import io.github.jeremylong.vulnz.cli.monitoring.CveCounterPerYear; 30 | import io.github.jeremylong.vulnz.cli.ui.IProgressMonitor; 31 | import io.github.jeremylong.vulnz.cli.ui.JlineShutdownHook; 32 | import io.github.jeremylong.vulnz.cli.ui.ProgressMonitor; 33 | import io.github.jeremylong.vulnz.cli.util.HexUtil; 34 | import io.prometheus.metrics.core.metrics.Gauge; 35 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; 36 | import org.apache.commons.io.FileUtils; 37 | import org.apache.commons.io.output.CountingOutputStream; 38 | import org.slf4j.Logger; 39 | import org.slf4j.LoggerFactory; 40 | 41 | import java.io.BufferedOutputStream; 42 | import java.io.File; 43 | import java.io.FileInputStream; 44 | import java.io.FileOutputStream; 45 | import java.io.IOException; 46 | import java.io.OutputStreamWriter; 47 | import java.io.PrintWriter; 48 | import java.nio.charset.StandardCharsets; 49 | import java.security.DigestOutputStream; 50 | import java.security.MessageDigest; 51 | import java.security.NoSuchAlgorithmException; 52 | import java.time.Year; 53 | import java.time.ZoneOffset; 54 | import java.time.ZonedDateTime; 55 | import java.time.format.DateTimeFormatter; 56 | import java.time.temporal.ChronoUnit; 57 | import java.util.ArrayList; 58 | import java.util.Arrays; 59 | import java.util.Collection; 60 | import java.util.List; 61 | import java.util.Optional; 62 | import java.util.zip.GZIPInputStream; 63 | import java.util.zip.GZIPOutputStream; 64 | 65 | /** 66 | * The NvdMirrorService class is responsible for managing the NVD mirror. This includes initializing, updating, and 67 | * persisting the NVD mirror. 68 | */ 69 | public class NvdMirrorService { 70 | /** 71 | * Reference to the logger. 72 | */ 73 | private static final Logger LOG = LoggerFactory.getLogger(NvdMirrorService.class); 74 | private static final int BUFFER_SIZE = 8192; 75 | private static final Gauge CVE_LOAD_COUNTER = Gauge.builder().name("cve_load_counter") 76 | .help("Total number of new loaded cve's").register(); 77 | 78 | /** 79 | * The name of the modified field in the NVD JSON. 80 | */ 81 | public static final String MODIFIED = "modified"; 82 | 83 | private boolean interactive; 84 | private CacheProperties properties; 85 | private ObjectMapper objectMapper; 86 | private Object2ObjectOpenHashMap> cves; 87 | private NvdCveClientBuilder nvdCveClientBuilder; 88 | private CveCounterPerYear cveCountPerYear; 89 | 90 | /** 91 | * The constructor for the NvdMirrorService class. 92 | * @param propertyFile the file containing the cache properties 93 | * @param prefix the prefix to use for the cache 94 | * @param nvdCveClientBuilder the NVD client builder 95 | * @param interactive whether the service is interactive 96 | */ 97 | public NvdMirrorService(File propertyFile, String prefix, NvdCveClientBuilder nvdCveClientBuilder, 98 | boolean interactive) { 99 | if (propertyFile.toString().startsWith("~")) { 100 | LOG.warn( 101 | "Cache directory is starting with '~', did you intend to target a folder within a user home folder? Then separate the --directory option and location by space instead of equals-sign ( --directory ~/__path__ ) so that shell-expansion resolves the proper location."); 102 | } 103 | this.properties = new CacheProperties(propertyFile); 104 | if (prefix != null) { 105 | properties.set("prefix", prefix); 106 | } 107 | this.nvdCveClientBuilder = nvdCveClientBuilder; 108 | this.interactive = interactive; 109 | objectMapper = new ObjectMapper(); 110 | objectMapper.registerModule(new JavaTimeModule()); 111 | cves = new Object2ObjectOpenHashMap<>(); 112 | cveCountPerYear = new CveCounterPerYear(properties.getDirectory(), properties.get("prefix", "nvdcve-")); 113 | populateKeys(cves); 114 | } 115 | 116 | /** 117 | * Initializes the NVD mirror service. 118 | * @param apiKey the API key to use for the NVD mirror 119 | * @return true if the initialization was successful, false otherwise 120 | */ 121 | public int process(String apiKey) { 122 | NvdCveClientBuilder builder = nvdCveClientBuilder.clone(); 123 | checkIfHttpCacheExists(); 124 | try { 125 | cveCountPerYear.initCveCountPerYear(cves); 126 | } catch (CacheException e) { 127 | LOG.error("File are corrupted", e); 128 | return 100; 129 | } 130 | if (properties.has("lastModifiedDate")) { 131 | ZonedDateTime start = properties.getTimestamp("lastModifiedDate"); 132 | ZonedDateTime end = start.plusDays(120); 133 | if (end.compareTo(ZonedDateTime.now()) > 0) { 134 | builder.withLastModifiedFilter(start, end); 135 | } else { 136 | LOG.warn("Requesting the entire set of NVD CVE data via the api " 137 | + "as the cache was last updated over 120 days ago"); 138 | } 139 | } 140 | 141 | try { 142 | int exitCode = processMirrorRequest(builder); 143 | if (exitCode == 0 /* success */) { 144 | boolean usedExistingCache = Boolean 145 | .parseBoolean(properties.get(CacheProperties.USING_EXISTING_CACHE, "false")); 146 | performCleanup(properties); 147 | properties.save(); 148 | if (usedExistingCache) { 149 | exitCode = updateMirrorWithLast24Hours(apiKey); 150 | } 151 | } 152 | return exitCode; 153 | } catch (CacheException ex) { 154 | LOG.error(ex.getMessage(), ex); 155 | } 156 | return 1; 157 | } 158 | 159 | private Integer processMirrorRequest(NvdCveClientBuilder builder) { 160 | 161 | determineHttpCachingStrategy(builder); 162 | 163 | ZonedDateTime lastModified = null; 164 | try { 165 | lastModified = downloadAllUpdates(builder, cves); 166 | } catch (CacheUpdateException e) { 167 | LOG.error("NVD Cache Update Failed", e); 168 | return 100; 169 | } 170 | if (lastModified != null) { 171 | properties.set("lastModifiedDate", lastModified); 172 | } 173 | // update cache 174 | writeJsonMirror(cves, lastModified); 175 | cveCountPerYear.writeCveCountPerYear(); 176 | return 0; 177 | } 178 | 179 | private int updateMirrorWithLast24Hours(String apiKey) { 180 | NvdCveClientBuilder builder; 181 | int exitCode; 182 | LOG.info( 183 | "Used previously cached HTTP Responses from the NVD; running the update again to obtain entries modified in the last 24 hours"); 184 | builder = nvdCveClientBuilder.clone(); 185 | ZonedDateTime start = ZonedDateTime.now(ZoneOffset.UTC).minusHours(24); 186 | ZonedDateTime end = start.plusDays(120); 187 | builder.withLastModifiedFilter(start, end); 188 | exitCode = processMirrorRequest(builder); 189 | if (exitCode == 0) { 190 | properties.save(); 191 | } 192 | return exitCode; 193 | } 194 | 195 | private void determineHttpCachingStrategy(NvdCveClientBuilder builder) { 196 | if (Boolean.parseBoolean(properties.get(CacheProperties.IS_NEW, "false"))) { 197 | final File httpCacheDir = getHttpCacheDir(properties); 198 | if (LOG.isDebugEnabled()) { 199 | LOG.debug("Using http cache directory {}", httpCacheDir); 200 | } 201 | builder.withCacheDirectory(httpCacheDir.toPath()); 202 | } 203 | } 204 | 205 | private void checkIfHttpCacheExists() { 206 | if (Boolean.parseBoolean(properties.get(CacheProperties.IS_NEW, "false"))) { 207 | File cacheDir = properties.getDirectory(); 208 | if (cacheDir != null && cacheDir.exists() && cacheDir.isDirectory()) { 209 | File[] files = cacheDir.listFiles(); 210 | if (files != null && files.length > 0 && Arrays.stream(files) 211 | .anyMatch(file -> file.lastModified() >= System.currentTimeMillis() - 72000000)) { 212 | properties.set(CacheProperties.USING_EXISTING_CACHE, "true"); 213 | } 214 | } 215 | } 216 | } 217 | 218 | private static File getHttpCacheDir(CacheProperties properties) { 219 | return new File(properties.getDirectory(), ".nvd_requests"); 220 | } 221 | 222 | private static void populateKeys( 223 | Object2ObjectOpenHashMap> cves) { 224 | cves.put(MODIFIED, new Object2ObjectOpenHashMap<>()); 225 | for (int i = 2002; i <= Year.now().getValue(); i++) { 226 | cves.put(Integer.toString(i), new Object2ObjectOpenHashMap<>()); 227 | } 228 | } 229 | 230 | private ZonedDateTime downloadAllUpdates(NvdCveClientBuilder builder, 231 | Object2ObjectOpenHashMap> cves) 232 | throws CacheUpdateException { 233 | ZonedDateTime lastModified = null; 234 | int count = 0; 235 | // retrieve from NVD API 236 | try (NvdCveClient api = builder.build(); IProgressMonitor monitor = new ProgressMonitor(interactive, "NVD")) { 237 | Runtime.getRuntime().addShutdownHook(new JlineShutdownHook()); 238 | while (api.hasNext()) { 239 | Collection data = api.next(); 240 | collectCves(cves, data); 241 | lastModified = api.getLastUpdated(); 242 | count += data.size(); 243 | CVE_LOAD_COUNTER.set(count); 244 | writeJsonMirror(cves, null, 2000); 245 | monitor.updateProgress("NVD", count, api.getTotalAvailable()); 246 | } 247 | } catch (Exception ex) { 248 | LOG.debug("\nERROR", ex); 249 | throw new CacheUpdateException("Unable to complete NVD cache update due to error: " + ex.getMessage()); 250 | } 251 | return lastModified; 252 | } 253 | 254 | private String getNvdYear(DefCveItem item) { 255 | int year = item.getCve().getPublished().getYear(); 256 | if (year < 2002) { 257 | year = 2002; 258 | } 259 | return Integer.toString(year); 260 | } 261 | 262 | private void collectCves(Object2ObjectOpenHashMap> cves, 263 | Collection vulnerabilities) { 264 | synchronized (cves) { 265 | for (DefCveItem item : vulnerabilities) { 266 | cves.get(getNvdYear(item)).put(item.getCve().getId(), item); 267 | if (ChronoUnit.DAYS.between(item.getCve().getLastModified(), ZonedDateTime.now()) <= 7) { 268 | cves.get(MODIFIED).put(item.getCve().getId(), item); 269 | } 270 | } 271 | } 272 | } 273 | 274 | private void writeJsonMirror(Object2ObjectOpenHashMap> cves, 275 | ZonedDateTime lastModified) { 276 | writeJsonMirror(cves, lastModified, 0); 277 | } 278 | 279 | @SuppressFBWarnings(value = "DM_GC", justification = "Without calling System.gc() the JVM uses 2+GB of memory, calling gc profiling results in less than 1GB") 280 | private void writeJsonMirror(Object2ObjectOpenHashMap> cves, 281 | ZonedDateTime lastModified, int writeThreshold) { 282 | final String format = "NVD_CVE"; 283 | final String version = "2.0"; 284 | final String prefix = properties.get("prefix", "nvdcve-"); 285 | 286 | List sortedKeys = new ArrayList<>(cves.keySet()); 287 | sortedKeys.sort((k1, k2) -> k2.compareTo(k1)); 288 | 289 | // flush older data after we find an entry that meets the threshold; meaning if 2020 290 | // meets the threshold and 2019 still have 500 entries - write the 500 entries 291 | boolean flushOlder = false; 292 | for (String key : sortedKeys) { 293 | Object2ObjectOpenHashMap cveApiData = cves.get(key); 294 | if ((cveApiData.isEmpty() || cveApiData.size() < writeThreshold) 295 | && !(flushOlder && !cveApiData.isEmpty())) { 296 | continue; 297 | } 298 | flushOlder = true; 299 | final File file = new File(properties.getDirectory(), prefix + key + ".json.gz"); 300 | final File meta = new File(properties.getDirectory(), prefix + key + ".meta"); 301 | final Object2ObjectOpenHashMap yearData = new Object2ObjectOpenHashMap<>(); 302 | // Load existing year data if present 303 | if (file.isFile()) { 304 | try (FileInputStream fis = new FileInputStream(file); GZIPInputStream gzis = new GZIPInputStream(fis)) { 305 | CveApiJson20 data = objectMapper.readValue(gzis, CveApiJson20.class); 306 | boolean isYearData = !MODIFIED.equals(key); 307 | // filter the "modified" data to load only the last 7 days of data 308 | data.getVulnerabilities().stream().filter(cve -> isYearData 309 | || ChronoUnit.DAYS.between(cve.getCve().getLastModified(), ZonedDateTime.now()) <= 7) 310 | .forEach(cve -> yearData.put(cve.getCve().getId(), cve)); 311 | } catch (IOException exception) { 312 | throw new CacheException("Unable to read cached data: " + file, exception); 313 | } 314 | } 315 | synchronized (cves) { 316 | yearData.putAll(cveApiData); 317 | cveApiData.clear(); 318 | } 319 | 320 | List vulnerabilities = new ArrayList(yearData.values()); 321 | vulnerabilities.sort((v1, v2) -> { 322 | return v1.getCve().getId().compareTo(v2.getCve().getId()); 323 | }); 324 | ZonedDateTime timestamp; 325 | Optional maxDate = vulnerabilities.stream().map(v -> v.getCve().getLastModified()) 326 | .max(ZonedDateTime::compareTo); 327 | if (maxDate.isPresent()) { 328 | timestamp = maxDate.get(); 329 | } else if (lastModified != null) { 330 | timestamp = lastModified; 331 | } else { 332 | timestamp = ZonedDateTime.now(); 333 | } 334 | properties.set("lastModifiedDate." + key, timestamp); 335 | int cveCount = vulnerabilities.size(); 336 | cveCountPerYear.setCveCountPerYear(key, cveCount); 337 | CveApiJson20 data = new CveApiJson20(cveCount, 0, cveCount, format, version, timestamp, vulnerabilities); 338 | MessageDigest md; 339 | try { 340 | md = MessageDigest.getInstance("SHA-256"); 341 | } catch (NoSuchAlgorithmException e) { 342 | throw new CacheException("Unable to calculate sha256 checksum", e); 343 | } 344 | long byteCount = 0; 345 | try (FileOutputStream fileOutputStream = new FileOutputStream(file); 346 | GZIPOutputStream gzipOutputStream = new GZIPOutputStream(fileOutputStream); 347 | DigestOutputStream digestOutputStream = new DigestOutputStream(gzipOutputStream, md); 348 | CountingOutputStream countingOutputStream = new CountingOutputStream(digestOutputStream); 349 | BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(countingOutputStream, 350 | BUFFER_SIZE)) { 351 | objectMapper.writeValue(bufferedOutputStream, data); 352 | byteCount = countingOutputStream.getByteCount(); 353 | } catch (IOException ex) { 354 | throw new CacheException("Unable to write cached data: " + file, ex); 355 | } 356 | yearData.clear(); 357 | vulnerabilities.clear(); 358 | System.gc(); 359 | String checksum = HexUtil.getHex(md.digest()); 360 | try (FileOutputStream fileOutputStream = new FileOutputStream(meta); 361 | OutputStreamWriter osw = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8); 362 | PrintWriter writer = new PrintWriter(osw)) { 363 | final String lmd = DateTimeFormatter.ISO_DATE_TIME.format(timestamp); 364 | writer.println("lastModifiedDate:" + lmd); 365 | writer.println("size:" + byteCount); 366 | writer.println("gzSize:" + file.length()); 367 | writer.println("sha256:" + checksum); 368 | } catch (IOException ex) { 369 | throw new CacheException("Unable to write cached meta-data: " + file, ex); 370 | } 371 | } 372 | } 373 | 374 | /** 375 | * Deletes the HTTP Cache Directory if it exists and is no longer needed, and saves the cache properties. 376 | * @param properties the cache properties to save 377 | */ 378 | private void performCleanup(CacheProperties properties) { 379 | if (Boolean.parseBoolean(properties.get(CacheProperties.IS_NEW, "false"))) { 380 | File cacheDir = getHttpCacheDir(properties); 381 | if (cacheDir.exists() && cacheDir.isDirectory()) { 382 | try { 383 | FileUtils.deleteDirectory(cacheDir); 384 | } catch (IOException ex) { 385 | LOG.error("Unable to delete the HTTP Cache Directory `{}`, error: {}", cacheDir.getAbsoluteFile(), 386 | ex.getMessage()); 387 | } 388 | } 389 | } 390 | properties.set(CacheProperties.IS_NEW, "false"); 391 | properties.remove(CacheProperties.USING_EXISTING_CACHE); 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/services/NvdService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.services; 18 | 19 | import com.diogonunes.jcolor.Attribute; 20 | import com.fasterxml.jackson.core.JsonEncoding; 21 | import com.fasterxml.jackson.core.JsonFactory; 22 | import com.fasterxml.jackson.core.JsonGenerator; 23 | import com.fasterxml.jackson.databind.ObjectMapper; 24 | import com.fasterxml.jackson.databind.SerializationFeature; 25 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 26 | import io.github.jeremylong.openvulnerability.client.nvd.DefCveItem; 27 | import io.github.jeremylong.openvulnerability.client.nvd.NvdCveClient; 28 | import io.github.jeremylong.openvulnerability.client.nvd.NvdCveClientBuilder; 29 | import io.github.jeremylong.vulnz.cli.model.BasicOutput; 30 | import io.github.jeremylong.vulnz.cli.ui.IProgressMonitor; 31 | import io.github.jeremylong.vulnz.cli.ui.JlineShutdownHook; 32 | import io.github.jeremylong.vulnz.cli.ui.ProgressMonitor; 33 | import org.slf4j.Logger; 34 | import org.slf4j.LoggerFactory; 35 | 36 | import java.io.IOException; 37 | import java.util.Collection; 38 | 39 | import static com.diogonunes.jcolor.Ansi.colorize; 40 | 41 | /** 42 | * Service to query the NVD CVE API. 43 | */ 44 | public class NvdService { 45 | 46 | /** 47 | * Reference to the logger. 48 | */ 49 | private static final Logger LOG = LoggerFactory.getLogger(NvdService.class); 50 | 51 | private boolean prettyPrint; 52 | private boolean interactive; 53 | private NvdCveClientBuilder builder; 54 | 55 | /** 56 | * Constructor for the NVD service. 57 | * 58 | * @param builder the builder 59 | * @param prettyPrint whether to pretty print the output 60 | * @param interactive whether to use interactive mode 61 | */ 62 | public NvdService(NvdCveClientBuilder builder, boolean prettyPrint, boolean interactive) { 63 | this.builder = builder; 64 | this.prettyPrint = prettyPrint; 65 | this.interactive = interactive; 66 | } 67 | 68 | /** 69 | * Process the request. 70 | * 71 | * @return the status code 72 | * @throws IOException if an error occurs 73 | */ 74 | public int process() throws IOException { 75 | JsonGenerator jsonOut = getJsonGenerator(); 76 | int status = 1; 77 | jsonOut.writeStartObject(); 78 | jsonOut.writeFieldName("cves"); 79 | jsonOut.writeStartArray(); 80 | BasicOutput output = new BasicOutput(); 81 | int count = 0; 82 | try (NvdCveClient api = builder.build(); IProgressMonitor monitor = new ProgressMonitor(interactive, "NVD")) { 83 | Runtime.getRuntime().addShutdownHook(new JlineShutdownHook()); 84 | while (api.hasNext()) { 85 | Collection list = api.next(); 86 | if (list != null) { 87 | count += list.size(); 88 | } 89 | monitor.updateProgress("NVD", count, api.getTotalAvailable()); 90 | if (list != null) { 91 | output.setSuccess(true); 92 | output.addCount(list.size()); 93 | for (DefCveItem c : list) { 94 | jsonOut.writeObject(c.getCve()); 95 | } 96 | if (output.getLastModifiedDate() == null 97 | || output.getLastModifiedDate().compareTo(api.getLastUpdated()) < 0) { 98 | output.setLastModifiedDate(api.getLastUpdated()); 99 | } 100 | } else { 101 | output.setSuccess(false); 102 | output.setReason(String.format("Received HTTP Status Code: %s", api.getLastStatusCode())); 103 | } 104 | } 105 | jsonOut.writeEndArray(); 106 | jsonOut.writeObjectField("results", output); 107 | jsonOut.writeEndObject(); 108 | jsonOut.close(); 109 | 110 | if (!output.isSuccess()) { 111 | String msg = String.format("%nFAILED: %s", output.getReason()); 112 | LOG.info(colorize(msg, Attribute.RED_TEXT())); 113 | status = 2; 114 | } 115 | LOG.info(colorize("\nSUCCESS", Attribute.GREEN_TEXT())); 116 | status = 0; 117 | } catch (Throwable ex) { 118 | LOG.error("\nERROR", ex); 119 | } 120 | return status; 121 | } 122 | 123 | private JsonGenerator getJsonGenerator() throws IOException { 124 | ObjectMapper objectMapper = new ObjectMapper(); 125 | objectMapper.registerModule(new JavaTimeModule()); 126 | objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); 127 | JsonFactory jfactory = objectMapper.getFactory(); 128 | JsonGenerator jsonOut = jfactory.createGenerator(System.out, JsonEncoding.UTF8); 129 | if (prettyPrint) { 130 | jsonOut.useDefaultPrettyPrinter(); 131 | } 132 | return jsonOut; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/ui/IProgressMonitor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2024-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.ui; 18 | 19 | /** 20 | * Interface for progress monitors. 21 | */ 22 | public interface IProgressMonitor extends AutoCloseable { 23 | 24 | /** 25 | * Add a monitor. 26 | * 27 | * @param name the name of the monitor 28 | */ 29 | public void addMonitor(String name); 30 | 31 | /** 32 | * Update the progress. 33 | * 34 | * @param name the name of the monitor 35 | * @param current the current progress 36 | * @param max the maximum progress 37 | */ 38 | public void updateProgress(String name, int current, int max); 39 | } 40 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/ui/JLineAppender.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2024-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.ui; 18 | 19 | import ch.qos.logback.classic.Level; 20 | import ch.qos.logback.classic.layout.TTLLLayout; 21 | import ch.qos.logback.classic.spi.ILoggingEvent; 22 | import ch.qos.logback.core.AppenderBase; 23 | import ch.qos.logback.core.Layout; 24 | import org.jline.terminal.Terminal; 25 | 26 | /** 27 | * JLineAppender is a custom appender for Logback that writes log messages to a JLine terminal. 28 | */ 29 | public class JLineAppender extends AppenderBase { 30 | 31 | private Layout layout; 32 | 33 | /** 34 | * Sets the layout for the appender. 35 | * 36 | * @param layout the layout to use 37 | */ 38 | public void setLayout(Layout layout) { 39 | this.layout = layout; 40 | } 41 | 42 | /** 43 | * Appends the logging event to the terminal. 44 | * 45 | * @param event the logging event 46 | */ 47 | @Override 48 | protected void append(ILoggingEvent event) { 49 | Terminal terminal = ProgressMonitor.getTerminal(); 50 | if (terminal != null) { 51 | terminal.writer().println(layout.doLayout(event)); 52 | terminal.flush(); 53 | } else { 54 | if (event.getLevel() == Level.TRACE || event.getLevel() == Level.DEBUG) { 55 | System.err.println(layout.doLayout(event)); 56 | } else { 57 | System.out.println(layout.doLayout(event)); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/ui/JlineShutdownHook.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2024-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.ui; 18 | 19 | import org.jline.terminal.Terminal; 20 | 21 | import java.io.IOException; 22 | 23 | /** 24 | * JLine shutdown hook. 25 | */ 26 | public class JlineShutdownHook extends Thread { 27 | 28 | /** 29 | * Constructs a new shutdown hook. 30 | */ 31 | public void run() { 32 | try { 33 | ProgressMonitor.closeTerminal(); 34 | } catch (IOException e) { 35 | // ignore 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/ui/ProgressMonitor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2023-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.ui; 18 | 19 | import java.io.IOException; 20 | import java.util.ArrayList; 21 | import java.util.Collections; 22 | import java.util.HashMap; 23 | import java.util.List; 24 | import java.util.Map; 25 | import java.util.stream.Collectors; 26 | 27 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 28 | import org.jline.terminal.Terminal; 29 | import org.jline.terminal.TerminalBuilder; 30 | import org.jline.utils.AttributedString; 31 | import org.jline.utils.Status; 32 | 33 | /** 34 | * Progress monitor. 35 | */ 36 | public class ProgressMonitor implements IProgressMonitor { 37 | 38 | private static Terminal terminal = null; 39 | private Status status; 40 | private boolean enabled; 41 | private Map rows = new HashMap<>(); 42 | 43 | static Terminal getTerminal() { 44 | return terminal; 45 | } 46 | 47 | /** 48 | * Constructs a new progress monitor. 49 | * @param enabled if true, progress monitor is enabled 50 | * @throws IOException if an I/O error occurs 51 | */ 52 | @SuppressFBWarnings("CT_CONSTRUCTOR_THROW") 53 | public ProgressMonitor(boolean enabled) throws IOException { 54 | this(enabled, null); 55 | } 56 | 57 | /** 58 | * Constructs a new progress monitor. 59 | * @param enabled if true, progress monitor is enabled 60 | * @param name the name of the progress monitor 61 | * @throws IOException if an I/O error occurs 62 | */ 63 | @SuppressFBWarnings({"CT_CONSTRUCTOR_THROW", "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD"}) 64 | public ProgressMonitor(boolean enabled, String name) throws IOException { 65 | this.enabled = enabled; 66 | if (enabled) { 67 | if (name != null) { 68 | addMonitor(name); 69 | } 70 | terminal = TerminalBuilder.terminal(); 71 | status = new Status(terminal); 72 | } 73 | } 74 | 75 | @Override 76 | public void addMonitor(String name) { 77 | this.rows.put(name, 0); 78 | } 79 | 80 | private List determineStatusBar() { 81 | int maxNameWidth = rows.keySet().stream().mapToInt(String::length).max().orElse(0); 82 | return rows.entrySet().stream().map(entry -> { 83 | String name = entry.getKey(); 84 | int percent = entry.getValue(); 85 | int remaining = terminal.getWidth(); 86 | remaining = Math.min(remaining, 100); 87 | StringBuilder string = new StringBuilder(remaining); 88 | remaining -= maxNameWidth; 89 | string.append(name); 90 | int spaces = maxNameWidth - name.length(); 91 | if (spaces > 0) { 92 | string.append(String.join("", Collections.nCopies(spaces, " "))); 93 | } 94 | if (percent >= 100) { 95 | string.append(" complete"); 96 | } else { 97 | String spacer = percent < 10 ? " " : ""; 98 | string.append(spacer).append(String.format(" %d%% [", percent)); 99 | remaining -= 10; 100 | int completed = remaining * percent / 100; 101 | int filler = remaining - completed; 102 | System.out.println("completed: " + completed + " filler: " + filler + " remaining: " + remaining); 103 | string.append(String.join("", Collections.nCopies(completed, "="))).append('>') 104 | .append(String.join("", Collections.nCopies(filler, " "))).append(']'); 105 | } 106 | String s = string.toString(); 107 | return new AttributedString(s); 108 | }).sorted().collect(Collectors.toList()); 109 | } 110 | 111 | @Override 112 | public void updateProgress(String name, int current, int max) { 113 | int percent = (int) (current * 100 / max); 114 | rows.put(name, percent); 115 | if (enabled) { 116 | status.update(new ArrayList()); 117 | status.resize(); 118 | List displayedRows = determineStatusBar(); 119 | status.update(displayedRows, true); 120 | } 121 | } 122 | 123 | @Override 124 | public void close() throws Exception { 125 | if (enabled) { 126 | if (status != null) { 127 | status.close(); 128 | } 129 | closeTerminal(); 130 | enabled = false; 131 | } 132 | } 133 | 134 | static void closeTerminal() throws IOException { 135 | if (terminal != null) { 136 | terminal.close(); 137 | terminal = null; 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /vulnz/src/main/java/io/github/jeremylong/vulnz/cli/util/HexUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.util; 18 | 19 | /** 20 | * Utility class for working with hex strings. 21 | */ 22 | public class HexUtil { 23 | /** 24 | * Hex code characters used in getHex. 25 | */ 26 | private static final String HEXES = "0123456789abcdef"; 27 | 28 | /** 29 | *

    30 | * Converts a byte array into a hex string. 31 | *

    32 | * 33 | *

    34 | * This method was copied from 35 | * http://www.rgagnon.com/javadetails/java-0596.html 36 | *

    37 | * 38 | * @param raw a byte array 39 | * @return the hex representation of the byte array 40 | */ 41 | public static String getHex(byte[] raw) { 42 | if (raw == null) { 43 | return null; 44 | } 45 | final StringBuilder hex = new StringBuilder(2 * raw.length); 46 | for (final byte b : raw) { 47 | hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt(b & 0x0F)); 48 | } 49 | return hex.toString(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /vulnz/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.profiles.active=default 2 | spring.main.lazy-initialization=true 3 | spring.banner.location=banner.txt 4 | spring.main.banner-mode=LOG 5 | spring.application.name=nvd 6 | spring.main.web-application-type=none 7 | 8 | spring.main.log-startup-info=false 9 | application.version=@version@ 10 | metrics.enable=false 11 | metrics.write.interval=5000 12 | metrics.writer.format=openmetrics 13 | -------------------------------------------------------------------------------- /vulnz/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _/ 2 | _/ _/ _/ _/ _/ _/_/_/ _/_/_/_/ 3 | _/ _/ _/ _/ _/ _/ _/ _/ 4 | _/ _/ _/ _/ _/ _/ _/ _/ 5 | _/ _/_/_/ _/ _/ _/ _/_/_/_/ 6 | 7 | Version: ${AnsiColor.GREEN}${application.version}${AnsiColor.DEFAULT} 8 | 9 | Open Vulnerability Project 10 | 💖 Sponsor: https://github.com/sponsors/jeremylong 11 | -------------------------------------------------------------------------------- /vulnz/src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %msg%n%throwable 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /vulnz/src/test/java/io/github/jeremylong/vulnz/cli/NvdApplicationTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2022-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli; 18 | 19 | import io.github.jeremylong.vulnz.cli.commands.MainCommand; 20 | import org.junit.jupiter.api.Test; 21 | import org.springframework.beans.factory.annotation.Autowired; 22 | import org.springframework.boot.test.context.SpringBootTest; 23 | 24 | import static org.assertj.core.api.AssertionsForClassTypes.assertThat; 25 | 26 | @SpringBootTest 27 | class NvdApplicationTests { 28 | @Autowired 29 | private MainCommand mainCommand; 30 | 31 | @Test 32 | void shouldLoadContext() { 33 | assertThat(mainCommand).isNotNull(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /vulnz/src/test/java/io/github/jeremylong/vulnz/cli/ui/ProgressMonitorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | * 14 | * SPDX-License-Identifier: Apache-2.0 15 | * Copyright (c) 2024-2025 Jeremy Long. All Rights Reserved. 16 | */ 17 | package io.github.jeremylong.vulnz.cli.ui; 18 | 19 | import org.junit.jupiter.api.Test; 20 | 21 | import static org.junit.jupiter.api.Assertions.*; 22 | 23 | class ProgressMonitorTest { 24 | 25 | @Test 26 | void addRow() { 27 | } 28 | } --------------------------------------------------------------------------------