├── .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 |
131 |
132 |
133 |
134 |
135 |
136 |
CISA KEV
137 |
140 |
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 | }
--------------------------------------------------------------------------------