├── .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 | [![Node Version Audit Logo](https://www.github.developerdan.com/node-version-audit/node-version-audit-logo.svg)](https://www.github.developerdan.com/node-version-audit/) 4 | 5 | [![Github Stars](https://img.shields.io/github/stars/lightswitch05/node-version-audit)](https://github.com/lightswitch05/php-version-audit) 6 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/lightswitch05/node-version-audit/auto-updates.yml)](https://github.com/lightswitch05/node-version-audit/actions/workflows/auto-updates.yml) 7 | [![NPM Version](https://img.shields.io/npm/v/node-version-audit)](https://www.npmjs.com/package/node-version-audit) 8 | [![Docker Pulls](https://img.shields.io/docker/pulls/lightswitch05/node-version-audit)](https://hub.docker.com/r/lightswitch05/node-version-audit) 9 | [![license](https://img.shields.io/github/license/lightswitch05/node-version-audit.svg)](https://github.com/lightswitch05/node-version-audit/blob/master/LICENSE) 10 | [![last commit](https://img.shields.io/github/last-commit/lightswitch05/node-version-audit.svg)](https://github.com/lightswitch05/node-version-audit/commits/master) 11 | [![commit activity](https://img.shields.io/github/commit-activity/y/lightswitch05/node-version-audit.svg)](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 |

43 | 44 | lightswitch05 avatar 45 | 46 | Node Version Audit: 47 | Source on Github 48 |

49 | 50 |

51 | 52 | Github Stars 57 | 58 | 62 | GitHub Workflow Status 67 | 68 | 69 | NPM Version 74 | 75 | 76 | Docker Pulls 81 | 82 | 83 | license 88 | 89 | 90 | last commit 95 | 96 | 97 | commit activity 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 |

Index

115 | 134 |
135 | 136 |
137 |

Features

138 | 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 | 370 |
371 |
372 |

License & Acknowledgments

373 | 409 |
410 |
411 | 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 | --------------------------------------------------------------------------------