├── .editorconfig
├── .github
├── codeql-config.yml
└── workflows
│ ├── auto-updates.yml
│ ├── codeql.yml
│ └── tests.yml
├── .gitignore
├── .npmignore
├── .prettierignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── bin
└── node-version-audit
├── cspell.json
├── docker
├── Dockerfile
└── docker-entrypoint.sh
├── docs
├── apple-touch-icon.png
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── fonts
│ ├── Inter-Black.woff
│ ├── Inter-Black.woff2
│ ├── Inter-BlackItalic.woff
│ ├── Inter-BlackItalic.woff2
│ ├── Inter-Bold.woff
│ ├── Inter-Bold.woff2
│ ├── Inter-BoldItalic.woff
│ ├── Inter-BoldItalic.woff2
│ ├── Inter-ExtraBold.woff
│ ├── Inter-ExtraBold.woff2
│ ├── Inter-ExtraBoldItalic.woff
│ ├── Inter-ExtraBoldItalic.woff2
│ ├── Inter-ExtraLight.woff
│ ├── Inter-ExtraLight.woff2
│ ├── Inter-ExtraLightItalic.woff
│ ├── Inter-ExtraLightItalic.woff2
│ ├── Inter-Italic.woff
│ ├── Inter-Italic.woff2
│ ├── Inter-Light.woff
│ ├── Inter-Light.woff2
│ ├── Inter-LightItalic.woff
│ ├── Inter-LightItalic.woff2
│ ├── Inter-Medium.woff
│ ├── Inter-Medium.woff2
│ ├── Inter-MediumItalic.woff
│ ├── Inter-MediumItalic.woff2
│ ├── Inter-Regular.woff
│ ├── Inter-Regular.woff2
│ ├── Inter-SemiBold.woff
│ ├── Inter-SemiBold.woff2
│ ├── Inter-SemiBoldItalic.woff
│ ├── Inter-SemiBoldItalic.woff2
│ ├── Inter-Thin.woff
│ ├── Inter-Thin.woff2
│ ├── Inter-ThinItalic.woff
│ ├── Inter-ThinItalic.woff2
│ ├── Inter-italic.var.woff2
│ ├── Inter-roman.var.woff2
│ ├── Inter.var.woff2
│ └── inter.css
├── index.css
├── index.html
├── lightswitch05.svg
├── node-version-audit-logo-inkscape.svg
├── node-version-audit-logo.png
├── node-version-audit-logo.svg
└── rules-v1.json
├── lib
├── Args.js
├── AuditResults.js
├── CachedDownload.js
├── Changelogs.js
├── Cli.js
├── CveDetails.js
├── CveFeed.js
├── CveId.js
├── Exceptions.js
├── Logger.js
├── NodeRelease.js
├── NodeVersion.js
├── NodeVersionAudit.js
├── Rules.js
├── SupportEndDate.js
└── SupportSchedule.js
├── package-lock.json
├── package.json
├── scripts
├── bump-version.sh
└── github-commit-auto-updates.sh
├── sonar-project.properties
└── tests
├── functional
├── CachedDownload.test.js
├── Changelogs.test.js
├── Cli.test.js
└── NodeVersionAudit.test.js
└── unit
├── Args.test.js
├── Changelogs.test.js
├── Cli.test.js
├── CveDetails.test.js
├── CveFeed.test.js
├── CveId.test.js
├── Exceptions.test.js
├── NodeRelease.test.js
├── NodeVersion.test.js
├── Rules.test.js
└── SupportSchedule.test.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 4
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
--------------------------------------------------------------------------------
/.github/codeql-config.yml:
--------------------------------------------------------------------------------
1 | name: Security-and-Quality
2 |
3 | queries:
4 | - uses: security-and-quality
5 |
--------------------------------------------------------------------------------
/.github/workflows/auto-updates.yml:
--------------------------------------------------------------------------------
1 | name: Auto Updates
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: '5 1 * * *'
7 | - cron: '5 13 * * *'
8 |
9 | env:
10 | GITHUB_TOKEN: ${{ secrets.GITHUB_PAT }}
11 |
12 | jobs:
13 | run-updates:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v4
17 | with:
18 | ref: master
19 | fetch-depth: 10
20 | token: ${{ secrets.GH_PAT }}
21 |
22 | - name: Use Node.js 22.x
23 | uses: actions/setup-node@v4
24 | with:
25 | node-version: 22.x
26 | registry-url: 'https://registry.npmjs.org'
27 |
28 | - name: Change origin to bypass gh-pages issues with actions
29 | run: git remote set-url origin https://x-access-token:${{ secrets.GH_PAT }}@github.com/lightswitch05/node-version-audit.git
30 | - name: Ensure latest commit with tags
31 | run: git fetch; git fetch --tags --all; git checkout master; git pull
32 |
33 | - name: Install dependencies
34 | run: npm ci
35 |
36 | - name: Cache downloaded files
37 | uses: actions/cache@v4
38 | with:
39 | path: ${{ github.workspace }}/tmp
40 | key: ${{ runner.os }}-node-version-audit-${{ hashFiles('**/docs/rules-v1.json') }}
41 | restore-keys: |
42 | ${{ runner.os }}-node-version-audit-
43 |
44 | - run: npm run test:unit
45 | - run: ./bin/node-version-audit --full-update --no-update --vvv
46 | env:
47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48 | - run: |
49 | ./scripts/bump-version.sh
50 | npm publish
51 | env:
52 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
54 | - run: ./scripts/github-commit-auto-updates.sh
55 |
56 | - uses: docker/setup-qemu-action@v3
57 | - uses: docker/setup-buildx-action@v3
58 | - uses: docker/login-action@v3
59 | with:
60 | username: lightswitch05
61 | password: ${{ secrets.DOCKERHUB_TOKEN }}
62 | - uses: docker/login-action@v3
63 | with:
64 | registry: ghcr.io
65 | username: lightswitch05
66 | password: ${{ secrets.GITHUB_TOKEN }}
67 | - name: Build and push
68 | uses: docker/build-push-action@v6
69 | with:
70 | push: true
71 | pull: true
72 | cache-from: type=registry,ref=lightswitch05/node-version-audit:latest-cache
73 | cache-to: type=registry,ref=lightswitch05/node-version-audit:latest-cache,mode=max
74 | context: ./
75 | platforms: linux/amd64, linux/arm64
76 | build-args: |
77 | NODE_IMAGE_TAG=22
78 | file: ./docker/Dockerfile
79 | tags: |
80 | lightswitch05/node-version-audit:latest
81 | lightswitch05/node-version-audit:1
82 | ghcr.io/lightswitch05/node-version-audit:latest
83 | ghcr.io/lightswitch05/node-version-audit:1
84 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: 'CodeQL'
13 |
14 | on:
15 | push:
16 | branches: [master]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [master]
20 | schedule:
21 | - cron: '17 21 * * 0'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: ['javascript']
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v4
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v3
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
52 |
53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
54 | # If this step fails, then you should remove it and run the build manually (see below)
55 | - name: Autobuild
56 | uses: github/codeql-action/autobuild@v3
57 |
58 | # ℹ️ Command-line programs to run using the OS shell.
59 | # 📚 https://git.io/JvXDl
60 |
61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
62 | # and modify them (or add more) to build your code if your project
63 | # uses a compiled language
64 |
65 | #- run: |
66 | # make bootstrap
67 | # make release
68 |
69 | - name: Perform CodeQL Analysis
70 | uses: github/codeql-action/analyze@v3
71 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | types: [opened, synchronize, reopened]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | matrix:
15 | node-version: [18.x, 20.x, 22.x]
16 |
17 | env:
18 | IS_PRIMARY_VERSION: ${{ matrix.node-version == '22.x' }}
19 | PUBLISH: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
20 |
21 | steps:
22 | - uses: actions/checkout@v4
23 | with:
24 | fetch-depth: 0
25 |
26 | - name: Use Node.js ${{ matrix.node-version }}
27 | uses: actions/setup-node@v4
28 | with:
29 | node-version: ${{ matrix.node-version }}
30 | registry-url: 'https://registry.npmjs.org'
31 |
32 | - name: Install dependencies
33 | run: npm ci
34 |
35 | - name: Run all tests if primary node version
36 | if: ${{ env.IS_PRIMARY_VERSION }}
37 | run: npm run test
38 | env:
39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40 |
41 | - name: SonarScanner if primary node version
42 | if: ${{ env.IS_PRIMARY_VERSION }}
43 | uses: SonarSource/sonarqube-scan-action@v5
44 | env:
45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
47 |
48 | - name: Run cspell if primary node version
49 | if: ${{ env.IS_PRIMARY_VERSION }}
50 | run: npm run cspell
51 |
52 | - name: Run format:check if primary node version
53 | if: ${{ env.IS_PRIMARY_VERSION }}
54 | run: npm run format:check
55 |
56 | - name: Only run unit tests if not primary node version
57 | if: ${{ !env.IS_PRIMARY_VERSION }}
58 | run: npm run test:unit
59 | env: # Override the index file name to prevent concurrency issues
60 | NVA_INDEX_FILE_NAME: 'index${{ matrix.node-version }}.json'
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 | coverage
4 | tmp
5 | .DS_Store
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 |
2 | .github
3 | docs/**/*
4 | tests
5 | .editorconfig
6 | .prettierrc.json
7 | sonar-project.properties
8 | .idea
9 | coverage
10 | tmp
11 | scripts
12 | docker
13 |
14 | !docs/rules-v1.json
15 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | tmp
2 | coverage
3 | docs/fonts
4 | docs/rules-v1.json
5 | .scannerwork
6 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "singleQuote": true,
4 | "tabWidth": 4
5 | }
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Node Version Audit
2 |
3 | [](https://www.github.developerdan.com/node-version-audit/)
4 |
5 | [](https://github.com/lightswitch05/php-version-audit)
6 | [](https://github.com/lightswitch05/node-version-audit/actions/workflows/auto-updates.yml)
7 | [](https://www.npmjs.com/package/node-version-audit)
8 | [](https://hub.docker.com/r/lightswitch05/node-version-audit)
9 | [](https://github.com/lightswitch05/node-version-audit/blob/master/LICENSE)
10 | [](https://github.com/lightswitch05/node-version-audit/commits/master)
11 | [](https://github.com/lightswitch05/node-version-audit/commits/master)
12 |
13 | Node Version Audit is a convenience tool to easily check a given Node.js version against a regularly updated
14 | list of CVE exploits, new releases, and end of life dates.
15 |
16 | **Node Version Audit is not:** exploit detection/mitigation, vendor-specific version tracking, a replacement for
17 | staying informed on Node.js releases and security exploits.
18 |
19 | > - [Features](#features)
20 | > - [Example](#example)
21 | > - [Usage](#usage)
22 | > - [CLI](#cli)
23 | > - [Docker](#docker)
24 | > - [Direct Invocation](#direct-invocation)
25 | > - [JSON Rules](#json-rules)
26 | > - [Options](#options)
27 | > - [Output](#output)
28 | > - [Project Goals](#project-goals)
29 | > - [Acknowledgments & License](#acknowledgments--license)
30 |
31 | ## Features:
32 |
33 | - List known CVEs for a given version of Node.js
34 | - Check either the runtime version of Node.js, or a supplied version
35 | - Display end-of-life dates for a given version of Node.js
36 | - Display new releases for a given version of Node.js with configurable specificity (latest/minor/patch)
37 | - Patch: 16.13.0 -> 16.13.2
38 | - Minor: 16.13.0 -> 16.14.2
39 | - Latest: 16.13.0 -> 17.9.0
40 | - Rules automatically updated daily. Information is sourced directly from nodejs.org - you'll never be waiting on someone like me to merge a pull request before getting the latest patch information.
41 | - Multiple interfaces: CLI (via NPM), Docker, direct code import
42 | - Easily scriptable for use with CI/CD workflows. All Docker/CLI outputs are in JSON format to be consumed with your favorite tools - such as [jq](https://stedolan.github.io/jq/)
43 | - Configurable exit conditions. Use CLI flags like `--fail-security` to set a failure exit code if the given version of Node.js has a known CVE or is no longer supported.
44 | - Zero dependencies
45 |
46 | ## Example:
47 |
48 | npx node-version-audit@latest --version=16.14.1
49 | {
50 | "auditVersion": "16.14.1",
51 | "hasVulnerabilities": true,
52 | "hasSupport": true,
53 | "supportType": "active",
54 | "isLatestPatchVersion": false,
55 | "isLatestMinorVersion": false,
56 | "isLatestVersion": false,
57 | "latestPatchVersion": "16.14.2",
58 | "latestMinorVersion": "16.14.2",
59 | "latestVersion": "17.9.0",
60 | "activeSupportEndDate": "2022-10-18T00:00:00.000Z",
61 | "supportEndDate": "2024-04-30T00:00:00.000Z",
62 | "rulesLastUpdatedDate": "2022-04-13T02:37:54.081Z",
63 | "vulnerabilities": {
64 | "CVE-2022-778": {
65 | "id": "CVE-2022-778",
66 | "baseScore": 7.5,
67 | "publishedDate": "2022-03-15T17:15:00.000Z",
68 | "lastModifiedDate": "2022-04-06T20:15:00.000Z",
69 | "description": "The BN_mod_sqrt() function, which computes a modular square root, contains a bug that can cause it to loop forever for non-prime moduli. Internally this function is used when parsing certificates that contain elliptic curve public keys in compressed form or explicit elliptic curve parameters with a base point encoded in compressed form. It is possible to trigger the infinite loop by crafting a certificate that has invalid explicit curve parameters. Since certificate parsing happens prior to verification of the certificate signature, any process that parses an externally supplied certificate may thus be subject to a denial of service attack. The infinite loop can also be reached when parsing crafted private keys as they can contain explicit elliptic curve parameters. Thus vulnerable situations include: - TLS clients consuming server certificates - TLS servers consuming client certificates - Hosting providers taking certificates or private keys from customers - Certificate authorities parsing certification requests from subscribers - Anything else which parses ASN.1 elliptic curve parameters Also any other applications that use the BN_mod_sqrt() where the attacker can control the parameter values are vulnerable to this DoS issue. In the OpenSSL 1.0.2 version the public key is not parsed during initial parsing of the certificate which makes it slightly harder to trigger the infinite loop. However any operation which requires the public key from the certificate will trigger the infinite loop. In particular the attacker can use a self-signed certificate to trigger the loop during verification of the certificate signature. This issue affects OpenSSL versions 1.0.2, 1.1.1 and 3.0. It was addressed in the releases of 1.1.1n and 3.0.2 on the 15th March 2022. Fixed in OpenSSL 3.0.2 (Affected 3.0.0,3.0.1). Fixed in OpenSSL 1.1.1n (Affected 1.1.1-1.1.1m). Fixed in OpenSSL 1.0.2zd (Affected 1.0.2-1.0.2zc)."
70 | }
71 | }
72 | }
73 |
74 | ## Usage
75 |
76 | ### CLI
77 |
78 | Running directly with npx is the preferred and easiest way to use Node Version Audit.
79 |
80 | Execute the script, checking the run-time version of Node.js:
81 |
82 | npx node-version-audit@latest
83 |
84 | Produce an exit code if any CVEs are found or support has ended
85 |
86 | npx node-version-audit@latest --fail-security
87 |
88 | ### Docker
89 |
90 | Prefer Docker? Not a problem. It is just as easy to run using Docker:
91 |
92 | Check a specific version of Node.js using Docker:
93 |
94 | docker run --rm -t lightswitch05/node-version-audit:latest --version=17.9.0
95 |
96 | Check the host's Node.js version using Docker:
97 |
98 | docker run --rm -t lightswitch05/node-version-audit:latest --version=$(node -e "console.log(process.versions.node)")
99 |
100 | Run behind an HTTPS proxy (for use on restricted networks). Requires a volume mount of a directory with your trusted cert (with .crt extension) - see [update-ca-certificates](https://manpages.debian.org/buster/ca-certificates/update-ca-certificates.8.en.html) for more details.
101 |
102 | docker run --rm -t -e https_proxy='https://your.proxy.server:port/' --volume /full/path/to/trusted/certs/directory:/usr/local/share/ca-certificates lightswitch05/node-version-audit:latest --version=17.9.0
103 |
104 | ### Direct Invocation
105 |
106 | Want to integrate with Node Version Audit? That is certainly possible. A word caution, this is a very early release. I do not have any plans for breaking changes, but I am also not committed to keeping the interface code as-is if there are new features to implement. Docker/CLI/JSON is certainly the preferred over direct invocation.
107 |
108 | const { NodeVersionAudit } = require('node-version-audit/lib/NodeVersionAudit');
109 |
110 | const nodeVersionAudit = new NodeVersionAudit('17.8.0', true);
111 | const auditResults = await nodeVersionAudit.getAllAuditResults();
112 | auditResults.supportEndDate; //-> 2022-06-01T00:00:00.000Z
113 | auditResults.hasVulnerabilities(); //-> true
114 |
115 | ### JSON Rules
116 |
117 | The data used to drive Node Version Audit is automatically updated on a regular basis and is hosted on GitHub pages. This is the real meat-and-potatoes of Node Version Audit, and you can consume it directly for use in other tools. If you choose to do this, please respect the project license by giving proper attribution notices. Also, I ask any implementations to read the lastUpdatedDate and fail if it has become out of date (2+ weeks). This should not happen since it is automatically updated… but we all know how fragile software is.
118 |
119 | Get the latest Node.js 17 release version directly from the rules using [curl](https://curl.haxx.se/) and [jq](https://stedolan.github.io/jq/):
120 |
121 | curl -s https://www.github.developerdan.com/node-version-audit/rules-v1.json | jq '.latestVersions["17"]'
122 |
123 | ### Options
124 |
125 | usage: node-version-audit [--help] [--version=NODE_VERSION]
126 | [--fail-security] [--fail-support]
127 | [--fail-patch] [--fail-latest]
128 | [--no-update] [--silent]
129 | [--v]
130 | optional arguments:
131 | --help show this help message and exit.
132 | --version set the Node Version to run against. Defaults to the runtime version. This is required when running with docker.
133 | --fail-security generate a 10 exit code if any CVEs are found, or security support has ended.
134 | --fail-support generate a 20 exit code if the version of Node no longer gets active (bug) support.
135 | --fail-patch generate a 30 exit code if there is a newer patch-level release.
136 | --fail-minor generate a 40 exit code if there is a newer minor-level release.
137 | --fail-latest generate a 50 exit code if there is a newer release.
138 | --no-update do not download the latest rules. NOT RECOMMENDED!
139 | --silent do not write any error messages to STDERR.
140 | --v Set verbosity. v=warnings, vv=info, vvv=debug. Default is error. All logging writes to STDERR.
141 |
142 | ### Output
143 |
144 | - auditVersion: string - The version of Node.js that is being audited.
145 | - hasVulnerabilities: bool - If the auditVersion has any known CVEs or not.
146 | - hasSupport: bool - If the auditVersion is still receiving support.
147 | - supportType: string - The current support status of auditVersion: 'current'|'active'|'maintenance'|'none'.
148 | - isLatestPatchVersion: bool - If auditVersion is the latest patch-level release (17.9.x).
149 | - isLatestMinorVersion: bool - If auditVersion is the latest minor-level release (17.x.x).
150 | - isLatestVersion: bool - If auditVersion is the latest release (x.x.x).
151 | - latestPatchVersion: string - The latest patch-level version for auditVersion.
152 | - latestMinorVersion: string - The latest minor-level version for auditVersion.
153 | - latestVersion: string - The latest Node.js version.
154 | - activeSupportEndDate: string|null - ISO8601 formatted date for the end of active support for auditVersion.
155 | - supportEndDate: string|null - ISO8601 formatted date for the end of maintenance support for auditVersion.
156 | - rulesLastUpdatedDate: string - ISO8601 formatted date for the last time the rules were auto-updated.
157 | - vulnerabilities: object - CVEs known to affect auditVersion with details about the CVE. CVE Details might be null for recently discovered CVEs.
158 |
159 | ## Project Goals:
160 |
161 | - Always use update-to-date information and fail if it becomes too stale. Since this tool is designed to help its users stay informed, it must in turn fail if it becomes outdated.
162 | - Fail if the requested information is unavailable. ex. auditing an unknown version of Node.js like 12.50.0, or 0.9.0. Again, since this tool is designed to help its users stay informed, it must in turn fail if the requested information is unavailable.
163 | - Work in both open and closed networks (as long as the tool is up-to-date).
164 | - Minimal footprint and dependencies (no runtime dependencies).
165 | - Runtime support for the oldest supported version of Node.js. If you are using this tool with an unsupported version of Node.js, then you already have all the answers that this tool can give you: Yes, you have vulnerabilities and are out of date. Of course that is just for the run-time, it is still the goal of this project to supply information about any reasonable version of Node.js.
166 |
167 | ## Acknowledgments & License
168 |
169 | - This project is released under the [Apache License 2.0](https://raw.githubusercontent.com/lightswitch05/node-version-audit/master/LICENSE).
170 | - The accuracy of the information provided by this project cannot be verified or guaranteed. All functions are provided as convenience only and should not be relied on for accuracy or punctuality.
171 | - The logo was created using Mathias Pettersson and Brian Hammond's [Node.js Logo](https://nodejs.org/en/about/resources/#logo-downloads) as the base image. The logo has been modified from its original form to include overlay graphics.
172 | - This project and the use of the modified Node.js logo is not endorsed by Mathias Pettersson or Brian Hammond.
173 | - This project and the use of the Node.js name is not endorsed by OpenJS Foundation.
174 | - CVE details and descriptions are downloaded from National Institute of Standard and Technology's [National Vulnerability Database](https://nvd.nist.gov/). This project and the use of CVE information is not endorsed by NIST or the NVD. CVE details are provided as convenience only. The accuracy of the information cannot be verified.
175 | - Node.js release details and support dates are generated from [Changelogs](https://github.com/nodejs/node/tree/master/doc/changelogs) and the [Release Schedule](https://github.com/nodejs/Release/blob/main/schedule.json). The accuracy of the information cannot be verified.
176 |
177 | Copyright © 2022 Daniel White
178 |
--------------------------------------------------------------------------------
/bin/node-version-audit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const { Cli } = require('../lib/Cli.js');
4 | const cli = new Cli(process.argv);
5 | cli.run();
6 |
--------------------------------------------------------------------------------
/cspell.json:
--------------------------------------------------------------------------------
1 | {
2 | "words": [
3 | "lightswitch",
4 | "lcov",
5 | "gzipped",
6 | "netrc",
7 | "numstat",
8 | "scriptable",
9 | "problemtype",
10 | "refsource",
11 | "Thinkpad",
12 | "exploitability",
13 | "Insuf",
14 | "Vulerabilties",
15 | "deref",
16 | "richardlau",
17 | "mylesborins",
18 | "gsmchunk",
19 | "Pettersson",
20 | "casualbot",
21 | "cves",
22 | "IOJS",
23 | "protoype",
24 | "noline",
25 | "WORKDIR",
26 | "scannerwork"
27 | ],
28 | "includeRegExpList": ["**"],
29 | "ignorePaths": ["node_modules/", "tmp/", "docs/rules-v1.json", "*.svg", "coverage/", ".scannerwork/"]
30 | }
31 |
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG NODE_IMAGE_TAG
2 | FROM scratch AS files
3 | WORKDIR /opt/node-version-audit
4 | # none of these files change often
5 | ADD ./lib ./lib/
6 | ADD ./bin ./bin/
7 |
8 | FROM node:${NODE_IMAGE_TAG} AS builder
9 | ENV NODE_ENV=production
10 | WORKDIR /opt/node-version-audit
11 | ADD ./package-lock.json .
12 | ADD ./package.json .
13 | RUN npm ci --omit=dev
14 |
15 | FROM node:${NODE_IMAGE_TAG}
16 | WORKDIR /opt/node-version-audit
17 | ENV NVA_REQUIRE_VERSION_ARG=true
18 | ENV NODE_ENV=production
19 | COPY ./docker/docker-entrypoint.sh ./docker/docker-entrypoint.sh
20 | ENTRYPOINT ["/opt/node-version-audit/docker/docker-entrypoint.sh"]
21 | COPY --link --from=builder /opt/node-version-audit /opt/node-version-audit
22 | COPY --link --from=files /opt/node-version-audit /opt/node-version-audit
23 | # this is the only file that changes regularly
24 | COPY ./docs/rules-v1.json ./docs/
25 |
--------------------------------------------------------------------------------
/docker/docker-entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 |
4 | CA_CERT_PATH=${CA_CERT_PATH:-"/usr/local/share/ca-certificates"}
5 |
6 | # If there are files within the CA Certificates path, run update-ca-certificates
7 | if [ -e "${CA_CERT_PATH}" ] && [ "$(ls -A "${CA_CERT_PATH}")" ]; then
8 | update-ca-certificates 1> /tmp/update-ca-certificates.log
9 | fi
10 |
11 | /opt/node-version-audit/bin/node-version-audit "$@"
12 |
--------------------------------------------------------------------------------
/docs/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/apple-touch-icon.png
--------------------------------------------------------------------------------
/docs/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/favicon-16x16.png
--------------------------------------------------------------------------------
/docs/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/favicon-32x32.png
--------------------------------------------------------------------------------
/docs/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/favicon.ico
--------------------------------------------------------------------------------
/docs/fonts/Inter-Black.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-Black.woff
--------------------------------------------------------------------------------
/docs/fonts/Inter-Black.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-Black.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter-BlackItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-BlackItalic.woff
--------------------------------------------------------------------------------
/docs/fonts/Inter-BlackItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-BlackItalic.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-Bold.woff
--------------------------------------------------------------------------------
/docs/fonts/Inter-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-Bold.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter-BoldItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-BoldItalic.woff
--------------------------------------------------------------------------------
/docs/fonts/Inter-BoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-BoldItalic.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter-ExtraBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-ExtraBold.woff
--------------------------------------------------------------------------------
/docs/fonts/Inter-ExtraBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-ExtraBold.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter-ExtraBoldItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-ExtraBoldItalic.woff
--------------------------------------------------------------------------------
/docs/fonts/Inter-ExtraBoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-ExtraBoldItalic.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter-ExtraLight.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-ExtraLight.woff
--------------------------------------------------------------------------------
/docs/fonts/Inter-ExtraLight.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-ExtraLight.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter-ExtraLightItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-ExtraLightItalic.woff
--------------------------------------------------------------------------------
/docs/fonts/Inter-ExtraLightItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-ExtraLightItalic.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-Italic.woff
--------------------------------------------------------------------------------
/docs/fonts/Inter-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-Italic.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter-Light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-Light.woff
--------------------------------------------------------------------------------
/docs/fonts/Inter-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-Light.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter-LightItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-LightItalic.woff
--------------------------------------------------------------------------------
/docs/fonts/Inter-LightItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-LightItalic.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-Medium.woff
--------------------------------------------------------------------------------
/docs/fonts/Inter-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-Medium.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter-MediumItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-MediumItalic.woff
--------------------------------------------------------------------------------
/docs/fonts/Inter-MediumItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-MediumItalic.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-Regular.woff
--------------------------------------------------------------------------------
/docs/fonts/Inter-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-Regular.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter-SemiBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-SemiBold.woff
--------------------------------------------------------------------------------
/docs/fonts/Inter-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-SemiBold.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter-SemiBoldItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-SemiBoldItalic.woff
--------------------------------------------------------------------------------
/docs/fonts/Inter-SemiBoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-SemiBoldItalic.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter-Thin.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-Thin.woff
--------------------------------------------------------------------------------
/docs/fonts/Inter-Thin.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-Thin.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter-ThinItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-ThinItalic.woff
--------------------------------------------------------------------------------
/docs/fonts/Inter-ThinItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-ThinItalic.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter-italic.var.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-italic.var.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter-roman.var.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter-roman.var.woff2
--------------------------------------------------------------------------------
/docs/fonts/Inter.var.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/fonts/Inter.var.woff2
--------------------------------------------------------------------------------
/docs/fonts/inter.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Inter';
3 | font-style: normal;
4 | font-weight: 100;
5 | font-display: swap;
6 | src: url("Inter-Thin.woff2?v=3.19") format("woff2"),
7 | url("Inter-Thin.woff?v=3.19") format("woff");
8 | }
9 | @font-face {
10 | font-family: 'Inter';
11 | font-style: italic;
12 | font-weight: 100;
13 | font-display: swap;
14 | src: url("Inter-ThinItalic.woff2?v=3.19") format("woff2"),
15 | url("Inter-ThinItalic.woff?v=3.19") format("woff");
16 | }
17 |
18 | @font-face {
19 | font-family: 'Inter';
20 | font-style: normal;
21 | font-weight: 200;
22 | font-display: swap;
23 | src: url("Inter-ExtraLight.woff2?v=3.19") format("woff2"),
24 | url("Inter-ExtraLight.woff?v=3.19") format("woff");
25 | }
26 | @font-face {
27 | font-family: 'Inter';
28 | font-style: italic;
29 | font-weight: 200;
30 | font-display: swap;
31 | src: url("Inter-ExtraLightItalic.woff2?v=3.19") format("woff2"),
32 | url("Inter-ExtraLightItalic.woff?v=3.19") format("woff");
33 | }
34 |
35 | @font-face {
36 | font-family: 'Inter';
37 | font-style: normal;
38 | font-weight: 300;
39 | font-display: swap;
40 | src: url("Inter-Light.woff2?v=3.19") format("woff2"),
41 | url("Inter-Light.woff?v=3.19") format("woff");
42 | }
43 | @font-face {
44 | font-family: 'Inter';
45 | font-style: italic;
46 | font-weight: 300;
47 | font-display: swap;
48 | src: url("Inter-LightItalic.woff2?v=3.19") format("woff2"),
49 | url("Inter-LightItalic.woff?v=3.19") format("woff");
50 | }
51 |
52 | @font-face {
53 | font-family: 'Inter';
54 | font-style: normal;
55 | font-weight: 400;
56 | font-display: swap;
57 | src: url("Inter-Regular.woff2?v=3.19") format("woff2"),
58 | url("Inter-Regular.woff?v=3.19") format("woff");
59 | }
60 | @font-face {
61 | font-family: 'Inter';
62 | font-style: italic;
63 | font-weight: 400;
64 | font-display: swap;
65 | src: url("Inter-Italic.woff2?v=3.19") format("woff2"),
66 | url("Inter-Italic.woff?v=3.19") format("woff");
67 | }
68 |
69 | @font-face {
70 | font-family: 'Inter';
71 | font-style: normal;
72 | font-weight: 500;
73 | font-display: swap;
74 | src: url("Inter-Medium.woff2?v=3.19") format("woff2"),
75 | url("Inter-Medium.woff?v=3.19") format("woff");
76 | }
77 | @font-face {
78 | font-family: 'Inter';
79 | font-style: italic;
80 | font-weight: 500;
81 | font-display: swap;
82 | src: url("Inter-MediumItalic.woff2?v=3.19") format("woff2"),
83 | url("Inter-MediumItalic.woff?v=3.19") format("woff");
84 | }
85 |
86 | @font-face {
87 | font-family: 'Inter';
88 | font-style: normal;
89 | font-weight: 600;
90 | font-display: swap;
91 | src: url("Inter-SemiBold.woff2?v=3.19") format("woff2"),
92 | url("Inter-SemiBold.woff?v=3.19") format("woff");
93 | }
94 | @font-face {
95 | font-family: 'Inter';
96 | font-style: italic;
97 | font-weight: 600;
98 | font-display: swap;
99 | src: url("Inter-SemiBoldItalic.woff2?v=3.19") format("woff2"),
100 | url("Inter-SemiBoldItalic.woff?v=3.19") format("woff");
101 | }
102 |
103 | @font-face {
104 | font-family: 'Inter';
105 | font-style: normal;
106 | font-weight: 700;
107 | font-display: swap;
108 | src: url("Inter-Bold.woff2?v=3.19") format("woff2"),
109 | url("Inter-Bold.woff?v=3.19") format("woff");
110 | }
111 | @font-face {
112 | font-family: 'Inter';
113 | font-style: italic;
114 | font-weight: 700;
115 | font-display: swap;
116 | src: url("Inter-BoldItalic.woff2?v=3.19") format("woff2"),
117 | url("Inter-BoldItalic.woff?v=3.19") format("woff");
118 | }
119 |
120 | @font-face {
121 | font-family: 'Inter';
122 | font-style: normal;
123 | font-weight: 800;
124 | font-display: swap;
125 | src: url("Inter-ExtraBold.woff2?v=3.19") format("woff2"),
126 | url("Inter-ExtraBold.woff?v=3.19") format("woff");
127 | }
128 | @font-face {
129 | font-family: 'Inter';
130 | font-style: italic;
131 | font-weight: 800;
132 | font-display: swap;
133 | src: url("Inter-ExtraBoldItalic.woff2?v=3.19") format("woff2"),
134 | url("Inter-ExtraBoldItalic.woff?v=3.19") format("woff");
135 | }
136 |
137 | @font-face {
138 | font-family: 'Inter';
139 | font-style: normal;
140 | font-weight: 900;
141 | font-display: swap;
142 | src: url("Inter-Black.woff2?v=3.19") format("woff2"),
143 | url("Inter-Black.woff?v=3.19") format("woff");
144 | }
145 | @font-face {
146 | font-family: 'Inter';
147 | font-style: italic;
148 | font-weight: 900;
149 | font-display: swap;
150 | src: url("Inter-BlackItalic.woff2?v=3.19") format("woff2"),
151 | url("Inter-BlackItalic.woff?v=3.19") format("woff");
152 | }
153 |
--------------------------------------------------------------------------------
/docs/index.css:
--------------------------------------------------------------------------------
1 | :root,
2 | :root.darkTheme {
3 | --color: #c9d1d9;
4 | --background-color: #0d1117;
5 | --link-color: #58a6ff;
6 | }
7 |
8 | html {
9 | font-family: 'Inter', 'system-ui', sans-serif;
10 | line-height: 1.2;
11 | text-rendering: optimizeLegibility;
12 | }
13 |
14 | body {
15 | color: var(--color);
16 | background-color: var(--background-color);
17 | max-width: 960px;
18 | margin-left: auto;
19 | margin-right: auto;
20 | }
21 |
22 | main {
23 | margin: 0 5px 0 5px;
24 | }
25 |
26 | a {
27 | color: var(--link-color);
28 | text-decoration: none;
29 | }
30 |
31 | a:hover {
32 | text-decoration: underline;
33 | }
34 |
35 | a.noline:hover {
36 | text-decoration: none;
37 | }
38 |
39 | section {
40 | border-color: #30363d;
41 | border-style: solid;
42 | border-width: 1px;
43 | border-radius: 6px;
44 | margin: 0 0 10px 0;
45 | padding: 1px 5px 10px 5px;
46 | }
47 |
48 | code,
49 | dl {
50 | padding: 10px;
51 | margin: 10px 10px 0 10px;
52 | font-size: 0.9rem;
53 | font-family: 'Courier New', monospace;
54 | line-height: 1.4;
55 | }
56 |
57 | code {
58 | display: block;
59 | overflow: scroll;
60 | white-space: nowrap;
61 | }
62 |
63 | pre {
64 | overflow: scroll;
65 | font-size: 0.9rem;
66 | font-family: 'Courier New', monospace;
67 | line-height: 1.4;
68 | }
69 |
70 | li {
71 | margin-bottom: 3px;
72 | }
73 |
74 | li ul {
75 | margin-top: 3px;
76 | }
77 |
78 | .logo {
79 | display: block;
80 | width: 100%;
81 | max-width: 650px;
82 | margin-left: auto;
83 | margin-right: auto;
84 | }
85 |
86 | .avatar {
87 | height: 1.5em;
88 | vertical-align: bottom;
89 | display: inline-block;
90 | }
91 |
92 | footer {
93 | text-align: center;
94 | margin-bottom: 10px;
95 | }
96 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Node Version Audit
7 |
11 |
12 |
13 |
14 |
15 |
16 |
20 |
24 |
25 |
26 |
27 |
28 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
49 |
50 |
51 |
52 |
57 |
58 |
62 |
67 |
68 |
69 |
74 |
75 |
76 |
81 |
82 |
83 |
88 |
89 |
90 |
95 |
96 |
97 |
102 |
103 |
104 |
105 | Node Version Audit is a convenience tool to easily check a given Node.js version against a regularly
106 | updated list of CVE exploits, new releases, and end of life dates.
107 |
108 |
109 | Node Version Audit is not: exploit detection/mitigation, vendor-specific version
110 | tracking, a replacement for staying informed on Node.js releases and security exploits.
111 |
112 |
113 |
114 |
115 |
116 |
133 |
134 |
135 |
136 |
137 | Features
138 |
139 | List known CVEs for a given version of Node.js
140 | Check either the runtime version of Node.js, or a supplied version
141 | Display end-of-life dates for a given version of Node.js
142 |
143 | Display new releases for a given version of Node.js with configurable specificity
144 | (latest/minor/patch)
145 |
146 | Patch: 16.13.0 -> 16.13.2
147 | Minor: 16.13.0 -> 16.14.2
148 | Latest: 16.13.0 -> 17.9.0
149 |
150 |
151 |
152 | Rules automatically updated daily. Information is sourced directly from nodejs.org - you'll
153 | never be waiting on someone like me to merge a pull request before getting the latest patch
154 | information.
155 |
156 | Multiple interfaces: CLI (via NPM), Docker, direct code import
157 |
158 | Easily scriptable for use with CI/CD workflows. All Docker/CLI outputs are in JSON format to be
159 | consumed with your favorite tools - such as jq
160 |
161 |
162 | Configurable exit conditions. Use CLI flags like `--fail-security` to set a failure exit code if
163 | the given version of Node.js has a known CVE or is no longer supported.
164 |
165 | Zero dependencies
166 |
167 |
168 |
169 |
170 | Example
171 |
172 |
173 | npx node-version-audit@latest --version=16.14.1
174 | {
175 | "auditVersion": "16.14.1",
176 | "hasVulnerabilities": true,
177 | "hasSupport": true,
178 | "supportType": "active",
179 | "isLatestPatchVersion": false,
180 | "isLatestMinorVersion": false,
181 | "isLatestVersion": false,
182 | "latestPatchVersion": "16.14.2",
183 | "latestMinorVersion": "16.14.2",
184 | "latestVersion": "17.9.0",
185 | "activeSupportEndDate": "2022-10-18T00:00:00.000Z",
186 | "supportEndDate": "2024-04-30T00:00:00.000Z",
187 | "rulesLastUpdatedDate": "2022-04-13T02:37:54.081Z",
188 | "vulnerabilities": {
189 | "CVE-2022-778": {
190 | "id": "CVE-2022-778",
191 | "baseScore": 7.5,
192 | "publishedDate": "2022-03-15T17:15:00.000Z",
193 | "lastModifiedDate": "2022-04-06T20:15:00.000Z",
194 | "description": "The BN_mod_sqrt() function, which computes a modular square root, contains a bug that can cause it to loop forever for non-prime moduli. Internally this function is used when parsing certificates that contain elliptic curve public keys in compressed form or explicit elliptic curve parameters with a base point encoded in compressed form. It is possible to trigger the infinite loop by crafting a certificate that has invalid explicit curve parameters. Since certificate parsing happens prior to verification of the certificate signature, any process that parses an externally supplied certificate may thus be subject to a denial of service attack. The infinite loop can also be reached when parsing crafted private keys as they can contain explicit elliptic curve parameters. Thus vulnerable situations include: - TLS clients consuming server certificates - TLS servers consuming client certificates - Hosting providers taking certificates or private keys from customers - Certificate authorities parsing certification requests from subscribers - Anything else which parses ASN.1 elliptic curve parameters Also any other applications that use the BN_mod_sqrt() where the attacker can control the parameter values are vulnerable to this DoS issue. In the OpenSSL 1.0.2 version the public key is not parsed during initial parsing of the certificate which makes it slightly harder to trigger the infinite loop. However any operation which requires the public key from the certificate will trigger the infinite loop. In particular the attacker can use a self-signed certificate to trigger the loop during verification of the certificate signature. This issue affects OpenSSL versions 1.0.2, 1.1.1 and 3.0. It was addressed in the releases of 1.1.1n and 3.0.2 on the 15th March 2022. Fixed in OpenSSL 3.0.2 (Affected 3.0.0,3.0.1). Fixed in OpenSSL 1.1.1n (Affected 1.1.1-1.1.1m). Fixed in OpenSSL 1.0.2zd (Affected 1.0.2-1.0.2zc)."
195 | }
196 | }
197 | }
198 |
200 |
201 |
202 |
203 |
204 | Usage
205 |
206 | CLI
207 |
208 | Running directly with npx is the preferred and easiest way to use Node Version Audit.
209 |
210 | Execute the script, checking the run-time version of Node.js:
211 | npx node-version-audit@latest
212 |
213 |
214 | Produce an exit code if any CVEs are found or support has ended
215 | npx node-version-audit@latest --fail-security
216 |
217 |
218 | Docker
219 |
220 | Prefer Docker? Not a problem. It is just as easy to run using Docker:
221 |
222 | Check a specific version of Node.js using Docker:
223 | docker run --rm -t lightswitch05/node-version-audit:latest --version=17.9.0
224 |
225 |
226 | Check the host's Node.js version using Docker:
227 | docker run --rm -t lightswitch05/node-version-audit:latest --version=$(node -e
229 | "console.log(process.versions.node)")
231 |
232 |
233 | Run behind an HTTPS proxy (for use on restricted networks). Requires a volume mount of a directory
234 | with your trusted cert (with .crt extension) - see
235 | update-ca-certificates
238 | for more details.
239 | docker run --rm -t -e https_proxy='https://your.proxy.server:port/' --volume
241 | /full/path/to/trusted/certs/directory:/usr/local/share/ca-certificates
242 | lightswitch05/node-version-audit:latest --version=17.9.0
244 |
245 |
246 | Direct Invocation
247 |
248 |
249 | Want to integrate with Node Version Audit? That is certainly possible. A word caution, this is a
250 | very early release. I do not have any plans for breaking changes, but I am also not committed to
251 | keeping the interface code as-is if there are new features to implement. Docker/CLI/JSON is
252 | certainly the preferred over direct invocation.
253 |
254 | const { NodeVersionAudit } = require('node-version-audit/lib/NodeVersionAudit');
255 | const nodeVersionAudit = new NodeVersionAudit('17.8.0', true);
256 | const auditResults = await nodeVersionAudit.getAllAuditResults();
257 | auditResults.supportEndDate; //-> 2022-06-01T00:00:00.000Z
258 | auditResults.hasVulnerabilities(); //-> true
259 |
260 |
261 |
262 | JSON Rules
263 |
264 |
265 | The data used to drive Node Version Audit is automatically updated on a regular basis and is hosted
266 | on GitHub pages. This is the real meat-and-potatoes of Node Version Audit, and you can consume it
267 | directly for use in other tools. If you choose to do this, please respect the project license by
268 | giving proper attribution notices. Also, I ask any implementations to read the lastUpdatedDate and
269 | fail if it has become out of date (2+ weeks). This should not happen since it is automatically
270 | updated… but we all know how fragile software is.
271 |
272 |
273 | Get the latest Node.js 17 release version directly from the rules using
274 | curl and jq :
275 |
276 | curl -s https://www.github.developerdan.com/node-version-audit/rules-v1.json | jq
277 | '.latestVersions["17"]'
278 |
279 |
280 |
281 | Options
282 |
283 |
284 | --help
285 | show arguments help message and exit.
286 | --version=VERSION
287 |
288 | set the Node.js Version to run against. Defaults to the runtime version. This is required when
289 | running with docker.
290 |
291 | --fail-security
292 | generate a 10 exit code if any CVEs are found, or security support has ended.
293 | --fail-support
294 | generate a 20 exit code if the version of Node.js no longer gets active (bug) support.
295 | --fail-patch
296 | generate a 30 exit code if there is a newer patch-level release.
297 | --fail-minor
298 | generate a 40 exit code if there is a newer minor-level release.
299 | --fail-latest
300 | generate a 50 exit code if there is a newer release.
301 | --no-update
302 | do not download the latest rules. NOT RECOMMENDED!
303 | --silent
304 | do not write any error messages to STDERR.
305 | --v
306 |
307 | Set verbosity. v=warnings, vv=info, vvv=debug. Default is error. All logging writes to STDERR.
308 |
309 |
310 |
311 |
312 |
313 | Output
314 |
315 | • auditVersion: string
316 | The version of Node.js that is being audited.
317 | • hasVulnerabilities: bool
318 | If the auditVersion has any known CVEs or not.
319 | • hasSupport: bool
320 | If the auditVersion is still receiving support.
321 | • supportType: string
322 | The current support status of auditVersion: 'current'|'active'|'maintenance'|'none'.
323 | • isLatestPatchVersion: bool
324 | If auditVersion is the latest patch-level release (17.9.x).
325 | • isLatestMinorVersion: bool
326 | If auditVersion is the latest minor-level release (17.x.x).
327 | • isLatestVersion: bool
328 | If auditVersion is the latest release (x.x.x).
329 | • latestPatchVersion: string
330 | The latest patch-level version for auditVersion.
331 | • latestMinorVersion: string
332 | The latest minor-level version for auditVersion.
333 | • latestVersion: string
334 | The latest Node.js version.
335 | • activeSupportEndDate: string|null
336 | ISO8601 formatted date for the end of active support for auditVersion.
337 | • supportEndDate: string|null
338 | ISO8601 formatted date for the end of maintenance support for auditVersion.
339 | • rulesLastUpdatedDate: string
340 | ISO8601 formatted date for the last time the rules were auto-updated.
341 | • vulnerabilities: object
342 |
343 | CVEs known to affect auditVersion with details about the CVE. CVE Details might be null for
344 | recently discovered CVEs.
345 |
346 |
347 |
348 |
349 | Project Goals
350 |
351 |
352 | Always use update-to-date information and fail if it becomes too stale. Since this tool is
353 | designed to help its users stay informed, it must in turn fail if it becomes outdated.
354 |
355 |
356 | Fail if the requested information is unavailable. ex. auditing an unknown version of Node.js
357 | like 12.50.0, or 0.9.0. Again, since this tool is designed to help its users stay informed, it
358 | must in turn fail if the requested information is unavailable.
359 |
360 | Work in both open and closed networks (as long as the tool is up-to-date).
361 | Minimal footprint and dependencies (no runtime dependencies).
362 |
363 | Runtime support for the oldest supported version of Node.js. If you are using this tool with an
364 | unsupported version of Node.js, then you already have all the answers that this tool can give
365 | you: Yes, you have vulnerabilities and are out of date. Of course that is just for the run-time,
366 | it is still the goal of this project to supply information about any reasonable version of
367 | Node.js.
368 |
369 |
370 |
371 |
372 | License & Acknowledgments
373 |
374 |
375 | This project is released under the
376 | Apache License 2.0 .
379 |
380 |
381 | The accuracy of the information provided by this project cannot be verified or guaranteed. All
382 | functions are provided as convenience only and should not be relied on for accuracy or
383 | punctuality.
384 |
385 |
386 | The logo was created using Mathias Pettersson and Brian Hammond's
387 | Node.js Logo as the base
388 | image. The logo has been modified from its original form to include overlay graphics.
389 |
390 |
391 | This project and the use of the modified Node.js logo is not endorsed by Mathias Pettersson or
392 | Brian Hammond.
393 |
394 | This project and the use of the Node.js name is not endorsed by OpenJS Foundation.
395 |
396 | CVE details and descriptions are downloaded from National Institute of Standard and Technology's
397 | National Vulnerability Database . This project and the use of
398 | CVE information is not endorsed by NIST or the NVD. CVE details are provided as convenience
399 | only. The accuracy of the information cannot be verified.
400 |
401 |
402 | Node.js release details and support dates are generated from
403 | Changelogs
404 | and the
405 | Release Schedule . The
406 | accuracy of the information cannot be verified.
407 |
408 |
409 |
410 |
411 | Copyright © 2022 Daniel White
412 |
413 |
414 |
--------------------------------------------------------------------------------
/docs/lightswitch05.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/docs/node-version-audit-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lightswitch05/node-version-audit/c988c83ab9bce075b5bb7815d26b86248ccd2e56/docs/node-version-audit-logo.png
--------------------------------------------------------------------------------
/docs/node-version-audit-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/Args.js:
--------------------------------------------------------------------------------
1 | const { Logger } = require('./Logger');
2 | const { InvalidArgumentException, InvalidVersionException } = require('./Exceptions');
3 |
4 | function Args() {
5 | // simple constructor
6 | }
7 |
8 | /**
9 | * @return {number}
10 | */
11 | Args.prototype.getVerbosity = function () {
12 | if (this[Args.OPTIONS.V_DEBUG]) {
13 | return Logger.DEBUG;
14 | }
15 | if (this[Args.OPTIONS.V_INFO]) {
16 | return Logger.INFO;
17 | }
18 | if (this[Args.OPTIONS.V_WARNING]) {
19 | return Logger.WARNING;
20 | }
21 | if (this[Args.OPTIONS.SILENT]) {
22 | return Logger.SILENT;
23 | }
24 | return Logger.ERROR;
25 | };
26 |
27 | Args.OPTIONS = {
28 | NODE_VERSION: 'version',
29 | HELP: 'help',
30 | NO_UPDATE: 'no-update',
31 | FULL_UPDATE: 'full-update',
32 | FAIL_SECURITY: 'fail-security',
33 | FAIL_SUPPORT: 'fail-support',
34 | FAIL_PATCH: 'fail-patch',
35 | FAIL_MINOR: 'fail-minor',
36 | FAIL_LATEST: 'fail-latest',
37 | SILENT: 'silent',
38 | V_WARNING: 'v',
39 | V_INFO: 'vv',
40 | V_DEBUG: 'vvv',
41 | };
42 |
43 | Args[Args.OPTIONS.NODE_VERSION] = null;
44 | Args[Args.OPTIONS.HELP] = false;
45 | Args[Args.OPTIONS.NO_UPDATE] = false;
46 | Args[Args.OPTIONS.FULL_UPDATE] = false;
47 | Args[Args.OPTIONS.FAIL_SECURITY] = false;
48 | Args[Args.OPTIONS.FAIL_SUPPORT] = false;
49 | Args[Args.OPTIONS.FAIL_PATCH] = false;
50 | Args[Args.OPTIONS.FAIL_MINOR] = false;
51 | Args[Args.OPTIONS.FAIL_LATEST] = false;
52 | Args[Args.OPTIONS.SILENT] = false;
53 | Args[Args.OPTIONS.V_WARNING] = false;
54 | Args[Args.OPTIONS.V_INFO] = false;
55 | Args[Args.OPTIONS.V_DEBUG] = false;
56 |
57 | /**
58 | * @param {string[]} argv
59 | * @return {Args}
60 | */
61 | Args.parseArgs = function (argv) {
62 | const args = new Args();
63 |
64 | for (let i = 2; i < argv.length; i++) {
65 | let arg = argv[i];
66 | if (!arg.startsWith('--')) {
67 | throw InvalidArgumentException.fromString(`Invalid argument: '${arg}'`);
68 | }
69 | arg = arg.replace('--', '');
70 |
71 | if (arg.startsWith(Args.OPTIONS.NODE_VERSION)) {
72 | const split = arg.split('=');
73 | if (split.length !== 2 || !split[1] || split[1].length < 5) {
74 | throw InvalidArgumentException.fromString(`Version arg must be in format --version=1.2.3`);
75 | }
76 | args[Args.OPTIONS.NODE_VERSION] = split[1];
77 | continue;
78 | }
79 |
80 | if (arg in Args) {
81 | args[arg] = true;
82 | continue;
83 | }
84 | throw InvalidArgumentException.fromString(`Invalid argument: '${arg}'`);
85 | }
86 |
87 | if (!args[Args.OPTIONS.NODE_VERSION]) {
88 | if (process.env.NVA_REQUIRE_VERSION_ARG === 'true') {
89 | throw InvalidArgumentException.fromString('Missing required argument: --version');
90 | }
91 | args[Args.OPTIONS.NODE_VERSION] = process.versions.node;
92 | }
93 |
94 | return args;
95 | };
96 |
97 | module.exports = {
98 | Args,
99 | };
100 |
--------------------------------------------------------------------------------
/lib/AuditResults.js:
--------------------------------------------------------------------------------
1 | const { SUPPORT_TYPE } = require('./SupportSchedule');
2 | const { NodeVersion } = require('./NodeVersion');
3 |
4 | /**
5 | * @constructor
6 | */
7 | function AuditResults() {
8 | this.auditVersion = null;
9 | this.supportType = null;
10 | this.latestPatchVersion = null;
11 | this.latestMinorVersion = null;
12 | this.latestVersion = null;
13 | this.activeSupportEndDate = null;
14 | this.supportEndDate = null;
15 | this.rulesLastUpdatedDate = null;
16 | this.vulnerabilities = null;
17 | }
18 |
19 | AuditResults.prototype.hasVulnerabilities = function () {
20 | return this.vulnerabilities && Object.keys(this.vulnerabilities).length > 0;
21 | };
22 |
23 | AuditResults.prototype.hasSupport = function () {
24 | return this.supportType && this.supportType !== SUPPORT_TYPE.NONE;
25 | };
26 |
27 | AuditResults.prototype.isLatestPatchVersion = function () {
28 | return NodeVersion.compare(this.auditVersion, this.latestPatchVersion) === 0;
29 | };
30 |
31 | AuditResults.prototype.isLatestMinorVersion = function () {
32 | return NodeVersion.compare(this.auditVersion, this.latestMinorVersion) === 0;
33 | };
34 |
35 | AuditResults.prototype.isLatestVersion = function () {
36 | return NodeVersion.compare(this.latestVersion, this.auditVersion) === 0;
37 | };
38 |
39 | AuditResults.prototype.toJSON = function () {
40 | const self = this;
41 | return {
42 | auditVersion: self.auditVersion,
43 | hasVulnerabilities: self.hasVulnerabilities(),
44 | hasSupport: self.hasSupport(),
45 | supportType: self.supportType,
46 | isLatestPatchVersion: self.isLatestPatchVersion(),
47 | isLatestMinorVersion: self.isLatestMinorVersion(),
48 | isLatestVersion: self.isLatestVersion(),
49 | latestPatchVersion: self.latestPatchVersion,
50 | latestMinorVersion: self.latestMinorVersion,
51 | latestVersion: self.latestVersion,
52 | activeSupportEndDate: self.activeSupportEndDate,
53 | supportEndDate: self.supportEndDate,
54 | rulesLastUpdatedDate: self.rulesLastUpdatedDate,
55 | vulnerabilities: self.vulnerabilities,
56 | };
57 | };
58 |
59 | module.exports = {
60 | AuditResults,
61 | };
62 |
--------------------------------------------------------------------------------
/lib/CachedDownload.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 | const util = require('util');
4 | const https = require('https');
5 | const crypto = require('crypto');
6 | const zlib = require('zlib');
7 | const { Logger } = require('./Logger');
8 | const { DownloadException } = require('./Exceptions');
9 | const mkdir = util.promisify(fs.mkdir);
10 | const writeFile = util.promisify(fs.writeFile);
11 | const readFile = util.promisify(fs.readFile);
12 | const gunzip = util.promisify(zlib.gunzip);
13 |
14 | const INDEX_FILE_NAME = process.env.NVA_INDEX_FILE_NAME || 'index.json';
15 | let indexFileCache = null;
16 |
17 | const CachedDownload = {};
18 |
19 | /**
20 | * @param url
21 | * @return {Promise}
22 | * @throws DownloadException
23 | */
24 | CachedDownload.json = async function (url) {
25 | const response = await CachedDownload.download(url);
26 | try {
27 | return JSON.parse(response);
28 | } catch (e) {
29 | throw DownloadException.fromString(`Unable to parse expected JSON download file: ${url}: ${response}`);
30 | }
31 | };
32 |
33 | /**
34 | * @param {string }url
35 | * @return {Promise}
36 | * @throws DownloadException
37 | */
38 | CachedDownload.download = async function (url) {
39 | await setup();
40 | return downloadCachedFile(url);
41 | };
42 |
43 | /**
44 | * @param {string} url
45 | * @return {Promise}
46 | */
47 | async function downloadCachedFile(url) {
48 | if (await isCached(url)) {
49 | return getFileFromCache(url);
50 | }
51 | const data = await downloadFile(url);
52 | await writeCacheFile(url, data);
53 | return data;
54 | }
55 |
56 | /**
57 | * @param {string} url
58 | * @return {Promise}
59 | */
60 | function downloadFile(url) {
61 | Logger.debug('Downloading: ', url);
62 | return new Promise((resolve, reject) => {
63 | const headers = getHeaders(url);
64 | const req = https.request(url, { timeout: 10000, headers: headers }, (res) => {
65 | if (res.statusCode !== 200) {
66 | const e = DownloadException.fromString(`Error downloading file from ${url}: ${res.statusCode}`);
67 | Logger.error(e);
68 | return reject(e);
69 | }
70 | const body = [];
71 | res.on('data', (chunk) => {
72 | body.push(chunk);
73 | });
74 | res.on('end', () => {
75 | try {
76 | const buffer = Buffer.concat(body);
77 | if (url.endsWith('gz')) {
78 | gunzip(buffer).then((unzipped) => {
79 | resolve(unzipped.toString());
80 | });
81 | } else {
82 | resolve(buffer.toString());
83 | }
84 | } catch (e) {
85 | Logger.error(e);
86 | reject(DownloadException.fromException(e));
87 | }
88 | });
89 | });
90 | req.on('error', (errorEvent) => {
91 | const e = DownloadException.fromString(`Error downloading file from ${url}: ${errorEvent}`);
92 | Logger.error(e);
93 | return reject(e);
94 | });
95 | req.end();
96 | });
97 | }
98 |
99 | /**
100 | * @param {string} url
101 | * @return {{string}}
102 | */
103 | function getHeaders(url) {
104 | const headers = {
105 | 'user-agent': 'node-version-audit/1',
106 | accept: '*/*',
107 | };
108 | const hostname = new URL(url).hostname;
109 | if (hostname === 'api.github.com') {
110 | headers.accept = 'application/vnd.github+json';
111 | headers['X-GitHub-Api-Version'] = '2022-11-28';
112 | if (process.env.GITHUB_TOKEN) {
113 | headers['Authorization'] = `Bearer ${process.env.GITHUB_TOKEN}`;
114 | }
115 | }
116 | return headers;
117 | }
118 |
119 | /**
120 | *
121 | * @param {string} url
122 | * @param {string} data
123 | * @return {Promise}
124 | */
125 | async function writeCacheFile(url, data) {
126 | Logger.debug('Writing file cache: ', url);
127 | const cacheIndex = await getCacheIndex();
128 | const filename = urlToFileName(url);
129 | cacheIndex[url] = {
130 | filename: filename,
131 | lastModifiedDate: new Date().toISOString(),
132 | };
133 | await writeFile(getCachePath(filename), data);
134 | return saveCacheIndex(cacheIndex);
135 | }
136 |
137 | /**
138 | * @param {string} url
139 | * @return {Promise}
140 | */
141 | async function getFileFromCache(url) {
142 | Logger.debug('Loading file from cache: ', url);
143 | const filename = urlToFileName(url);
144 | const fullPath = getCachePath(filename);
145 | if (!fs.existsSync(fullPath)) {
146 | throw new Error('Cached file not found: ' + fullPath);
147 | }
148 | const buf = await readFile(fullPath);
149 | return buf.toString('utf8');
150 | }
151 |
152 | /**
153 | * @param {string} url
154 | * @return string
155 | */
156 | function urlToFileName(url) {
157 | const hash = crypto.createHash('sha256').update(url).digest('base64');
158 | return hash.replace(/[+/]/g, '').substring(0, 15) + '.txt';
159 | }
160 |
161 | /**
162 | * @param {string} url
163 | * @return {Promise}
164 | */
165 | async function isCached(url) {
166 | const cacheIndex = await getCacheIndex();
167 | if (!cacheIndex[url]) {
168 | Logger.debug('Cache does not exist for ', url);
169 | return false;
170 | }
171 | const lastModifiedDate = new Date(cacheIndex[url].lastModifiedDate);
172 | const expired = await isExpired(url, lastModifiedDate);
173 | if (expired) {
174 | Logger.debug('Cache has expired for ', url);
175 | } else {
176 | Logger.debug('Cache is valid for ', url);
177 | }
178 | return !expired;
179 | }
180 |
181 | /**
182 | * @param {string} url
183 | * @param {Date} lastModifiedDate
184 | * @return {Promise}
185 | */
186 | async function isExpired(url, lastModifiedDate) {
187 | const elapsedSeconds = (new Date().getTime() - lastModifiedDate.getTime()) / 1000;
188 | // enforce a minimum cache of 1 hour to makeup for lack of last modified time on changelog
189 | if (elapsedSeconds < 3600) {
190 | Logger.debug('Cache time under 3600: ', url);
191 | return false;
192 | }
193 | const serverLastModifiedDate = await getServerLastModifiedDate(url);
194 | return serverLastModifiedDate.getTime() > lastModifiedDate.getTime();
195 | }
196 |
197 | /**
198 | * @param {string} url
199 | * @return {Promise}
200 | */
201 | function getServerLastModifiedDate(url) {
202 | return new Promise((resolve, reject) => {
203 | const headers = {
204 | 'user-agent': 'node-version-audit/1',
205 | accept: '*/*',
206 | };
207 | const req = https.request(url, { method: 'HEAD', timeout: 10000, headers: headers }, (res) => {
208 | if (res.headers['last-modified']) {
209 | return resolve(new Date(res.headers['last-modified']));
210 | }
211 | resolve(new Date());
212 | });
213 | req.on('error', reject);
214 | req.end();
215 | });
216 | }
217 |
218 | /**
219 | * @return {Promise}
220 | */
221 | async function setup() {
222 | const tempDir = getCachePath();
223 | if (!fs.existsSync(tempDir)) {
224 | await mkdir(tempDir);
225 | }
226 | const indexPath = getCachePath(INDEX_FILE_NAME);
227 | if (!fs.existsSync(indexPath)) {
228 | Logger.debug('Cache index not found, creating new one.');
229 | saveCacheIndex({});
230 | }
231 | }
232 |
233 | /**
234 | * @param {object} index
235 | * @return {void}
236 | */
237 | function saveCacheIndex(index) {
238 | indexFileCache = index;
239 | const fullPath = getCachePath(INDEX_FILE_NAME);
240 | const data = JSON.stringify(index, null, 4);
241 | fs.writeFileSync(fullPath, data);
242 | }
243 |
244 | /**
245 | * @return {Promise}
246 | */
247 | async function getCacheIndex() {
248 | if (indexFileCache) {
249 | return indexFileCache;
250 | }
251 | const fullPath = getCachePath(INDEX_FILE_NAME);
252 | const buf = fs.readFileSync(fullPath);
253 | const fileContent = buf.toString('utf8');
254 | try {
255 | return JSON.parse(fileContent);
256 | } catch (e) {
257 | Logger.warning('Corrupted cache index:', fileContent);
258 | saveCacheIndex({});
259 | return {};
260 | }
261 | }
262 |
263 | /**
264 | * @param {string?} filename
265 | * @return {string}
266 | */
267 | function getCachePath(filename = '') {
268 | return path.join(__dirname, '..', 'tmp', filename);
269 | }
270 |
271 | module.exports = {
272 | CachedDownload,
273 | };
274 |
--------------------------------------------------------------------------------
/lib/Changelogs.js:
--------------------------------------------------------------------------------
1 | const { CachedDownload } = require('./CachedDownload');
2 | const { CveId } = require('./CveId');
3 | const { NodeVersion } = require('./NodeVersion');
4 | const { NodeRelease } = require('./NodeRelease');
5 | const { Logger } = require('./Logger');
6 |
7 | const CHANGELOGS_URL = 'https://api.github.com/repos/nodejs/node/contents/doc/changelogs';
8 |
9 | const Changelogs = {};
10 |
11 | /**
12 | *
13 | * @return {Promise<{string: NodeRelease}>}
14 | */
15 | Changelogs.parse = async function () {
16 | const changelogUrls = await Changelogs.getChangelogUrls();
17 | const fullChangelog = (await Promise.all(changelogUrls.map(CachedDownload.download))).join('\n');
18 | return Changelogs.parseChangelog(fullChangelog);
19 | };
20 |
21 | /**
22 | * @return {Promise}
23 | */
24 | Changelogs.getChangelogUrls = async function () {
25 | const folderContent = await CachedDownload.json(CHANGELOGS_URL);
26 | // Accept only file names matching CHANGELOG_VXX or CHANGELOG_IOJS.md
27 | const filenamePattern = new RegExp(/^CHANGELOG_(V\d+|IOJS)\.md$/);
28 | return folderContent.reduce((urls, fileOrFolder) => {
29 | if (fileOrFolder.type === 'file' && filenamePattern.test(fileOrFolder.name)) {
30 | Logger.debug('Changelog URL found', fileOrFolder.name);
31 | urls.push(fileOrFolder.download_url);
32 | } else {
33 | Logger.debug('Excluding changelog', fileOrFolder.type, fileOrFolder.name);
34 | }
35 | return urls;
36 | }, []);
37 | };
38 |
39 | /**
40 | * @param {string} changelog
41 | * @return {{string: NodeRelease}}
42 | */
43 | Changelogs.parseChangelog = function (changelog) {
44 | let releases = [];
45 | let currentRelease = null;
46 | const versionBlockMatch = new RegExp(/<\/a>$/);
47 | for (const line of changelog.split('\n')) {
48 | const matches = versionBlockMatch.exec(line);
49 | if (matches && matches.length === 2) {
50 | const foundVersion = NodeVersion.fromString(matches[1]);
51 | currentRelease = new NodeRelease(foundVersion);
52 | releases.push(currentRelease);
53 | continue;
54 | }
55 | if (!currentRelease) {
56 | continue;
57 | }
58 | if (!currentRelease.releaseDate) {
59 | const releaseDateMatch = line.match(/## (\d{4}-\d{1,2}-\d{1,2}),.+Version.+/i);
60 | if (releaseDateMatch && releaseDateMatch.length === 2) {
61 | currentRelease.releaseDate = new Date(releaseDateMatch[1]);
62 | continue;
63 | }
64 | }
65 | const cveMatches = line.matchAll(/(CVE-\d{4}-\d+)/gi);
66 | for (const cveMatch of cveMatches) {
67 | currentRelease.addPatchedCveId(CveId.fromString(cveMatch[1]));
68 | }
69 | }
70 | releases = releases.sort((a, b) => {
71 | return NodeVersion.compare(a.version, b.version);
72 | });
73 | return releases.reduce((releasesByKey, release) => {
74 | releasesByKey[release.version.toString()] = release;
75 | return releasesByKey;
76 | }, {});
77 | };
78 |
79 | module.exports = {
80 | Changelogs,
81 | };
82 |
--------------------------------------------------------------------------------
/lib/Cli.js:
--------------------------------------------------------------------------------
1 | const {
2 | InvalidArgumentException,
3 | InvalidVersionException,
4 | StaleRulesException,
5 | InvalidRuntimeException,
6 | } = require('./Exceptions');
7 | const { EOL } = require('os');
8 | const util = require('util');
9 | const { NodeVersionAudit } = require('./NodeVersionAudit');
10 | const { Args } = require('./Args');
11 | const { SUPPORT_TYPE } = require('./SupportSchedule');
12 | const { Logger } = require('./Logger');
13 |
14 | const EXIT_CODES = {
15 | FAIL_SECURITY: 10,
16 | FAIL_SUPPORT: 20,
17 | FAIL_PATCH: 30,
18 | FAIL_MINOR: 40,
19 | FAIL_LATEST: 50,
20 | FAIL_STALE: 100,
21 | INVALID_ARG: 110,
22 | INVALID_VERSION: 120,
23 | INVALID_RUNTIME: 130,
24 | };
25 |
26 | /**
27 | * @param {string[]} argv
28 | * @constructor
29 | */
30 | function Cli(argv) {
31 | try {
32 | this.args = Args.parseArgs(argv);
33 | Logger.setVerbosity(this.args.getVerbosity());
34 | } catch (e) {
35 | if (e instanceof InvalidArgumentException) {
36 | Cli.showHelp();
37 | Logger.error(e);
38 | process.exit(EXIT_CODES.INVALID_ARG);
39 | }
40 | throw e;
41 | }
42 | }
43 |
44 | /**
45 | * @return {Promise}
46 | */
47 | Cli.prototype.run = async function () {
48 | if (this.args[Args.OPTIONS.HELP]) {
49 | Cli.showHelp();
50 | return process.exit(0);
51 | }
52 |
53 | let app;
54 | try {
55 | app = new NodeVersionAudit(this.args[Args.OPTIONS.NODE_VERSION], !this.args[Args.OPTIONS.NO_UPDATE]);
56 | } catch (e) {
57 | if (e instanceof InvalidVersionException) {
58 | Logger.error(e);
59 | Cli.showHelp();
60 | return process.exit(EXIT_CODES.INVALID_VERSION);
61 | }
62 | if (e instanceof InvalidRuntimeException) {
63 | Logger.error(e);
64 | return process.exit(EXIT_CODES.INVALID_RUNTIME);
65 | }
66 | throw e;
67 | }
68 |
69 | if (this.args[Args.OPTIONS.FULL_UPDATE]) {
70 | /**
71 | * PLEASE DO NOT USE THIS. This function is intended to only be used internally for updating
72 | * project rules in github, which can then be accessed by ALL instances of Node Version Audit.
73 | * Running it locally puts unnecessary load on the source servers and cannot be re-used by others.
74 | *
75 | * The github hosted rules are setup on a cron schedule to update regularly.
76 | * Running it directly will not provide you with any new information and will only
77 | * waste time and server resources.
78 | */
79 | await app.fullRulesUpdate();
80 | }
81 |
82 | try {
83 | const auditDetails = await app.getAllAuditResults();
84 | console.log(JSON.stringify(auditDetails, null, 4));
85 | this.setExitCode(auditDetails);
86 | return auditDetails;
87 | } catch (e) {
88 | if (e instanceof StaleRulesException) {
89 | Logger.error('Rules are stale: ', e);
90 | return process.exit(EXIT_CODES.FAIL_STALE);
91 | }
92 | Logger.error(e);
93 | }
94 | };
95 |
96 | /**
97 | * @param {AuditResults} auditResults
98 | */
99 | Cli.prototype.setExitCode = function (auditResults) {
100 | if (this.args[Args.OPTIONS.FAIL_SECURITY] && (auditResults.hasVulnerabilities() || !auditResults.hasSupport())) {
101 | process.exitCode = EXIT_CODES.FAIL_SECURITY;
102 | } else if (
103 | this.args[Args.OPTIONS.FAIL_SUPPORT] &&
104 | (!auditResults.hasSupport() || ![SUPPORT_TYPE.CURRENT, SUPPORT_TYPE.ACTIVE].includes(auditResults.supportType))
105 | ) {
106 | process.exitCode = EXIT_CODES.FAIL_SUPPORT;
107 | } else if (this.args[Args.OPTIONS.FAIL_LATEST] && !auditResults.isLatestVersion()) {
108 | process.exitCode = EXIT_CODES.FAIL_LATEST;
109 | } else if (this.args[Args.OPTIONS.FAIL_MINOR] && !auditResults.isLatestMinorVersion()) {
110 | process.exitCode = EXIT_CODES.FAIL_MINOR;
111 | } else if (this.args[Args.OPTIONS.FAIL_PATCH] && !auditResults.isLatestPatchVersion()) {
112 | process.exitCode = EXIT_CODES.FAIL_PATCH;
113 | }
114 | };
115 |
116 | Cli.showHelp = function () {
117 | const usageMask = `\t\t\t\t[--%s] [--%s]${EOL}`;
118 | const argsMask = `--%s\t\t\t%s${EOL}`;
119 | const argsErrorCodeMask = `--%s\t\t\tgenerate a %s %s${EOL}`;
120 | let out = util.format(`%s${EOL}`, 'Node Version Audit');
121 | out += util.format(
122 | `%s\t%s${EOL}`,
123 | 'usage: node-version-audit',
124 | `[--help] [--${Args.OPTIONS.NODE_VERSION}=NODE_VERSION]`,
125 | );
126 | out += util.format(usageMask, Args.OPTIONS.FAIL_SECURITY, Args.OPTIONS.FAIL_SUPPORT);
127 | out += util.format(usageMask, Args.OPTIONS.FAIL_PATCH, Args.OPTIONS.FAIL_LATEST);
128 | out += util.format(usageMask, Args.OPTIONS.NO_UPDATE, 'silent');
129 | out += util.format(`\t\t\t\t[--%s]${EOL}`, 'v');
130 | out += util.format(`%s${EOL}`, 'optional arguments:');
131 | out += util.format(argsMask, Args.OPTIONS.HELP, '\tshow this help message and exit.');
132 | out += util.format(
133 | argsMask,
134 | Args.OPTIONS.NODE_VERSION,
135 | 'set the Node Version to run against. Defaults to the runtime version. This is required when running with docker.',
136 | );
137 | out += util.format(
138 | argsErrorCodeMask,
139 | Args.OPTIONS.FAIL_SECURITY,
140 | EXIT_CODES.FAIL_SECURITY,
141 | 'exit code if any CVEs are found, or security support has ended.',
142 | );
143 | out += util.format(
144 | argsErrorCodeMask,
145 | Args.OPTIONS.FAIL_SUPPORT,
146 | EXIT_CODES.FAIL_SUPPORT,
147 | 'exit code if the version of Node no longer gets active (bug) support.',
148 | );
149 | out += util.format(
150 | argsErrorCodeMask,
151 | Args.OPTIONS.FAIL_PATCH,
152 | EXIT_CODES.FAIL_PATCH,
153 | 'exit code if there is a newer patch-level release.',
154 | );
155 | out += util.format(
156 | argsErrorCodeMask,
157 | Args.OPTIONS.FAIL_MINOR,
158 | EXIT_CODES.FAIL_MINOR,
159 | 'exit code if there is a newer minor-level release.',
160 | );
161 | out += util.format(
162 | argsErrorCodeMask,
163 | Args.OPTIONS.FAIL_LATEST,
164 | EXIT_CODES.FAIL_LATEST,
165 | 'exit code if there is a newer release.',
166 | );
167 | out += util.format(argsMask, Args.OPTIONS.NO_UPDATE, 'do not download the latest rules. NOT RECOMMENDED!');
168 | out += util.format(argsMask, 'silent', 'do not write any error messages to STDERR.');
169 | out += util.format(
170 | argsMask,
171 | 'v',
172 | '\tSet verbosity. v=warnings, vv=info, vvv=debug. Default is error. All logging writes to STDERR.',
173 | );
174 | console.info(out);
175 | };
176 |
177 | module.exports = {
178 | Cli,
179 | };
180 |
--------------------------------------------------------------------------------
/lib/CveDetails.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {CveId} id
3 | * @param {?number} baseScore
4 | * @param {?string} publishedDate
5 | * @param {?string} lastModifiedDate
6 | * @param {?string} description
7 | * @constructor
8 | */
9 | const { CveId } = require('./CveId');
10 |
11 | function CveDetails(id, baseScore, publishedDate, lastModifiedDate, description) {
12 | this.id = id;
13 | this.baseScore = baseScore;
14 | this.publishedDate = publishedDate;
15 | this.lastModifiedDate = lastModifiedDate;
16 | this.description = description;
17 | }
18 |
19 | /**
20 | * @param {CveDetails} first
21 | * @param {CveDetails} second
22 | */
23 | CveDetails.compare = (first, second) => {
24 | return CveId.compare(first.id, second.id);
25 | };
26 |
27 | module.exports = {
28 | CveDetails,
29 | };
30 |
--------------------------------------------------------------------------------
/lib/CveFeed.js:
--------------------------------------------------------------------------------
1 | const { CachedDownload } = require('./CachedDownload');
2 | const { ParseException } = require('./Exceptions');
3 | const { CveId } = require('./CveId');
4 | const { CveDetails } = require('./CveDetails');
5 | const { Logger } = require('./Logger');
6 |
7 | const CVE_START_YEAR = 2013;
8 |
9 | const CveFeed = {};
10 |
11 | /**
12 | * @param {CveId[]} cveIds
13 | * @return {Promise<{string: CveDetails}>}
14 | */
15 | CveFeed.parse = async (cveIds) => {
16 | const feedNames = ['modified', 'recent'];
17 | const currentYear = new Date().getFullYear();
18 | for (let year = CVE_START_YEAR; year <= currentYear; year++) {
19 | feedNames.push(year.toString());
20 | }
21 | let cveDetails = [];
22 | for (let feedName of feedNames) {
23 | let cveFeed = await CveFeed._downloadFeed(feedName);
24 | Logger.info('Beginning NVD feed parse: ', feedName);
25 | cveDetails = cveDetails.concat(CveFeed._parseFeed(cveIds, cveFeed));
26 | }
27 | cveDetails.sort(CveDetails.compare);
28 | return cveDetails.reduce((sortedCveDetails, cveDetail) => {
29 | if (!sortedCveDetails[cveDetail.id.toString()]) {
30 | sortedCveDetails[cveDetail.id.toString()] = cveDetail;
31 | }
32 | return sortedCveDetails;
33 | }, {});
34 | };
35 |
36 | /**
37 | * @param {CveId[]} cveIds
38 | * @param {*} cveFeed
39 | * @return {CveDetails[]}
40 | * @private
41 | */
42 | CveFeed._parseFeed = function (cveIds, cveFeed) {
43 | const cveDetails = [];
44 | const cvesSet = new Set(cveIds.map((cve) => cve.toString()));
45 |
46 | for (let cveItem of cveFeed.CVE_Items) {
47 | const cve = CveFeed._parseCveItem(cveItem);
48 | if (cve && cvesSet.has(cve.id.toString())) {
49 | cveDetails.push(cve);
50 | cvesSet.delete(cve.id.toString());
51 | }
52 | }
53 | return cveDetails;
54 | };
55 |
56 | /**
57 | * @param {string} feedName
58 | * @throws {ParseException}
59 | * @return {*}
60 | */
61 | CveFeed._downloadFeed = async function (feedName) {
62 | try {
63 | return await CachedDownload.json(`https://nvd.nist.gov/feeds/json/cve/1.1/nvdcve-1.1-${feedName}.json.gz`);
64 | } catch (ex) {
65 | const currentYear = new Date().getFullYear();
66 | const currentMonth = new Date().getMonth();
67 | if (feedName === currentYear.toString() && currentMonth === 0) {
68 | Logger.warn(`Unable to download feed ${feedName}. Skipping due to beginning of the year.`);
69 | return {
70 | CVE_Items: [],
71 | };
72 | } else {
73 | throw ParseException.fromException(ex);
74 | }
75 | }
76 | };
77 |
78 | /**
79 | * @param cveItem
80 | * @return {null|CveDetails}
81 | */
82 | CveFeed._parseCveItem = function (cveItem) {
83 | let id = null;
84 | try {
85 | id = CveId.fromString(cveItem.cve.CVE_data_meta.ID);
86 | } catch (e) {
87 | return null;
88 | }
89 | const publishedDate = new Date(cveItem.publishedDate).toISOString();
90 | const lastModifiedDate = new Date(cveItem.lastModifiedDate).toISOString();
91 | let description = null;
92 | let baseScore = null;
93 | if (cveItem.cve.description.description_data) {
94 | for (let descriptionLang of cveItem.cve.description.description_data) {
95 | if (descriptionLang.lang === 'en') {
96 | description = descriptionLang.value;
97 | break;
98 | }
99 | }
100 | }
101 |
102 | if (cveItem.impact.baseMetricV3 && cveItem.impact.baseMetricV3.cvssV3.baseScore) {
103 | baseScore = cveItem.impact.baseMetricV3.cvssV3.baseScore;
104 | } else if (cveItem.impact.baseMetricV2 && cveItem.impact.baseMetricV2.cvssV2.baseScore) {
105 | baseScore = cveItem.impact.baseMetricV2.cvssV2.baseScore;
106 | }
107 | return new CveDetails(id, baseScore, publishedDate, lastModifiedDate, description);
108 | };
109 |
110 | module.exports = {
111 | CveFeed,
112 | };
113 |
--------------------------------------------------------------------------------
/lib/CveId.js:
--------------------------------------------------------------------------------
1 | const { ParseException } = require('./Exceptions');
2 |
3 | /**
4 | * @param {string} id
5 | * @constructor
6 | * @private
7 | */
8 | function CveId(id) {
9 | const matches = id.match(/CVE-(\d{4})-(\d+)/i);
10 | this.year = parseInt(matches[1]);
11 | this.sequenceNumber = parseInt(matches[2]);
12 | this.id = `CVE-${this.year}-${this.sequenceNumber}`;
13 | }
14 |
15 | /**
16 | * @param {string} cveId
17 | * @throws {ParseException}
18 | * @return {CveId}
19 | */
20 | CveId.fromString = (cveId) => {
21 | if (/CVE-(\d{4})-(\d+)/i.test(cveId)) {
22 | return new CveId(cveId);
23 | }
24 | throw ParseException.fromString(`Unable to parse CVE: ${cveId}`);
25 | };
26 |
27 | /**
28 | * @param {CveId} first
29 | * @param {CveId} second
30 | * @return {number} difference
31 | */
32 | CveId.compare = (first, second) => {
33 | if (!first && !second) {
34 | return 0;
35 | }
36 | if (!first) {
37 | return -1;
38 | }
39 | if (!second) {
40 | return 1;
41 | }
42 | if (first.year !== second.year) {
43 | return first.year - second.year;
44 | }
45 | return first.sequenceNumber - second.sequenceNumber;
46 | };
47 |
48 | /**
49 | * @return {string}
50 | */
51 | CveId.prototype.toJSON = function () {
52 | return this.toString();
53 | };
54 |
55 | /**
56 | * @return {string}
57 | */
58 | CveId.prototype.toString = function () {
59 | return this.id;
60 | };
61 |
62 | module.exports = {
63 | CveId,
64 | };
65 |
--------------------------------------------------------------------------------
/lib/Exceptions.js:
--------------------------------------------------------------------------------
1 | class ParseException extends Error {
2 | /**
3 | * @param {string} message
4 | * @return {ParseException}
5 | */
6 | static fromString(message) {
7 | return new ParseException(message);
8 | }
9 |
10 | /**
11 | * @param {Error} exception
12 | * @return {ParseException}
13 | */
14 | static fromException(exception) {
15 | return new ParseException(exception.message);
16 | }
17 | }
18 |
19 | class DownloadException extends Error {
20 | /**
21 | * @param {string} message
22 | * @return {DownloadException}
23 | */
24 | static fromString(message) {
25 | return new DownloadException(message);
26 | }
27 |
28 | /**
29 | * @param {Error} exception
30 | * @return {DownloadException}
31 | */
32 | static fromException(exception) {
33 | return new DownloadException(exception.message);
34 | }
35 | }
36 |
37 | class StaleRulesException extends Error {
38 | /**
39 | * @param {string} message
40 | * @return {StaleRulesException}
41 | */
42 | static fromString(message) {
43 | return new StaleRulesException(message);
44 | }
45 | }
46 |
47 | class InvalidArgumentException extends Error {
48 | /**
49 | * @param {string} message
50 | * @return {InvalidArgumentException}
51 | */
52 | static fromString(message) {
53 | return new InvalidArgumentException(message);
54 | }
55 | }
56 |
57 | class InvalidVersionException extends Error {
58 | /**
59 | * @param {?string} version
60 | * @return {InvalidVersionException}
61 | */
62 | static fromString(version) {
63 | return new InvalidVersionException(`Node.js version [${version}] is not valid`);
64 | }
65 | }
66 |
67 | class UnknownVersionException extends Error {
68 | /**
69 | * @param {?string} version
70 | * @return {UnknownVersionException}
71 | */
72 | static fromString(version) {
73 | return new UnknownVersionException(`Unknown Node.js version [${version}], perhaps rules are stale.`);
74 | }
75 | }
76 |
77 | class InvalidRuntimeException extends Error {
78 | /**
79 | * @param {NodeVersion} version
80 | * @returns {InvalidRuntimeException}
81 | */
82 | static fromVersion(version) {
83 | return new InvalidRuntimeException(
84 | `Node.js runtime ${version} is unsupported, please update the runtime Node.js version or use Docker.`,
85 | );
86 | }
87 | }
88 |
89 | module.exports = {
90 | ParseException,
91 | DownloadException,
92 | StaleRulesException,
93 | InvalidArgumentException,
94 | InvalidVersionException,
95 | UnknownVersionException,
96 | InvalidRuntimeException,
97 | };
98 |
--------------------------------------------------------------------------------
/lib/Logger.js:
--------------------------------------------------------------------------------
1 | let verbosity;
2 |
3 | function Logger() {
4 | // simple constructor
5 | }
6 |
7 | Logger.SILENT = 0;
8 | Logger.ERROR = 1;
9 | Logger.WARNING = 2;
10 | Logger.INFO = 3;
11 | Logger.DEBUG = 4;
12 |
13 | /**
14 | * @param {number} newVerbosity
15 | */
16 | Logger.setVerbosity = function (newVerbosity) {
17 | verbosity = newVerbosity;
18 | };
19 |
20 | /**
21 | * @param {...*} logMessages
22 | */
23 | Logger.error = function (...logMessages) {
24 | log(Logger.ERROR, 'error', logMessages);
25 | };
26 |
27 | /**
28 | * @param {...*} logMessages
29 | */
30 | Logger.warning = function (...logMessages) {
31 | log(Logger.WARNING, 'warning', logMessages);
32 | };
33 |
34 | /**
35 | * @param {...*} logMessages
36 | */
37 | Logger.info = function (...logMessages) {
38 | log(Logger.INFO, 'info', logMessages);
39 | };
40 |
41 | /**
42 | * @param {...*} logMessages
43 | */
44 | Logger.debug = function (...logMessages) {
45 | log(Logger.DEBUG, 'debug', logMessages);
46 | };
47 |
48 | /**
49 | * @return {number}
50 | */
51 | Logger.getVerbosity = function () {
52 | if (verbosity || verbosity === 0) {
53 | return verbosity;
54 | }
55 | return Logger.ERROR;
56 | };
57 |
58 | /**
59 | * @param {number} levelCode
60 | * @param {string} levelName
61 | * @param {array} logArgs
62 | */
63 | function log(levelCode, levelName, logArgs) {
64 | if (Logger.getVerbosity() < levelCode) {
65 | return;
66 | }
67 | const logEvent = {
68 | level: levelName,
69 | time: new Date().toISOString(),
70 | logMessage: '',
71 | };
72 |
73 | for (const logArg of logArgs) {
74 | if (logEvent.logMessage.length > 0) {
75 | logEvent.logMessage += ' ';
76 | }
77 | if (typeof logArg === 'string') {
78 | logEvent.logMessage += logArg;
79 | } else if (logArg instanceof Error) {
80 | logEvent.logMessage += logArg.message;
81 | } else {
82 | logEvent.logMessage += JSON.stringify(logArg, null, 4);
83 | }
84 | }
85 |
86 | console.error(JSON.stringify(logEvent, null, 4));
87 | }
88 |
89 | module.exports = {
90 | Logger,
91 | };
92 |
--------------------------------------------------------------------------------
/lib/NodeRelease.js:
--------------------------------------------------------------------------------
1 | const { CveId } = require('./CveId');
2 | const { NodeVersion } = require('./NodeVersion');
3 |
4 | /**
5 | *
6 | * @param {NodeVersion} version
7 | * @constructor
8 | */
9 | function NodeRelease(version) {
10 | this.version = version;
11 | this.releaseDate = null;
12 | this.patchedCveIds = [];
13 | }
14 |
15 | /**
16 | * @param {CveId} newCveId
17 | */
18 | NodeRelease.prototype.addPatchedCveId = function (newCveId) {
19 | for (let i = 0; i < this.patchedCveIds.length; i++) {
20 | const difference = CveId.compare(this.patchedCveIds[i], newCveId);
21 | if (difference === 0) {
22 | return;
23 | } else if (difference > 0) {
24 | this.patchedCveIds.splice(i, 0, newCveId);
25 | return;
26 | }
27 | }
28 | this.patchedCveIds.push(newCveId);
29 | };
30 |
31 | /**
32 | * @param {NodeRelease} first
33 | * @param {NodeRelease} second
34 | * @return {number}
35 | */
36 | NodeRelease.compare = (first, second) => {
37 | return NodeVersion.compare(first.version, second.version);
38 | };
39 |
40 | module.exports = {
41 | NodeRelease,
42 | };
43 |
--------------------------------------------------------------------------------
/lib/NodeVersion.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {number} major
3 | * @param {number} minor
4 | * @param {number} patch
5 | * @constructor
6 | * @private
7 | */
8 | function NodeVersion(major, minor, patch) {
9 | this.major = major;
10 | this.minor = minor;
11 | this.patch = patch;
12 | }
13 |
14 | /**
15 | * Initialize from a string
16 | * @param {string|null} fullVersion
17 | * @returns {null|NodeVersion}
18 | */
19 | NodeVersion.fromString = (fullVersion) => {
20 | const matches = /^(\d+)\.(\d+)\.(\d+)$/g.exec(fullVersion);
21 | if (!matches || matches.length !== 4) {
22 | return null;
23 | }
24 | const major = parseInt(matches[1], 10);
25 | const minor = parseInt(matches[2], 10);
26 | const patch = parseInt(matches[3], 10);
27 | if ([major, minor, patch].includes(NaN)) {
28 | return null;
29 | }
30 | return new NodeVersion(major, minor, patch);
31 | };
32 |
33 | /**
34 | * @param {NodeVersion} first
35 | * @param {NodeVersion} second
36 | * @return {number} difference
37 | */
38 | NodeVersion.compare = function (first, second) {
39 | if (!first && !second) {
40 | return 0;
41 | }
42 | if (!first) {
43 | return -1;
44 | }
45 | if (!second) {
46 | return 1;
47 | }
48 | if (first.major !== second.major) {
49 | return first.major - second.major;
50 | }
51 | if (first.minor !== second.minor) {
52 | return first.minor - second.minor;
53 | }
54 | if (first.patch !== second.patch) {
55 | return first.patch - second.patch;
56 | }
57 | return 0;
58 | };
59 |
60 | NodeVersion.prototype.toJSON = function () {
61 | return this.toString();
62 | };
63 |
64 | NodeVersion.prototype.toString = function () {
65 | return `${this.major}.${this.minor}.${this.patch}`;
66 | };
67 |
68 | module.exports = {
69 | NodeVersion,
70 | };
71 |
--------------------------------------------------------------------------------
/lib/NodeVersionAudit.js:
--------------------------------------------------------------------------------
1 | const {
2 | StaleRulesException,
3 | InvalidVersionException,
4 | UnknownVersionException,
5 | InvalidRuntimeException,
6 | } = require('./Exceptions');
7 | const { Changelogs } = require('./Changelogs');
8 | const { Rules } = require('./Rules');
9 | const { NodeRelease } = require('./NodeRelease');
10 | const { NodeVersion } = require('./NodeVersion');
11 | const { SupportSchedule, SUPPORT_TYPE } = require('./SupportSchedule');
12 | const { CveFeed } = require('./CveFeed');
13 | const { AuditResults } = require('./AuditResults');
14 | const { SupportEndDate } = require('./SupportEndDate');
15 | const { Logger } = require('./Logger');
16 |
17 | /**
18 | * @param {string} auditVersion
19 | * @param {boolean} updateRules
20 | * @throws {InvalidRuntimeException}
21 | * @throws {InvalidVersionException}
22 | * @constructor
23 | */
24 | function NodeVersionAudit(auditVersion, updateRules) {
25 | this.auditVersion = NodeVersion.fromString(auditVersion);
26 | if (!this.auditVersion) {
27 | throw InvalidVersionException.fromString(auditVersion);
28 | }
29 | assertRuntimeVersion();
30 | this.rules = Rules.loadRules(updateRules);
31 | }
32 |
33 | /**
34 | * @return {Promise}
35 | */
36 | NodeVersionAudit.prototype.fullRulesUpdate = async function () {
37 | Logger.warning('Running full rules update, this is slow and should not be ran locally!');
38 | const releases = await Changelogs.parse();
39 | const uniqueCves = Object.values(releases).reduce(
40 | (dummyRelease, release) => {
41 | for (let cve of release.patchedCveIds) {
42 | dummyRelease.addPatchedCveId(cve);
43 | }
44 | return dummyRelease;
45 | },
46 | new NodeRelease(new NodeVersion(0, 0, 0)),
47 | ).patchedCveIds;
48 | const supportSchedule = await SupportSchedule.parse();
49 |
50 | const cves = await CveFeed.parse(uniqueCves);
51 |
52 | if (
53 | this.rules.releasesCount > Object.keys(releases).length ||
54 | this.rules.cveCount > Object.keys(cves).length ||
55 | this.rules.supportEndDates > Object.keys(supportSchedule).length
56 | ) {
57 | throw StaleRulesException.fromString('Updated rules failed to meet expected counts.');
58 | }
59 | await Rules.saveRules(releases, cves, supportSchedule);
60 | };
61 |
62 | /**
63 | * @return {Promise}
64 | */
65 | NodeVersionAudit.prototype.getAllAuditResults = async function () {
66 | this.rules = await this.rules;
67 | Rules.assertFreshRules(this.rules);
68 | const auditResults = new AuditResults();
69 | auditResults.auditVersion = this.auditVersion;
70 | auditResults.supportType = this.supportType();
71 | auditResults.latestPatchVersion = this.getLatestPatchVersion();
72 | auditResults.latestMinorVersion = this.getLatestMinorVersion();
73 | auditResults.latestVersion = this.getLatestVersion();
74 | auditResults.activeSupportEndDate = this.getActiveSupportEndDate();
75 | auditResults.supportEndDate = this.getSupportEndDate();
76 | auditResults.rulesLastUpdatedDate = this.rules.lastUpdatedDate;
77 | auditResults.vulnerabilities = await this.getVulnerabilities();
78 | return auditResults;
79 | };
80 |
81 | /**
82 | *
83 | * @return {Promise<{}>}
84 | */
85 | NodeVersionAudit.prototype.getVulnerabilities = async function () {
86 | const dummyRelease = new NodeRelease(new NodeVersion(0, 0, 0));
87 | const maxMinorVersion = new NodeVersion(this.auditVersion.major, 99999, 99999);
88 | for (let release of Object.values((await this.rules).releases)) {
89 | if (
90 | NodeVersion.compare(release.version, this.auditVersion) <= 0 ||
91 | NodeVersion.compare(release.version, maxMinorVersion) > 0
92 | ) {
93 | continue;
94 | }
95 | for (let cve of release.patchedCveIds) {
96 | dummyRelease.addPatchedCveId(cve);
97 | }
98 | }
99 |
100 | const vulnerabilities = {};
101 | for (let patchedCveId of dummyRelease.patchedCveIds) {
102 | const cveString = patchedCveId.toString();
103 | vulnerabilities[cveString] = null;
104 | if (this.rules.cves[cveString]) {
105 | vulnerabilities[cveString] = this.rules.cves[cveString];
106 | }
107 | }
108 | return vulnerabilities;
109 | };
110 |
111 | /**
112 | * @return {NodeVersion}
113 | */
114 | NodeVersionAudit.prototype.getLatestVersion = function () {
115 | if (!this.rules.latestVersion) {
116 | throw StaleRulesException.fromString('Latest Node.js version is unknown!');
117 | }
118 | return this.rules.latestVersion;
119 | };
120 |
121 | /**
122 | * @return {NodeVersion}
123 | */
124 | NodeVersionAudit.prototype.getLatestPatchVersion = function () {
125 | const majorAndMinor = `${this.auditVersion.major}.${this.auditVersion.minor}`;
126 | if (!this.rules.latestVersions[majorAndMinor]) {
127 | throw UnknownVersionException.fromString(this.auditVersion.toString());
128 | }
129 | const latestPatch = this.rules.latestVersions[majorAndMinor];
130 | if (NodeVersion.compare(this.auditVersion, latestPatch) > 0) {
131 | throw UnknownVersionException.fromString(this.auditVersion.toString());
132 | }
133 | return latestPatch;
134 | };
135 |
136 | /**
137 | * @return {NodeVersion}
138 | */
139 | NodeVersionAudit.prototype.getLatestMinorVersion = function () {
140 | const major = this.auditVersion.major;
141 | if (!this.rules.latestVersions[major]) {
142 | throw UnknownVersionException.fromString(this.auditVersion.toString());
143 | }
144 | return this.rules.latestVersions[major];
145 | };
146 |
147 | /**
148 | * @return {string}
149 | */
150 | NodeVersionAudit.prototype.supportType = function () {
151 | if (this.hasCurrentSupport()) {
152 | return SUPPORT_TYPE.CURRENT;
153 | }
154 | if (this.hasActiveLtsSupport()) {
155 | return SUPPORT_TYPE.ACTIVE;
156 | }
157 | if (this.hasMaintenanceSupport()) {
158 | return SUPPORT_TYPE.MAINTENANCE;
159 | }
160 | return SUPPORT_TYPE.NONE;
161 | };
162 |
163 | /**
164 | * @return {boolean}
165 | */
166 | NodeVersionAudit.prototype.hasCurrentSupport = function () {
167 | const supportDates = this.getSupportDates(this.auditVersion.major);
168 | const endDate = supportDates.lts || supportDates.maintenance;
169 | return nowBetweenDates(supportDates.start, endDate);
170 | };
171 |
172 | /**
173 | * @return {boolean}
174 | */
175 | NodeVersionAudit.prototype.hasActiveLtsSupport = function () {
176 | const supportDates = this.getSupportDates(this.auditVersion.major);
177 | if (!supportDates.lts) {
178 | return false;
179 | }
180 | return nowBetweenDates(supportDates.lts, supportDates.maintenance);
181 | };
182 |
183 | /**
184 | * @return {boolean}
185 | */
186 | NodeVersionAudit.prototype.hasMaintenanceSupport = function () {
187 | const supportDates = this.getSupportDates(this.auditVersion.major);
188 | return nowBetweenDates(supportDates.maintenance, supportDates.end);
189 | };
190 |
191 | /**
192 | * @return {Date|null}
193 | */
194 | NodeVersionAudit.prototype.getActiveSupportEndDate = function () {
195 | const supportDates = this.getSupportDates(this.auditVersion.major);
196 | return supportDates.maintenance;
197 | };
198 |
199 | /**
200 | * @return {Date|null}
201 | */
202 | NodeVersionAudit.prototype.getSupportEndDate = function () {
203 | const supportDates = this.getSupportDates(this.auditVersion.major);
204 | return supportDates.end;
205 | };
206 |
207 | /**
208 | * @param {number} major
209 | * @return {SupportEndDate}
210 | */
211 | NodeVersionAudit.prototype.getSupportDates = function (major) {
212 | const supportDates = this.rules.supportEndDates[major.toString()];
213 | if (!supportDates) {
214 | Logger.warning('Cannot find support dates for ', this.auditVersion.toString());
215 | return new SupportEndDate(null, null, null, null);
216 | }
217 | return supportDates;
218 | };
219 |
220 | /**
221 | * @param {Date} start
222 | * @param {Date} end
223 | * @return {boolean}
224 | */
225 | function nowBetweenDates(start, end) {
226 | if (!start || !end) {
227 | return false;
228 | }
229 | const now = new Date().getTime();
230 | return now >= start.getTime() && now < end.getTime();
231 | }
232 |
233 | /**
234 | * @throws {InvalidRuntimeException}
235 | */
236 | function assertRuntimeVersion() {
237 | const minimumSupportedVersion = new NodeVersion(14, 0, 0);
238 | const runtimeVersion = NodeVersion.fromString(process.versions.node);
239 | if (NodeVersion.compare(minimumSupportedVersion, runtimeVersion) > 0) {
240 | throw InvalidRuntimeException.fromVersion(runtimeVersion);
241 | }
242 | }
243 |
244 | module.exports = {
245 | NodeVersionAudit,
246 | };
247 |
--------------------------------------------------------------------------------
/lib/Rules.js:
--------------------------------------------------------------------------------
1 | const util = require('util');
2 | const fs = require('fs');
3 | const readFile = util.promisify(fs.readFile);
4 | const writeFile = util.promisify(fs.writeFile);
5 |
6 | const { CachedDownload } = require('./CachedDownload');
7 | const { StaleRulesException } = require('./Exceptions');
8 | const path = require('path');
9 | const { NodeVersion } = require('./NodeVersion');
10 | const { NodeRelease } = require('./NodeRelease');
11 | const { CveId } = require('./CveId');
12 | const { SupportEndDate } = require('./SupportEndDate');
13 | const { Logger } = require('./Logger');
14 | const { CveDetails } = require('./CveDetails');
15 |
16 | const RULES_PATH = '/../docs/rules-v1.json';
17 | const HOSTED_RULES_PATH = 'https://www.github.developerdan.com/node-version-audit/rules-v1.json';
18 |
19 | const Rules = {};
20 |
21 | /**
22 | * @typedef Rules
23 | * @type {object}
24 | * @property {Date} lastUpdatedDate
25 | * @property {string} name
26 | * @property {string} website
27 | * @property {string} license
28 | * @property {string} source
29 | * @property {number} releasesCount
30 | * @property {number} cveCount
31 | * @property {number} supportVersionsCount
32 | * @property {NodeVersion} latestVersion
33 | * @property {{string: string}} latestVersions
34 | * @property {{string: SupportEndDate}} supportEndDates
35 | * @property {NodeRelease[]} releases
36 | * @property {Object} cves
37 | */
38 |
39 | /**
40 | * @param {boolean} update
41 | * @return {Promise<{}>}
42 | */
43 | async function loadRawRules(update) {
44 | if (update) {
45 | try {
46 | return await CachedDownload.json(HOSTED_RULES_PATH);
47 | } catch (e) {
48 | Logger.warning('Unable to download hosted rules:', e);
49 | }
50 | }
51 |
52 | if (fs.existsSync(path.join(__dirname, RULES_PATH))) {
53 | try {
54 | const buf = await readFile(path.join(__dirname, RULES_PATH));
55 | return JSON.parse(buf.toString('utf8'));
56 | } catch (e) {
57 | Logger.warning('Unable to read local rules:', e);
58 | }
59 | }
60 |
61 | throw StaleRulesException.fromString('Unable to load rules from disk');
62 | }
63 |
64 | /**
65 | *
66 | * @param update
67 | * @return {Promise}
68 | */
69 | Rules.loadRules = async (update) => {
70 | const rawRules = await loadRawRules(update);
71 | return transformRules(rawRules);
72 | };
73 |
74 | /**
75 | * @param {{string: NodeRelease}} releases
76 | * @param {{string: CveDetails}} cves
77 | * @param {{string: {start: Date, lts: Date, end: Date, maintenance: Date}}} supportSchedule
78 | * @return {Promise}
79 | */
80 | Rules.saveRules = (releases, cves, supportSchedule) => {
81 | const latestRelease = Object.values(releases).sort(NodeRelease.compare)[Object.keys(releases).length - 1];
82 | const rules = {
83 | lastUpdatedDate: new Date().toISOString(),
84 | name: 'Node Version Audit',
85 | website: 'https://github.com/lightswitch05/node-version-audit',
86 | license: 'https://github.com/lightswitch05/node-version-audit/blob/master/LICENSE',
87 | source: HOSTED_RULES_PATH,
88 | releasesCount: Object.keys(releases).length,
89 | cveCount: Object.keys(cves).length,
90 | supportVersionsCount: Object.keys(supportSchedule).length,
91 | latestVersion: latestRelease.version,
92 | latestVersions: releasesToLatestVersions(releases),
93 | supportEndDates: supportSchedule,
94 | releases: releases,
95 | cves: cves,
96 | };
97 | const data = JSON.stringify(rules, null, 4);
98 | return writeFile(path.join(__dirname, RULES_PATH), data);
99 | };
100 |
101 | /**
102 | * @param {Rules} rules
103 | */
104 | Rules.assertFreshRules = (rules) => {
105 | const elapsedSeconds = (new Date().getTime() - rules.lastUpdatedDate.getTime()) / 1000;
106 | const elapsedDays = Math.round(elapsedSeconds / 86400);
107 | if (elapsedSeconds > 1209600) {
108 | throw StaleRulesException.fromString(`Rules are older then two weeks (${elapsedDays} days)`);
109 | }
110 | Logger.debug(`Rules are ${elapsedDays} days old`);
111 | };
112 |
113 | /**
114 | * @param {{string: NodeRelease}} releases
115 | * @return {*}
116 | */
117 | function releasesToLatestVersions(releases) {
118 | const latestVersions = {};
119 | for (let release of Object.values(releases)) {
120 | const version = release.version;
121 | const major = `${version.major}`;
122 | const minor = `${version.minor}`;
123 | const majorAndMinor = `${major}.${minor}`;
124 | if (latestVersions[major]) {
125 | latestVersions[major] = version;
126 | }
127 | if (!latestVersions[majorAndMinor]) {
128 | latestVersions[majorAndMinor] = version;
129 | }
130 | if (NodeVersion.compare(version, latestVersions[major]) > 0) {
131 | latestVersions[major] = version;
132 | }
133 | if (NodeVersion.compare(version, latestVersions[majorAndMinor]) > 0) {
134 | latestVersions[majorAndMinor] = version;
135 | }
136 | }
137 | return latestVersions;
138 | }
139 |
140 | /**
141 | * @param {Object} rules
142 | * @return Rules
143 | */
144 | function transformRules(rules) {
145 | if (!rules.lastUpdatedDate || !rules.latestVersions || !rules.latestVersion || !rules.supportEndDates) {
146 | throw StaleRulesException.fromString('Unable to load rules');
147 | }
148 | rules.lastUpdatedDate = new Date(rules.lastUpdatedDate);
149 | for (let version in rules.latestVersions) {
150 | rules.latestVersions[version] = NodeVersion.fromString(rules.latestVersions[version]);
151 | }
152 | rules.latestVersion = NodeVersion.fromString(rules.latestVersion);
153 | for (let supportVersion in rules.supportEndDates) {
154 | let supportVersionDates = rules.supportEndDates[supportVersion];
155 | let lts = supportVersionDates.lts ? new Date(supportVersionDates.lts) : null;
156 | rules.supportEndDates[supportVersion] = new SupportEndDate(
157 | new Date(supportVersionDates.start),
158 | lts,
159 | new Date(supportVersionDates.maintenance),
160 | new Date(supportVersionDates.end),
161 | );
162 | }
163 | for (let [versionString, rawRelease] of Object.entries(rules.releases)) {
164 | const version = NodeVersion.fromString(versionString);
165 | const release = new NodeRelease(version);
166 | release.releaseDate = new Date(rawRelease.releaseDate);
167 | for (let cveString of rawRelease.patchedCveIds) {
168 | release.addPatchedCveId(CveId.fromString(cveString));
169 | }
170 | rules.releases[versionString] = release;
171 | }
172 | return rules;
173 | }
174 |
175 | module.exports = {
176 | Rules,
177 | };
178 |
--------------------------------------------------------------------------------
/lib/SupportEndDate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {Date|null} start
3 | * @param {Date|null} lts
4 | * @param {Date|null} maintenance
5 | * @param {Date|null} end
6 | * @constructor
7 | */
8 | function SupportEndDate(start, lts, maintenance, end) {
9 | this.start = start;
10 | if (lts) {
11 | this.lts = lts;
12 | }
13 | this.maintenance = maintenance;
14 | this.end = end;
15 | }
16 |
17 | module.exports = {
18 | SupportEndDate,
19 | };
20 |
--------------------------------------------------------------------------------
/lib/SupportSchedule.js:
--------------------------------------------------------------------------------
1 | const { CachedDownload } = require('./CachedDownload');
2 |
3 | const SUPPORT_SCHEDULE_URL = 'https://raw.githubusercontent.com/nodejs/Release/main/schedule.json';
4 |
5 | const SupportSchedule = {};
6 |
7 | /**
8 | *
9 | * @return {Promise<{}>}
10 | */
11 | SupportSchedule.parse = async function () {
12 | const supportSchedule = {};
13 | const supportScheduleRaw = await CachedDownload.json(SUPPORT_SCHEDULE_URL);
14 | for (let rawVersion in supportScheduleRaw) {
15 | if (rawVersion.startsWith('v0')) {
16 | continue;
17 | }
18 | const version = rawVersion.replace('v', '');
19 | supportSchedule[version] = {};
20 | for (let property of ['start', 'lts', 'maintenance', 'end']) {
21 | if (supportScheduleRaw[rawVersion][property]) {
22 | supportSchedule[version][property] = new Date(supportScheduleRaw[rawVersion][property]);
23 | }
24 | }
25 | }
26 | return supportSchedule;
27 | };
28 |
29 | const SUPPORT_TYPE = {
30 | CURRENT: 'current',
31 | ACTIVE: 'active',
32 | MAINTENANCE: 'maintenance',
33 | NONE: 'none',
34 | };
35 |
36 | module.exports = {
37 | SupportSchedule,
38 | SUPPORT_TYPE,
39 | };
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-version-audit",
3 | "version": "1.20250608.0",
4 | "description": "Audit your Node version for known CVEs and patches ",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "jest --coverage",
8 | "test:unit": "jest --coverage --selectProjects unit",
9 | "test:functional": "jest --coverage --selectProjects functional",
10 | "format": "prettier --write .",
11 | "format:check": "prettier --check .",
12 | "cspell": "cspell '**'"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/lightswitch05/node-version-audit.git"
17 | },
18 | "engines": {
19 | "node": ">=18.0.0"
20 | },
21 | "bin": {
22 | "node-version-audit": "./bin/node-version-audit"
23 | },
24 | "keywords": [
25 | "security",
26 | "audit",
27 | "cve",
28 | "node",
29 | "security-scanner",
30 | "cves",
31 | "security-audit"
32 | ],
33 | "author": "Daniel White",
34 | "license": "Apache-2.0",
35 | "bugs": {
36 | "url": "https://github.com/lightswitch05/node-version-audit/issues"
37 | },
38 | "homepage": "https://github.com/lightswitch05/node-version-audit#readme",
39 | "devDependencies": {
40 | "@casualbot/jest-sonar-reporter": "^2.4.0",
41 | "cspell": "^8.17.5",
42 | "jest": "^29.7.0",
43 | "prettier": "^3.5.3"
44 | },
45 | "jest": {
46 | "reporters": [
47 | "default",
48 | "@casualbot/jest-sonar-reporter",
49 | "github-actions"
50 | ],
51 | "projects": [
52 | {
53 | "displayName": "unit",
54 | "roots": [
55 | "/tests/unit"
56 | ]
57 | },
58 | {
59 | "displayName": "functional",
60 | "roots": [
61 | "/tests/functional"
62 | ]
63 | }
64 | ]
65 | },
66 | "@casualbot/jest-sonar-reporter": {
67 | "outputDirectory": "coverage",
68 | "outputName": "jest-report.xml"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/scripts/bump-version.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -x
4 | set -e
5 |
6 | LAST_VERSION=$(jq --raw-output '.version' < package.json)
7 | MAJOR_VERSION="${LAST_VERSION%%.*}"
8 | LAST_MINOR_VERSION="${LAST_VERSION%.*}"
9 | LAST_MINOR_VERSION="${LAST_MINOR_VERSION##*.}"
10 | NEW_MINOR_VERSION=$(date +"%Y%m%d")
11 |
12 | if [[ "${LAST_MINOR_VERSION}" == "${NEW_MINOR_VERSION}" ]]; then
13 | PATCH_VERSION="${LAST_VERSION##*.}"
14 | PATCH_VERSION="$((PATCH_VERSION+1))"
15 | else
16 | PATCH_VERSION="0"
17 | fi
18 |
19 | NEW_VERSION="${MAJOR_VERSION}.${NEW_MINOR_VERSION}.${PATCH_VERSION}"
20 |
21 | npm version --no-git-tag-version "${NEW_VERSION}"
22 |
--------------------------------------------------------------------------------
/scripts/github-commit-auto-updates.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -eu
3 |
4 | cat <<- EOF > "${HOME}/.netrc"
5 | machine github.com
6 | login $GITHUB_ACTOR
7 | password $GITHUB_TOKEN
8 | machine api.github.com
9 | login $GITHUB_ACTOR
10 | password $GITHUB_TOKEN
11 | EOF
12 | chmod 600 "${HOME}/.netrc"
13 | git config --global user.email "daniel@developerdan.com"
14 | git config --global user.name "Auto Updates"
15 |
16 | COMMIT_MESSAGE="Automatic github actions updates."
17 | LINES_ADDED=$(git diff --numstat docs/rules-v1.json | sed 's/^\([0-9]*\)\(.*\)/\1/g')
18 | if [ "$LINES_ADDED" -gt "1" ]; then
19 | COMMIT_MESSAGE="${COMMIT_MESSAGE} Changes found @lightswitch05"
20 | fi
21 |
22 | git add ./docs/rules-v1.json ./package.json ./package-lock.json
23 | git commit -m "${COMMIT_MESSAGE}"
24 |
25 | NEW_TAG=$(jq --raw-output '.version' < package.json)
26 | git tag "${NEW_TAG}"
27 | git push origin : "${NEW_TAG}"
28 |
--------------------------------------------------------------------------------
/sonar-project.properties:
--------------------------------------------------------------------------------
1 | sonar.organization=lightswitch05
2 | sonar.projectKey=lightswitch05_node-version-audit
3 | sonar.projectName=node-version-audit
4 | sonar.sourceEncoding=UTF-8
5 | sonar.sources=./lib, ./bin, ./docs
6 | sonar.tests=./tests
7 |
8 | sonar.javascript.lcov.reportPaths=./coverage/lcov.info
9 | sonar.testExecutionReportPaths=./coverage/jest-report.xml
10 |
--------------------------------------------------------------------------------
/tests/functional/CachedDownload.test.js:
--------------------------------------------------------------------------------
1 | const { CachedDownload } = require('../../lib/CachedDownload');
2 | const { DownloadException } = require('../../lib/Exceptions');
3 |
4 | describe('CachedDownload', () => {
5 | it('downloads gzip json', async () => {
6 | const url = 'https://httpbin.org/gzip?q=' + Date.now() + Math.random() + '&format=gz';
7 | const response = await CachedDownload.json(url);
8 | expect(response).toBeDefined();
9 | expect(response.gzipped).toBe(true);
10 | expect(response.method).toMatch('GET');
11 | });
12 |
13 | it('handles 500 error code', async () => {
14 | const download = CachedDownload.download('https://httpbin.org/status/500');
15 | await expect(download).rejects.toThrowError(DownloadException);
16 | });
17 |
18 | it('handles invalid JSON download', async () => {
19 | const download = CachedDownload.json('https://httpbin.org/status/200');
20 | await expect(download).rejects.toThrowError(DownloadException);
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/tests/functional/Changelogs.test.js:
--------------------------------------------------------------------------------
1 | const { Changelogs } = require('../../lib/Changelogs');
2 |
3 | describe('Changelogs', () => {
4 | it('gets change log URLs', async () => {
5 | const urls = await Changelogs.getChangelogUrls();
6 | expect(urls).toBeDefined();
7 | expect(urls.length).toBeGreaterThan(16);
8 | });
9 |
10 | it('parses changelogs', async () => {
11 | const nodeReleases = await Changelogs.parse();
12 | expect(nodeReleases).toBeDefined();
13 | const releaseVersions = Object.keys(nodeReleases);
14 | expect(releaseVersions.length).toBeGreaterThan(503);
15 | expect(nodeReleases['0.10.0']).toBeDefined();
16 | expect(releaseVersions[0]).toEqual('0.10.0');
17 | expect(releaseVersions[404]).toEqual('13.9.0');
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/tests/functional/Cli.test.js:
--------------------------------------------------------------------------------
1 | const { Cli } = require('../../lib/Cli');
2 | const { NodeVersion } = require('../../lib/NodeVersion');
3 |
4 | describe('Cli', () => {
5 | it('check version 1.8.4 from supplied args object', async () => {
6 | const args = [null, null, '--version=1.8.4', '--no-update'];
7 | const cli = new Cli(args);
8 | const results = await cli.run();
9 | expect(results).toBeDefined();
10 | expect(results.auditVersion).toEqual(NodeVersion.fromString('1.8.4'));
11 | expect(results.hasVulnerabilities()).toBe(false);
12 | expect(results.hasSupport()).toBe(false);
13 | expect(results.supportType).toEqual('none');
14 | expect(results.isLatestPatchVersion()).toBe(true);
15 | expect(results.isLatestMinorVersion()).toBe(true);
16 | expect(results.isLatestVersion()).toBe(false);
17 | expect(results.latestPatchVersion).toEqual(NodeVersion.fromString('1.8.4'));
18 | expect(results.latestMinorVersion).toEqual(NodeVersion.fromString('1.8.4'));
19 | expect(results.latestVersion).toBeDefined();
20 | expect(results.activeSupportEndDate).toBeNull();
21 | expect(results.supportEndDate).toBeNull();
22 | expect(results.rulesLastUpdatedDate).toBeTruthy();
23 | expect(results.vulnerabilities).toBeDefined();
24 | expect(Object.keys(results.vulnerabilities).length).toBe(0);
25 | });
26 |
27 | it('check version 1.8.4 from argv', async () => {
28 | const args = [null, null, '--version=1.8.4', '--no-update'];
29 | const cli = new Cli(args);
30 | const results = await cli.run();
31 | expect(results).toBeDefined();
32 | expect(results.auditVersion).toEqual(NodeVersion.fromString('1.8.4'));
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/tests/functional/NodeVersionAudit.test.js:
--------------------------------------------------------------------------------
1 | const { InvalidVersionException } = require('../../lib/Exceptions');
2 | const { NodeVersionAudit } = require('../../lib/NodeVersionAudit');
3 | const { NodeVersion } = require('../../lib/NodeVersion');
4 |
5 | describe('NodeVersionAudit', () => {
6 | it('throws on null version', () => {
7 | expect(() => {
8 | NodeVersionAudit(null, false);
9 | }).toThrowError(InvalidVersionException);
10 | });
11 |
12 | it('get the audit results of 0.10.0', async () => {
13 | const versionAudit = new NodeVersionAudit('0.10.0', false);
14 | const auditResults = await versionAudit.getAllAuditResults();
15 | expect(auditResults).toBeDefined();
16 | expect(auditResults.auditVersion).toEqual(NodeVersion.fromString('0.10.0'));
17 | expect(auditResults.supportType).toBe('none');
18 | expect(Object.keys(auditResults.vulnerabilities).length).toBeGreaterThan(19);
19 | });
20 |
21 | it('get the audit results of 17.8.0', async () => {
22 | const versionAudit = new NodeVersionAudit('17.8.0', false);
23 | const auditResults = await versionAudit.getAllAuditResults();
24 | expect(auditResults).toBeDefined();
25 | expect(auditResults.auditVersion).toEqual(NodeVersion.fromString('17.8.0'));
26 | expect(auditResults.supportType).toBe('none');
27 | });
28 |
29 | it('get the audit results of 18.9.0', async () => {
30 | const versionAudit = new NodeVersionAudit('18.9.0', false);
31 | const auditResults = await versionAudit.getAllAuditResults();
32 | expect(auditResults).toBeDefined();
33 | expect(auditResults.auditVersion).toEqual(NodeVersion.fromString('18.9.0'));
34 | expect(auditResults.supportType).not.toBe('none');
35 | });
36 |
37 | it('get the audit results of 16.14.2', async () => {
38 | const versionAudit = new NodeVersionAudit('16.14.2', false);
39 | const auditResults = await versionAudit.getAllAuditResults();
40 | expect(auditResults).toBeDefined();
41 | expect(auditResults.auditVersion).toEqual(NodeVersion.fromString('16.14.2'));
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/tests/unit/Args.test.js:
--------------------------------------------------------------------------------
1 | const { Logger } = require('../../lib/Logger');
2 | const { Args } = require('../../lib/Args');
3 | const { InvalidArgumentException } = require('../../lib/Exceptions');
4 |
5 | describe('Args.getVerbosity', () => {
6 | it('defaults to error', () => {
7 | const args = new Args();
8 | expect(args.getVerbosity()).toEqual(Logger.ERROR);
9 | });
10 |
11 | it('defaults to error', () => {
12 | const args = new Args();
13 | expect(args.getVerbosity()).toEqual(Logger.ERROR);
14 | });
15 |
16 | it('sets DEBUG', () => {
17 | const args = new Args();
18 | args[Args.OPTIONS.V_DEBUG] = true;
19 | expect(args.getVerbosity()).toEqual(Logger.DEBUG);
20 | });
21 |
22 | it('sets INFO', () => {
23 | const args = new Args();
24 | args[Args.OPTIONS.V_INFO] = true;
25 | expect(args.getVerbosity()).toEqual(Logger.INFO);
26 | });
27 |
28 | it('sets WARNING', () => {
29 | const args = new Args();
30 | args[Args.OPTIONS.V_WARNING] = true;
31 | expect(args.getVerbosity()).toEqual(Logger.WARNING);
32 | });
33 |
34 | it('sets SILENT', () => {
35 | const args = new Args();
36 | args[Args.OPTIONS.SILENT] = true;
37 | expect(args.getVerbosity()).toEqual(Logger.SILENT);
38 | });
39 |
40 | it('conflicts choose the more precise level', () => {
41 | const args = new Args();
42 | args[Args.OPTIONS.V_INFO] = true;
43 | args[Args.OPTIONS.V_WARNING] = true;
44 | args[Args.OPTIONS.SILENT] = true;
45 | expect(args.getVerbosity()).toEqual(Logger.INFO);
46 | });
47 | });
48 |
49 | describe('Args.parseArgs', () => {
50 | it('parses no args', () => {
51 | const args = Args.parseArgs([null, null]);
52 | expect(args).toBeDefined();
53 | expect(args[Args.OPTIONS.NODE_VERSION]).toEqual(process.versions.node);
54 | });
55 |
56 | it('parses version', () => {
57 | const args = Args.parseArgs([null, null, '--version=99.99.99']);
58 | expect(args).toBeDefined();
59 | expect(args[Args.OPTIONS.NODE_VERSION]).toEqual('99.99.99');
60 | });
61 |
62 | it('throws on unknown args', () => {
63 | expect(() => {
64 | Args.parseArgs([null, null, 'no-dashes']);
65 | }).toThrowError(InvalidArgumentException);
66 |
67 | expect(() => {
68 | Args.parseArgs([null, null, '--unknown-arg']);
69 | }).toThrowError(InvalidArgumentException);
70 | });
71 |
72 | it('throws on version without version', () => {
73 | expect(() => {
74 | Args.parseArgs([null, null, '--version']);
75 | }).toThrowError(InvalidArgumentException);
76 |
77 | expect(() => {
78 | Args.parseArgs([null, null, '--version=']);
79 | }).toThrowError(InvalidArgumentException);
80 |
81 | expect(() => {
82 | Args.parseArgs([null, null, '--version=1']);
83 | }).toThrowError(InvalidArgumentException);
84 |
85 | expect(() => {
86 | Args.parseArgs([null, null, '--version=1.2']);
87 | }).toThrowError(InvalidArgumentException);
88 |
89 | expect(() => {
90 | Args.parseArgs([null, null, '--version=1.2.']);
91 | }).toThrowError(InvalidArgumentException);
92 |
93 | expect(() => {
94 | Args.parseArgs([null, null, '--version', '1.2.3']);
95 | }).toThrowError(InvalidArgumentException);
96 | });
97 |
98 | it('throws on missing version if NVA_REQUIRE_VERSION_ARG', () => {
99 | process.env.NVA_REQUIRE_VERSION_ARG = 'true';
100 | expect(() => {
101 | Args.parseArgs([null, null]);
102 | }).toThrowError(InvalidArgumentException);
103 | });
104 | });
105 |
--------------------------------------------------------------------------------
/tests/unit/Changelogs.test.js:
--------------------------------------------------------------------------------
1 | const { Changelogs } = require('../../lib/Changelogs');
2 | const { NodeVersionAudit } = require('../../lib/NodeVersionAudit');
3 | const { NodeVersion } = require('../../lib/NodeVersion');
4 | const { CveId } = require('../../lib/CveId');
5 |
6 | describe('Changelogs', () => {
7 | it('parses a single release', async () => {
8 | const rawChangelog = `
9 |
10 |
11 | ## 2021-04-06, Version 10.24.1 'Dubnium' (LTS), @mylesborins
12 |
13 | This is a security release.
14 |
15 | ### Notable Changes
16 |
17 | Vulerabilties fixed:
18 |
19 | * **CVE-2021-3450**: OpenSSL - CA certificate check bypass with X509\\_V\\_FLAG\\_X509\\_STRICT (High)
20 | * This is a vulnerability in OpenSSL which may be exploited through Node.js. You can read more about it in
21 | * Impacts:
22 | * All versions of the 15.x, 14.x, 12.x and 10.x releases lines
23 | * **CVE-2021-3449**: OpenSSL - NULL pointer deref in signature\\_algorithms processing (High)
24 | * This is a vulnerability in OpenSSL which may be exploited through Node.js. You can read more about it in
25 | * Impacts:
26 | * All versions of the 15.x, 14.x, 12.x and 10.x releases lines
27 | * **CVE-2020-7774**: npm upgrade - Update y18n to fix Prototype-Pollution (High)
28 | * This is a vulnerability in the y18n npm module which may be exploited by prototype pollution. You can read more about it in
29 | * Impacts:
30 | * All versions of the 14.x, 12.x and 10.x releases lines
31 |
32 |
33 |
34 | ## 2021-02-23, Version 10.24.0 'Dubnium' (LTS), @richardlau
35 |
36 | This is a security release.
37 | `;
38 | const parsedChangelog = Changelogs.parseChangelog(rawChangelog);
39 | expect(parsedChangelog).toBeDefined();
40 | expect(parsedChangelog['10.24.1']).toBeDefined();
41 | expect(parsedChangelog['10.24.1'].version).toEqual(NodeVersion.fromString('10.24.1'));
42 | expect(parsedChangelog['10.24.1'].version).toEqual(NodeVersion.fromString('10.24.1'));
43 | expect(parsedChangelog['10.24.1'].releaseDate).toEqual(new Date('2021-04-06'));
44 | expect(parsedChangelog['10.24.1'].patchedCveIds.length).toEqual(3);
45 | expect(parsedChangelog['10.24.1'].patchedCveIds[0]).toEqual(CveId.fromString('CVE-2020-7774'));
46 | expect(parsedChangelog['10.24.0']).toBeDefined();
47 | expect(Object.keys(parsedChangelog)[0]).toEqual('10.24.0');
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/tests/unit/Cli.test.js:
--------------------------------------------------------------------------------
1 | const { Cli } = require('../../lib/Cli');
2 |
3 | describe('Cli', () => {
4 | it('prints helps with the help arg', async () => {
5 | const exitMock = jest.spyOn(process, 'exit').mockImplementation(() => {});
6 | const showHelpSpy = jest.spyOn(Cli, 'showHelp');
7 | const args = [null, null, '--help'];
8 | const cli = new Cli(args);
9 | await cli.run();
10 | expect(showHelpSpy).toHaveBeenCalled();
11 | expect(exitMock).toBeCalledWith(0);
12 | });
13 |
14 | it('prints helps with invalid version arg', async () => {
15 | const exitMock = jest.spyOn(process, 'exit').mockImplementation(() => {});
16 | const showHelpSpy = jest.spyOn(Cli, 'showHelp');
17 | const args = [null, null, '--version=invalid'];
18 | const cli = new Cli(args);
19 | await cli.run();
20 | expect(showHelpSpy).toHaveBeenCalled();
21 | expect(exitMock).toBeCalledWith(120);
22 | });
23 |
24 | it('prints helps with unknown arg', async () => {
25 | const exitMock = jest.spyOn(process, 'exit').mockImplementation(() => {});
26 | const showHelpSpy = jest.spyOn(Cli, 'showHelp');
27 | const args = [null, null, '--not-an-option'];
28 | expect(() => {
29 | new Cli(args);
30 | }).toThrowError("Invalid argument: 'not-an-option'");
31 | expect(showHelpSpy).toHaveBeenCalled();
32 | expect(exitMock).toBeCalledWith(120);
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/tests/unit/CveDetails.test.js:
--------------------------------------------------------------------------------
1 | const { CveDetails } = require('../../lib/CveDetails');
2 | const { CveId } = require('../../lib/CveId');
3 |
4 | describe('CveDetails', () => {
5 | it('creates a simple object', () => {
6 | const id = CveId.fromString('CVE-2021-00123');
7 | const baseScore = 9;
8 | const publishedDate = new Date('2022-03-16T20:15Z').toISOString();
9 | const lastModifiedDate = new Date('2022-03-16T18:15Z').toISOString();
10 | const description = 'This is a CVE description';
11 | const details = new CveDetails(id, baseScore, publishedDate, lastModifiedDate, description);
12 | expect(details).not.toBeNull();
13 | expect(details.id).toBe(id);
14 | expect(details.baseScore).toBe(baseScore);
15 | expect(details.publishedDate).toBe(publishedDate);
16 | expect(details.lastModifiedDate).toBe(lastModifiedDate);
17 | expect(details.description).toBe(description);
18 | });
19 | });
20 |
21 | describe('CveDetails.compare', () => {
22 | it('compares less than', () => {
23 | const first = new CveDetails(CveId.fromString('CVE-2022-00001'), 9, new Date(), new Date(), '1');
24 | const second = new CveDetails(CveId.fromString('CVE-2022-00002'), 9, new Date(), new Date(), '2');
25 | const compareResult = CveDetails.compare(first, second);
26 | expect(compareResult).toBeLessThan(0);
27 | });
28 |
29 | it('compares greater than', () => {
30 | const first = new CveDetails(CveId.fromString('CVE-2022-00002'), 9, new Date(), new Date(), '1');
31 | const second = new CveDetails(CveId.fromString('CVE-2022-00001'), 9, new Date(), new Date(), '2');
32 | const compareResult = CveDetails.compare(first, second);
33 | expect(compareResult).toBeGreaterThan(0);
34 | });
35 |
36 | it('compares equal', () => {
37 | const first = new CveDetails(CveId.fromString('CVE-2022-00001'), 9, new Date(), new Date(), '1');
38 | const second = new CveDetails(CveId.fromString('CVE-2022-00001'), 9, new Date(), new Date(), '2');
39 | const compareResult = CveDetails.compare(first, second);
40 | expect(compareResult).toBe(0);
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/tests/unit/CveFeed.test.js:
--------------------------------------------------------------------------------
1 | const { CveFeed } = require('../../lib/CveFeed');
2 | const { CveId } = require('../../lib/CveId');
3 | jest.mock('../../lib/CachedDownload');
4 | const CachedDownload = require('../../lib/CachedDownload');
5 |
6 | const FEED = {
7 | CVE_data_type: 'CVE',
8 | CVE_data_format: 'MITRE',
9 | CVE_data_version: '4.0',
10 | CVE_data_numberOfCVEs: '780',
11 | CVE_data_timestamp: '2022-04-23T00:00Z',
12 | CVE_Items: [
13 | {
14 | cve: {
15 | data_type: 'CVE',
16 | data_format: 'MITRE',
17 | data_version: '4.0',
18 | CVE_data_meta: {
19 | ID: 'CVE-2022-1108',
20 | ASSIGNER: 'psirt@lenovo.com',
21 | },
22 | problemtype: {
23 | problemtype_data: [
24 | {
25 | description: [],
26 | },
27 | ],
28 | },
29 | references: {
30 | reference_data: [
31 | {
32 | url: 'https://support.lenovo.com/us/en/product_security/LEN-84943',
33 | name: 'https://support.lenovo.com/us/en/product_security/LEN-84943',
34 | refsource: 'MISC',
35 | tags: [],
36 | },
37 | ],
38 | },
39 | description: {
40 | description_data: [
41 | {
42 | lang: 'en',
43 | value: 'A potential vulnerability due to improper buffer validation in the SMI handler LenovoFlashDeviceInterface in Thinkpad X1 Fold Gen 1 could be exploited by an attacker with local access and elevated privileges to execute arbitrary code.',
44 | },
45 | ],
46 | },
47 | },
48 | configurations: {
49 | CVE_data_version: '4.0',
50 | nodes: [],
51 | },
52 | impact: {},
53 | publishedDate: '2022-04-22T21:15Z',
54 | lastModifiedDate: '2022-04-22T21:15Z',
55 | },
56 | {
57 | cve: {
58 | data_type: 'CVE',
59 | data_format: 'MITRE',
60 | data_version: '4.0',
61 | CVE_data_meta: {
62 | ID: 'CVE-2022-27456',
63 | ASSIGNER: 'cve@mitre.org',
64 | },
65 | problemtype: {
66 | problemtype_data: [
67 | {
68 | description: [
69 | {
70 | lang: 'en',
71 | value: 'CWE-416',
72 | },
73 | ],
74 | },
75 | ],
76 | },
77 | references: {
78 | reference_data: [
79 | {
80 | url: 'https://jira.mariadb.org/browse/MDEV-28093',
81 | name: 'https://jira.mariadb.org/browse/MDEV-28093',
82 | refsource: 'MISC',
83 | tags: ['Exploit', 'Issue Tracking', 'Third Party Advisory'],
84 | },
85 | ],
86 | },
87 | description: {
88 | description_data: [
89 | {
90 | lang: 'en',
91 | value: 'MariaDB Server v10.6.3 and below was discovered to contain an use-after-free in the component VDec::VDec at /sql/sql_type.cc.',
92 | },
93 | ],
94 | },
95 | },
96 | configurations: {
97 | CVE_data_version: '4.0',
98 | nodes: [
99 | {
100 | operator: 'OR',
101 | children: [],
102 | cpe_match: [
103 | {
104 | vulnerable: true,
105 | cpe23Uri: 'cpe:2.3:a:mariadb:mariadb:*:*:*:*:*:*:*:*',
106 | versionEndIncluding: '10.6.3',
107 | cpe_name: [],
108 | },
109 | ],
110 | },
111 | ],
112 | },
113 | impact: {
114 | baseMetricV3: {
115 | cvssV3: {
116 | version: '3.1',
117 | vectorString: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H',
118 | attackVector: 'NETWORK',
119 | attackComplexity: 'LOW',
120 | privilegesRequired: 'NONE',
121 | userInteraction: 'NONE',
122 | scope: 'UNCHANGED',
123 | confidentialityImpact: 'NONE',
124 | integrityImpact: 'NONE',
125 | availabilityImpact: 'HIGH',
126 | baseScore: 7.5,
127 | baseSeverity: 'HIGH',
128 | },
129 | exploitabilityScore: 3.9,
130 | impactScore: 3.6,
131 | },
132 | baseMetricV2: {
133 | cvssV2: {
134 | version: '2.0',
135 | vectorString: 'AV:N/AC:L/Au:N/C:N/I:N/A:P',
136 | accessVector: 'NETWORK',
137 | accessComplexity: 'LOW',
138 | authentication: 'NONE',
139 | confidentialityImpact: 'NONE',
140 | integrityImpact: 'NONE',
141 | availabilityImpact: 'PARTIAL',
142 | baseScore: 5.0,
143 | },
144 | severity: 'MEDIUM',
145 | exploitabilityScore: 10.0,
146 | impactScore: 2.9,
147 | acInsufInfo: false,
148 | obtainAllPrivilege: false,
149 | obtainUserPrivilege: false,
150 | obtainOtherPrivilege: false,
151 | userInteractionRequired: false,
152 | },
153 | },
154 | publishedDate: '2022-04-14T13:15Z',
155 | lastModifiedDate: '2022-04-21T15:00Z',
156 | },
157 | {
158 | cve: {
159 | data_type: 'CVE',
160 | data_format: 'MITRE',
161 | data_version: '4.0',
162 | CVE_data_meta: {
163 | ID: 'CVE-2022-1350',
164 | ASSIGNER: 'cna@vuldb.com',
165 | },
166 | problemtype: {
167 | problemtype_data: [
168 | {
169 | description: [],
170 | },
171 | ],
172 | },
173 | references: {
174 | reference_data: [
175 | {
176 | url: 'https://bugs.ghostscript.com/show_bug.cgi?id=705156',
177 | name: 'https://bugs.ghostscript.com/show_bug.cgi?id=705156',
178 | refsource: 'MISC',
179 | tags: [],
180 | },
181 | {
182 | url: 'https://vuldb.com/?id.197290',
183 | name: 'https://vuldb.com/?id.197290',
184 | refsource: 'MISC',
185 | tags: [],
186 | },
187 | {
188 | url: 'https://bugs.ghostscript.com/attachment.cgi?id=22323',
189 | name: 'https://bugs.ghostscript.com/attachment.cgi?id=22323',
190 | refsource: 'MISC',
191 | tags: [],
192 | },
193 | ],
194 | },
195 | description: {
196 | description_data: [
197 | {
198 | lang: 'en',
199 | value: 'A vulnerability classified as problematic was found in GhostPCL 9.55.0. This vulnerability affects the function chunk_free_object of the file gsmchunk.c. The manipulation with a malicious file leads to a memory corruption. The attack can be initiated remotely but requires user interaction. The exploit has been disclosed to the public as a POC and may be used. It is recommended to apply the patches to fix this issue.',
200 | },
201 | ],
202 | },
203 | },
204 | configurations: {
205 | CVE_data_version: '4.0',
206 | nodes: [],
207 | },
208 | impact: {},
209 | publishedDate: '2022-04-14T07:15Z',
210 | lastModifiedDate: '2022-04-15T15:15Z',
211 | },
212 | {
213 | cve: {
214 | data_type: 'CVE',
215 | data_format: 'MITRE',
216 | data_version: '4.0',
217 | CVE_data_meta: {
218 | ID: 'CVE-2022-1350',
219 | ASSIGNER: 'cna@vuldb.com',
220 | },
221 | problemtype: {
222 | problemtype_data: [
223 | {
224 | description: [],
225 | },
226 | ],
227 | },
228 | references: {
229 | reference_data: [
230 | {
231 | url: 'https://bugs.ghostscript.com/show_bug.cgi?id=705156',
232 | name: 'https://bugs.ghostscript.com/show_bug.cgi?id=705156',
233 | refsource: 'MISC',
234 | tags: [],
235 | },
236 | {
237 | url: 'https://vuldb.com/?id.197290',
238 | name: 'https://vuldb.com/?id.197290',
239 | refsource: 'MISC',
240 | tags: [],
241 | },
242 | {
243 | url: 'https://bugs.ghostscript.com/attachment.cgi?id=22323',
244 | name: 'https://bugs.ghostscript.com/attachment.cgi?id=22323',
245 | refsource: 'MISC',
246 | tags: [],
247 | },
248 | ],
249 | },
250 | description: {
251 | description_data: [
252 | {
253 | lang: 'en',
254 | value: 'A vulnerability classified as problematic was found in GhostPCL 9.55.0. This vulnerability affects the function chunk_free_object of the file gsmchunk.c. The manipulation with a malicious file leads to a memory corruption. The attack can be initiated remotely but requires user interaction. The exploit has been disclosed to the public as a POC and may be used. It is recommended to apply the patches to fix this issue.',
255 | },
256 | ],
257 | },
258 | },
259 | configurations: {
260 | CVE_data_version: '4.0',
261 | nodes: [],
262 | },
263 | impact: {},
264 | publishedDate: '2022-04-14T07:15Z',
265 | lastModifiedDate: '2022-04-15T15:15Z',
266 | },
267 | ],
268 | };
269 |
270 | describe('CveFeed', () => {
271 | beforeAll(() => {
272 | CachedDownload.CachedDownload.json.mockResolvedValue(FEED);
273 | });
274 |
275 | describe('CveFeed._parseCveItem', () => {
276 | let rawCveItem = null;
277 | beforeEach(() => {
278 | rawCveItem = {
279 | publishedDate: '2014-07-02T10:35Z',
280 | lastModifiedDate: '2017-08-29T01:34Z',
281 | cve: {
282 | CVE_data_meta: {
283 | ID: 'CVE-2014-3066',
284 | },
285 | description: {
286 | description_data: [
287 | {
288 | lang: 'fr',
289 | value: 'FR description',
290 | },
291 | {
292 | lang: 'en',
293 | value: 'EN description',
294 | },
295 | ],
296 | },
297 | },
298 | impact: {
299 | baseMetricV2: {
300 | cvssV2: {
301 | baseScore: 5.0,
302 | },
303 | },
304 | baseMetricV3: {
305 | cvssV3: {
306 | baseScore: 9.8,
307 | },
308 | },
309 | },
310 | };
311 | });
312 |
313 | it('parses V2 score', () => {
314 | delete rawCveItem.impact.baseMetricV3;
315 | const cveItem = CveFeed._parseCveItem(rawCveItem);
316 | expect(cveItem).toBeDefined();
317 | expect(cveItem.id).toEqual(CveId.fromString('CVE-2014-3066'));
318 | expect(cveItem.baseScore).toEqual(5);
319 | expect(cveItem.description).toEqual('EN description');
320 | expect(cveItem.publishedDate).toEqual(new Date('2014-07-02T10:35Z').toISOString());
321 | expect(cveItem.lastModifiedDate).toEqual(new Date('2017-08-29T01:34Z').toISOString());
322 | });
323 |
324 | it('parses a V3 score', () => {
325 | const cveItem = CveFeed._parseCveItem(rawCveItem);
326 | expect(cveItem).toBeDefined();
327 | expect(cveItem.id).toEqual(CveId.fromString('CVE-2014-3066'));
328 | expect(cveItem.baseScore).toEqual(9.8);
329 | expect(cveItem.description).toEqual('EN description');
330 | expect(cveItem.publishedDate).toEqual(new Date('2014-07-02T10:35Z').toISOString());
331 | expect(cveItem.lastModifiedDate).toEqual(new Date('2017-08-29T01:34Z').toISOString());
332 | });
333 |
334 | it('does not parse a null CVE', () => {
335 | rawCveItem.cve.CVE_data_meta.ID = null;
336 | const cveItem = CveFeed._parseCveItem(rawCveItem);
337 | expect(cveItem).toBeNull();
338 | });
339 |
340 | it('parses a CVE without a description', () => {
341 | delete rawCveItem.cve.description.description_data;
342 | const cveItem = CveFeed._parseCveItem(rawCveItem);
343 | expect(cveItem).toBeDefined();
344 | expect(cveItem.description).toBeNull();
345 | });
346 |
347 | it('parses a CVE without a score', () => {
348 | delete rawCveItem.impact.baseMetricV3;
349 | delete rawCveItem.impact.baseMetricV2;
350 | const cveItem = CveFeed._parseCveItem(rawCveItem);
351 | expect(cveItem).toBeDefined();
352 | expect(cveItem.baseScore).toBeNull();
353 | });
354 | });
355 |
356 | describe('CveFeed._parseFeed', () => {
357 | it('parses a feed', () => {
358 | CveFeed._parseFeed(
359 | [
360 | CveId.fromString('CVE-2022-1350'),
361 | CveId.fromString('CVE-2022-27456'),
362 | CveId.fromString('CVE-2022-1108'),
363 | ],
364 | FEED,
365 | );
366 | });
367 | });
368 |
369 | describe('CveFeed._downloadFeed', () => {
370 | it('downloads a feed', async () => {
371 | const feed = await CveFeed._downloadFeed('recent');
372 | expect(feed).toBeDefined();
373 | });
374 | });
375 |
376 | describe('CveFeed.parse', () => {
377 | it('parses a feed', async () => {
378 | const cveDetails = await CveFeed.parse([CveId.fromString('CVE-2022-1108')]);
379 | expect(cveDetails).toBeDefined();
380 | expect(typeof cveDetails).toBe('object');
381 | expect(cveDetails['CVE-2022-1108']).toBeDefined();
382 | expect(Object.keys(cveDetails).length).toBe(1);
383 | });
384 | });
385 | });
386 |
--------------------------------------------------------------------------------
/tests/unit/CveId.test.js:
--------------------------------------------------------------------------------
1 | const { CveId } = require('../../lib/CveId');
2 | const { ParseException } = require('../../lib/Exceptions');
3 |
4 | describe('CveId.fromString', () => {
5 | it('parses a simple CVE', () => {
6 | const cve = CveId.fromString('CVE-2021-43803');
7 | expect(cve).not.toBeNull();
8 | expect(cve.year).toBe(2021);
9 | expect(cve.sequenceNumber).toBe(43803);
10 | expect(cve.id).toBe('CVE-2021-43803');
11 | });
12 |
13 | it('is not case sensitive', () => {
14 | const cve = CveId.fromString('cve-2021-22939');
15 | expect(cve).not.toBeNull();
16 | expect(cve.year).toBe(2021);
17 | expect(cve.sequenceNumber).toBe(22939);
18 | expect(cve.id).toBe('CVE-2021-22939');
19 | });
20 |
21 | it('does not parse null', () => {
22 | expect(() => {
23 | CveId.fromString(null);
24 | }).toThrow(ParseException);
25 | });
26 |
27 | it('does not parse empty string', () => {
28 | expect(() => {
29 | CveId.fromString('');
30 | }).toThrow(ParseException);
31 | });
32 |
33 | it('does not parse missing year', () => {
34 | expect(() => {
35 | CveId.fromString('CVE--22939');
36 | }).toThrow(ParseException);
37 |
38 | expect(() => {
39 | CveId.fromString('CVE-22939');
40 | }).toThrow(ParseException);
41 | });
42 |
43 | it('does not parse missing sequence', () => {
44 | expect(() => {
45 | CveId.fromString('CVE-2021-');
46 | }).toThrow(ParseException);
47 |
48 | expect(() => {
49 | CveId.fromString('CVE-2021');
50 | }).toThrow(ParseException);
51 | });
52 |
53 | it('does not parse missing CVE', () => {
54 | expect(() => {
55 | CveId.fromString('-2021-22939');
56 | }).toThrow(ParseException);
57 |
58 | expect(() => {
59 | CveId.fromString('2021-22939');
60 | }).toThrow(ParseException);
61 | });
62 | });
63 |
64 | describe('CveId.compare', () => {
65 | it('compares equal versions', () => {
66 | const a = CveId.fromString('CVE-2021-43803');
67 | const b = CveId.fromString('CVE-2021-43803');
68 | expect(CveId.compare(a, b)).toBe(0);
69 | expect(CveId.compare(b, a)).toBe(0);
70 | });
71 |
72 | it('compares null', () => {
73 | const a = CveId.fromString('CVE-2021-43803');
74 | expect(CveId.compare(a, null)).toBe(1);
75 | expect(CveId.compare(null, a)).toBe(-1);
76 | });
77 |
78 | it('compares different sequence number', () => {
79 | const a = CveId.fromString('CVE-2021-43803');
80 | const b = CveId.fromString('CVE-2021-43804');
81 | expect(CveId.compare(a, b)).toBe(-1);
82 | expect(CveId.compare(b, a)).toBe(1);
83 | });
84 |
85 | it('compares different year', () => {
86 | const a = CveId.fromString('CVE-2021-43803');
87 | const b = CveId.fromString('CVE-2022-43803');
88 | expect(CveId.compare(a, b)).toBe(-1);
89 | expect(CveId.compare(b, a)).toBe(1);
90 | });
91 |
92 | it('sorts', () => {
93 | const a = CveId.fromString('CVE-2017-30000');
94 | const b = CveId.fromString('CVE-2018-10000');
95 | const c = CveId.fromString('CVE-2018-10000');
96 | const d = CveId.fromString('CVE-2018-20000');
97 | const e = CveId.fromString('CVE-2019-10000');
98 | const f = CveId.fromString('CVE-2020-40000');
99 | const list = [f, e, d, null, b, c, null, a].sort(CveId.compare);
100 | expect(list).toEqual([null, null, a, b, c, d, e, f]);
101 | });
102 | });
103 |
104 | describe('CveId.toJSON', () => {
105 | it('serializes to a string', () => {
106 | const cve = CveId.fromString('CVE-2021-43803');
107 | expect(JSON.stringify(cve)).toEqual('"CVE-2021-43803"');
108 | });
109 | });
110 |
111 | describe('CveId.toString', () => {
112 | it('serializes to a string', () => {
113 | const cve = CveId.fromString('CVE-2021-43803');
114 | expect(`${cve}`).toEqual('CVE-2021-43803');
115 | });
116 | });
117 |
--------------------------------------------------------------------------------
/tests/unit/Exceptions.test.js:
--------------------------------------------------------------------------------
1 | const {
2 | ParseException,
3 | StaleRulesException,
4 | InvalidArgumentException,
5 | InvalidVersionException,
6 | UnknownVersionException,
7 | DownloadException,
8 | InvalidRuntimeException,
9 | } = require('../../lib/Exceptions');
10 | const { NodeVersion } = require('../../lib/NodeVersion');
11 |
12 | describe('ParseException', () => {
13 | describe('fromString', () => {
14 | it('Creates an exception from a string', () => {
15 | const message = 'Test exception';
16 | const exception = ParseException.fromString(message);
17 | expect(exception.message).toEqual(message);
18 | });
19 | });
20 |
21 | describe('fromException', () => {
22 | it('creates an exception from a exception', () => {
23 | const message = 'Test exception';
24 | const exception = ParseException.fromException(new Error(message));
25 | expect(exception.message).toEqual(message);
26 | });
27 | });
28 | });
29 |
30 | describe('StaleRulesException.fromString', () => {
31 | it('Creates an exception from a string', () => {
32 | const message = 'Test exception';
33 | const exception = StaleRulesException.fromString(message);
34 | expect(exception.message).toEqual(message);
35 | });
36 | });
37 |
38 | describe('InvalidArgumentException.fromString', () => {
39 | it('Creates an exception from a string', () => {
40 | const message = 'Test exception';
41 | const exception = InvalidArgumentException.fromString(message);
42 | expect(exception.message).toEqual(message);
43 | });
44 | });
45 |
46 | describe('InvalidVersionException.fromString', () => {
47 | it('Creates an exception from a string', () => {
48 | const version = 'a.b.c';
49 | const exception = InvalidVersionException.fromString(version);
50 | expect(exception.message).toMatch(/a\.b\.c/);
51 | });
52 | });
53 |
54 | describe('UnknownVersionException.fromString', () => {
55 | it('Creates an exception from a string', () => {
56 | const version = 'a.b.c';
57 | const exception = UnknownVersionException.fromString(version);
58 | expect(exception.message).toMatch(/a\.b\.c/);
59 | });
60 | });
61 |
62 | describe('DownloadException', () => {
63 | it('creates an exception from a string', () => {
64 | const message = 'unable to download file';
65 | const exception = DownloadException.fromString('unable to download file');
66 | expect(exception.message).toMatch(message);
67 | });
68 |
69 | it('creates an exception from a exception', () => {
70 | const message = 'Test exception';
71 | const exception = DownloadException.fromException(new Error(message));
72 | expect(exception.message).toEqual(message);
73 | });
74 | });
75 |
76 | describe('InvalidRuntimeException', () => {
77 | it('creates an exception from a version', () => {
78 | const exception = InvalidRuntimeException.fromVersion(new NodeVersion(12, 0, 0));
79 | expect(exception.message).toMatch(/12\.0\.0/);
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/tests/unit/NodeRelease.test.js:
--------------------------------------------------------------------------------
1 | const { NodeRelease } = require('../../lib/NodeRelease');
2 | const { NodeVersion } = require('../../lib/NodeVersion');
3 | const { CveId } = require('../../lib/CveId');
4 |
5 | const NODE_VERSION = new NodeVersion(17, 0, 0);
6 |
7 | describe('NodeRelease::addPatchedCveId', () => {
8 | it('keeps the CVE list sorted and duplicate free', () => {
9 | const a = CveId.fromString('CVE-2019-50000');
10 | const b = CveId.fromString('CVE-2020-40000');
11 | const c = CveId.fromString('CVE-2020-50000');
12 | const cc = CveId.fromString('CVE-2020-50000');
13 | const d = CveId.fromString('CVE-2020-50001');
14 | const e = CveId.fromString('CVE-2021-50000');
15 |
16 | const nodeRelease = new NodeRelease(NODE_VERSION);
17 | nodeRelease.addPatchedCveId(c);
18 | nodeRelease.addPatchedCveId(cc);
19 | nodeRelease.addPatchedCveId(d);
20 | nodeRelease.addPatchedCveId(b);
21 | nodeRelease.addPatchedCveId(a);
22 | nodeRelease.addPatchedCveId(e);
23 | expect(nodeRelease.patchedCveIds).toEqual([a, b, c, d, e]);
24 | });
25 | });
26 |
27 | describe('NodeRelease::compare', () => {
28 | it('compares less than', () => {
29 | const first = new NodeRelease(new NodeVersion(17, 0, 0));
30 | const second = new NodeRelease(new NodeVersion(18, 0, 0));
31 | const compareResult = NodeRelease.compare(first, second);
32 | expect(compareResult).toBeLessThan(0);
33 | });
34 |
35 | it('compares greater than', () => {
36 | const first = new NodeRelease(new NodeVersion(17, 0, 0));
37 | const second = new NodeRelease(new NodeVersion(16, 0, 0));
38 | const compareResult = NodeRelease.compare(first, second);
39 | expect(compareResult).toBeGreaterThan(0);
40 | });
41 |
42 | it('compares equal to', () => {
43 | const first = new NodeRelease(new NodeVersion(17, 0, 0));
44 | const second = new NodeRelease(new NodeVersion(17, 0, 0));
45 | const compareResult = NodeRelease.compare(first, second);
46 | expect(compareResult).toBe(0);
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/tests/unit/NodeVersion.test.js:
--------------------------------------------------------------------------------
1 | const { NodeVersion } = require('../../lib/NodeVersion');
2 |
3 | describe('NodeVersion.fromString', () => {
4 | it('parses a simple version', () => {
5 | const version = NodeVersion.fromString('1.2.3');
6 | expect(version).not.toBeNull();
7 | expect(version.major).toBe(1);
8 | expect(version.minor).toBe(2);
9 | expect(version.patch).toBe(3);
10 | });
11 |
12 | it('does not parse empty string', () => {
13 | const version = NodeVersion.fromString('');
14 | expect(version).toBeNull();
15 | });
16 |
17 | it('does not parse single number', () => {
18 | const version = NodeVersion.fromString('1');
19 | expect(version).toBeNull();
20 | });
21 |
22 | it('does not parse double number', () => {
23 | const version = NodeVersion.fromString('1.2');
24 | expect(version).toBeNull();
25 | });
26 |
27 | it('does not parse 4 numbers', () => {
28 | const version = NodeVersion.fromString('1.2.3.4');
29 | expect(version).toBeNull();
30 | });
31 |
32 | it('does not parse alpha chars in major', () => {
33 | const version = NodeVersion.fromString('a.2.3');
34 | expect(version).toBeNull();
35 | });
36 |
37 | it('does not parse alpha chars in minor', () => {
38 | const version = NodeVersion.fromString('1.2b.3');
39 | expect(version).toBeNull();
40 | });
41 |
42 | it('does not parse alpha chars in patch', () => {
43 | const version = NodeVersion.fromString('1.2.3c');
44 | expect(version).toBeNull();
45 | });
46 |
47 | it('does not parse multiline', () => {
48 | const version = NodeVersion.fromString('1.2.3\n\nextra-stuff\n');
49 | expect(version).toBeNull();
50 | });
51 |
52 | it('does not parse null', () => {
53 | const version = NodeVersion.fromString(null);
54 | expect(version).toBeNull();
55 | });
56 |
57 | it('does not parse negative versions', () => {
58 | const version = NodeVersion.fromString('1.-2.3');
59 | expect(version).toBeNull();
60 | });
61 |
62 | it('parses large versions', () => {
63 | const version = NodeVersion.fromString('1000.200000.3000000000');
64 | expect(version).not.toBeNull();
65 | expect(version.major).toBe(1000);
66 | expect(version.minor).toBe(200000);
67 | expect(version.patch).toBe(3000000000);
68 | });
69 | });
70 |
71 | describe('NodeVersion.compare', () => {
72 | it('compares equal versions', () => {
73 | const a = new NodeVersion(1, 2, 3);
74 | const b = new NodeVersion(1, 2, 3);
75 | expect(NodeVersion.compare(a, b)).toBe(0);
76 | expect(NodeVersion.compare(b, a)).toBe(0);
77 | });
78 |
79 | it('compares null', () => {
80 | const a = new NodeVersion(1, 2, 3);
81 | expect(NodeVersion.compare(a, null)).toBe(1);
82 | expect(NodeVersion.compare(null, a)).toBe(-1);
83 | });
84 |
85 | it('compares different patch version', () => {
86 | const a = new NodeVersion(1, 2, 3);
87 | const b = new NodeVersion(1, 2, 4);
88 | expect(NodeVersion.compare(a, b)).toBe(-1);
89 | expect(NodeVersion.compare(b, a)).toBe(1);
90 | });
91 |
92 | it('compares different minor version', () => {
93 | const a = new NodeVersion(1, 2, 3);
94 | const b = new NodeVersion(1, 1, 3);
95 | expect(NodeVersion.compare(a, b)).toBe(1);
96 | expect(NodeVersion.compare(b, a)).toBe(-1);
97 | });
98 |
99 | it('compares different major version', () => {
100 | const a = new NodeVersion(2, 2, 3);
101 | const b = new NodeVersion(1, 2, 3);
102 | expect(NodeVersion.compare(a, b)).toBe(1);
103 | expect(NodeVersion.compare(b, a)).toBe(-1);
104 | });
105 |
106 | it('sorts', () => {
107 | const a = new NodeVersion(1, 1, 1);
108 | const b = new NodeVersion(1, 1, 2);
109 | const c = new NodeVersion(1, 2, 0);
110 | const d = new NodeVersion(2, 0, 0);
111 | const e = new NodeVersion(2, 0, 0);
112 | const f = new NodeVersion(2, 0, 10);
113 | const g = new NodeVersion(2, 1, 0);
114 | const list = [g, f, e, d, null, b, c, null, a].sort(NodeVersion.compare);
115 | expect(list).toEqual([null, null, a, b, c, d, e, f, g]);
116 | });
117 | });
118 |
119 | describe('NodeVersion.toJSON', () => {
120 | it('serializes to a string', () => {
121 | const version = NodeVersion.fromString('1.2.3');
122 | expect(JSON.stringify(version)).toEqual('"1.2.3"');
123 | });
124 | });
125 |
126 | describe('NodeVersion.toString', () => {
127 | it('serializes to a string', () => {
128 | const version = NodeVersion.fromString('1.2.3');
129 | expect(`${version}`).toEqual('1.2.3');
130 | });
131 | });
132 |
--------------------------------------------------------------------------------
/tests/unit/Rules.test.js:
--------------------------------------------------------------------------------
1 | const { StaleRulesException } = require('../../lib/Exceptions');
2 | const { Rules } = require('../../lib/Rules');
3 |
4 | describe('Rules.assertFreshRules', () => {
5 | it('throws on stale rules', () => {
6 | const expiredDate = new Date(new Date().getTime() / 1000 - 1209601);
7 | expect(() => {
8 | Rules.assertFreshRules({
9 | lastUpdatedDate: expiredDate,
10 | });
11 | }).toThrowError(StaleRulesException);
12 | });
13 |
14 | it('does not throws on fresh rules', () => {
15 | const expiredDate = new Date(new Date().getTime() / 1000 - 1209590);
16 | expect(() => {
17 | Rules.assertFreshRules({
18 | lastUpdatedDate: expiredDate,
19 | });
20 | }).toThrowError(StaleRulesException);
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/tests/unit/SupportSchedule.test.js:
--------------------------------------------------------------------------------
1 | jest.mock('../../lib/CachedDownload');
2 | const CachedDownload = require('../../lib/CachedDownload');
3 | const { SupportSchedule } = require('../../lib/SupportSchedule');
4 |
5 | describe('SupportSchedule.parse', () => {
6 | beforeAll(() => {
7 | CachedDownload.CachedDownload.json.mockResolvedValue({
8 | 'v0.12': {
9 | start: '2015-02-06',
10 | end: '2016-12-31',
11 | },
12 | v19: {
13 | start: '2022-10-18',
14 | maintenance: '2023-04-01',
15 | end: '2023-06-01',
16 | },
17 | v20: {
18 | start: '2023-04-18',
19 | lts: '2023-10-24',
20 | maintenance: '2024-10-22',
21 | end: '2026-04-30',
22 | codename: '',
23 | },
24 | });
25 | });
26 |
27 | it('parses the mocked support schedule', async () => {
28 | const supportSchedule = await SupportSchedule.parse();
29 | expect(supportSchedule).not.toBeNull();
30 | expect(Object.keys(supportSchedule)).toEqual(['19', '20']);
31 | expect(supportSchedule['19']).not.toHaveProperty('lts');
32 | expect(supportSchedule['19'].start).toEqual(new Date('2022-10-18'));
33 | expect(supportSchedule['19'].maintenance).toEqual(new Date('2023-04-01'));
34 | expect(supportSchedule['19'].end).toEqual(new Date('2023-06-01'));
35 |
36 | expect(supportSchedule['20']).toEqual({
37 | start: new Date('2023-04-18'),
38 | lts: new Date('2023-10-24'),
39 | maintenance: new Date('2024-10-22'),
40 | end: new Date('2026-04-30'),
41 | });
42 | });
43 | });
44 |
--------------------------------------------------------------------------------