├── .all-contributorsrc ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── nodejs.yml │ └── scorecard.yml ├── .gitignore ├── .npmrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── bin ├── commands │ ├── execute.ts │ ├── index.ts │ └── init.ts └── index.ts ├── eslint.config.mjs ├── package.json ├── public ├── css │ ├── print.css │ ├── reset.css │ ├── style.css │ └── themes │ │ ├── dark.css │ │ └── light.css ├── font │ ├── mononoki-Bold.woff │ ├── mononoki-Bold.woff2 │ ├── mononoki-Italic.woff │ ├── mononoki-Italic.woff2 │ ├── mononoki-Regular.woff │ └── mononoki-Regular.woff2 ├── img │ └── avatar-default.png ├── lib │ ├── chart.js │ └── md5.js └── scripts │ └── main.js ├── scripts ├── nodesecure_payload.json └── preview.ts ├── src ├── analysis │ ├── extractScannerData.ts │ ├── fetch.ts │ └── scanner.ts ├── api │ └── report.ts ├── constants.ts ├── index.ts ├── localStorage.ts ├── reporting │ ├── html.ts │ ├── index.ts │ ├── pdf.ts │ └── template.ts └── utils │ ├── charts.ts │ ├── cleanReportName.ts │ ├── cloneGITRepository.ts │ ├── formatNpmPackages.ts │ ├── index.ts │ └── runInSpinner.ts ├── test ├── api │ └── report.spec.ts ├── commands │ ├── execute.e2e-spec.ts │ └── initialize.e2e-spec.ts ├── fixtures │ └── .nodesecurerc ├── helpers │ └── reportCommandRunner.ts └── utils │ ├── cleanReportName.spec.ts │ ├── cloneGITRepository.spec.ts │ └── formatNpmPackages.spec.ts ├── tsconfig.json └── views └── template.html /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "fraxken", 10 | "name": "Gentilhomme", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/4438263?v=4", 12 | "profile": "https://www.linkedin.com/in/thomas-gentilhomme/", 13 | "contributions": [ 14 | "code", 15 | "doc", 16 | "review", 17 | "security", 18 | "bug" 19 | ] 20 | }, 21 | { 22 | "login": "Kawacrepe", 23 | "name": "Vincent Dhennin", 24 | "avatar_url": "https://avatars.githubusercontent.com/u/40260517?v=4", 25 | "profile": "https://github.com/Kawacrepe", 26 | "contributions": [ 27 | "code", 28 | "doc", 29 | "review" 30 | ] 31 | }, 32 | { 33 | "login": "Rossb0b", 34 | "name": "Nicolas Hallaert", 35 | "avatar_url": "https://avatars.githubusercontent.com/u/39910164?v=4", 36 | "profile": "https://github.com/Rossb0b", 37 | "contributions": [ 38 | "doc" 39 | ] 40 | }, 41 | { 42 | "login": "Max2810", 43 | "name": "Max", 44 | "avatar_url": "https://avatars.githubusercontent.com/u/53535185?v=4", 45 | "profile": "https://github.com/Max2810", 46 | "contributions": [ 47 | "code" 48 | ] 49 | }, 50 | { 51 | "login": "fabnguess", 52 | "name": "Kouadio Fabrice Nguessan", 53 | "avatar_url": "https://avatars.githubusercontent.com/u/72697416?v=4", 54 | "profile": "https://github.com/fabnguess", 55 | "contributions": [ 56 | "maintenance" 57 | ] 58 | }, 59 | { 60 | "login": "halcin", 61 | "name": "halcin", 62 | "avatar_url": "https://avatars.githubusercontent.com/u/7302407?v=4", 63 | "profile": "https://github.com/halcin", 64 | "contributions": [ 65 | "bug", 66 | "code", 67 | "a11y" 68 | ] 69 | }, 70 | { 71 | "login": "PierreDemailly", 72 | "name": "PierreDemailly", 73 | "avatar_url": "https://avatars.githubusercontent.com/u/39910767?v=4", 74 | "profile": "https://github.com/PierreDemailly", 75 | "contributions": [ 76 | "code" 77 | ] 78 | }, 79 | { 80 | "login": "lilleeleex", 81 | "name": "Lilleeleex", 82 | "avatar_url": "https://avatars.githubusercontent.com/u/55240847?v=4", 83 | "profile": "https://github.com/lilleeleex", 84 | "contributions": [ 85 | "code" 86 | ] 87 | }, 88 | { 89 | "login": "Nishi46", 90 | "name": "Nishi", 91 | "avatar_url": "https://avatars.githubusercontent.com/u/46855953?v=4", 92 | "profile": "https://www.linkedin.com/in/nk-3906b7206/", 93 | "contributions": [ 94 | "doc" 95 | ] 96 | }, 97 | { 98 | "login": "orlando1108", 99 | "name": "Erwan Raulo", 100 | "avatar_url": "https://avatars.githubusercontent.com/u/22614778?v=4", 101 | "profile": "https://github.com/orlando1108", 102 | "contributions": [ 103 | "code" 104 | ] 105 | } 106 | ], 107 | "contributorsPerLine": 7, 108 | "projectName": "report", 109 | "projectOwner": "NodeSecure", 110 | "repoType": "github", 111 | "repoHost": "https://github.com", 112 | "skipCi": true, 113 | "commitConvention": "angular", 114 | "commitType": "docs" 115 | } 116 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | 12 | [*.md] 13 | max_line_length = off 14 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | versioning-strategy: widen 6 | schedule: 7 | interval: "weekly" 8 | groups: 9 | dependencies: 10 | dependency-type: "production" 11 | development-dependencies: 12 | dependency-type: "development" 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "monthly" 17 | groups: 18 | github-actions: 19 | patterns: 20 | - "*" 21 | -------------------------------------------------------------------------------- /.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: "0 0 * * 1" 22 | 23 | permissions: 24 | contents: read 25 | 26 | jobs: 27 | analyze: 28 | name: Analyze 29 | runs-on: ubuntu-latest 30 | permissions: 31 | actions: read 32 | contents: read 33 | security-events: write 34 | 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | language: ["javascript"] 39 | # CodeQL supports [ $supported-codeql-languages ] 40 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 41 | 42 | steps: 43 | - name: Harden Runner 44 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 45 | with: 46 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 47 | 48 | - name: Checkout repository 49 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 50 | 51 | # Initializes the CodeQL tools for scanning. 52 | - name: Initialize CodeQL 53 | uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 54 | with: 55 | languages: ${{ matrix.language }} 56 | # If you wish to specify custom queries, you can do so here or in a config file. 57 | # By default, queries listed here will override any specified in a config file. 58 | # Prefix the list here with "+" to use these queries and those in the config file. 59 | 60 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 61 | # If this step fails, then you should remove it and run the build manually (see below) 62 | - name: Autobuild 63 | uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 64 | 65 | # ℹ️ Command-line programs to run using the OS shell. 66 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 67 | 68 | # If the Autobuild fails above, remove it and uncomment the following three lines. 69 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 70 | 71 | # - run: | 72 | # echo "Run, Build Application using script" 73 | # ./location_of_script_within_repo/buildscript.sh 74 | 75 | - name: Perform CodeQL Analysis 76 | uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 77 | with: 78 | category: "/language:${{matrix.language}}" 79 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | runs-on: ${{matrix.os}} 15 | strategy: 16 | matrix: 17 | node-version: [20.x, 22.x] 18 | os: [ubuntu-latest] 19 | fail-fast: false 20 | steps: 21 | - name: Harden Runner 22 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 23 | with: 24 | egress-policy: audit 25 | 26 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 27 | - name: Use Node.js ${{ matrix.node-version }} 28 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | - name: Install dependencies 32 | run: npm i 33 | - name: Lint 34 | run: npm run lint 35 | - name: Run tests 36 | run: npm run test 37 | automerge: 38 | if: > 39 | github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' 40 | needs: 41 | - test 42 | runs-on: ubuntu-latest 43 | permissions: 44 | contents: write 45 | pull-requests: write 46 | steps: 47 | - name: Merge Dependabot PR 48 | uses: fastify/github-action-merge-dependabot@e820d631adb1d8ab16c3b93e5afe713450884a4a # v3.11.1 49 | with: 50 | github-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '21 18 * * 5' 14 | push: 15 | branches: [ "master" ] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | # Uncomment the permissions below if installing in a private repository. 30 | # contents: read 31 | # actions: read 32 | 33 | steps: 34 | - name: Harden Runner 35 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 36 | with: 37 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 38 | 39 | - name: "Checkout code" 40 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 41 | with: 42 | persist-credentials: false 43 | 44 | - name: "Run analysis" 45 | uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 46 | with: 47 | results_file: results.sarif 48 | results_format: sarif 49 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 50 | # - you want to enable the Branch-Protection check on a *public* repository, or 51 | # - you are installing Scorecard on a *private* repository 52 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 53 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 54 | 55 | # Public repositories: 56 | # - Publish results to OpenSSF REST API for easy access by consumers 57 | # - Allows the repository to include the Scorecard badge. 58 | # - See https://github.com/ossf/scorecard-action#publishing-results. 59 | # For private repositories: 60 | # - `publish_results` will always be set to `false`, regardless 61 | # of the value entered here. 62 | publish_results: true 63 | 64 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 65 | # format to the repository Actions tab. 66 | - name: "Upload artifact" 67 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 68 | with: 69 | name: SARIF file 70 | path: results.sarif 71 | retention-days: 5 72 | 73 | # Upload the results to GitHub's code scanning dashboard. 74 | - name: "Upload to code-scanning" 75 | uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 76 | with: 77 | sarif_file: results.sarif 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # JSDoc 61 | docs/ 62 | jsdoc/ 63 | temp/ 64 | json/ 65 | reports/ 66 | preview/ 67 | dist/ 68 | /.nodesecurerc 69 | .DS_Store 70 | 71 | # IDE 72 | .vscode 73 | jsconfig.json -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to NodeSecure 2 | 3 | Contributions to NodeSecure include code, documentation, answering user questions and 4 | running the project's infrastructure 5 | 6 | The NodeSecure project welcomes all contributions from anyone willing to work in 7 | good faith with other contributors and the community. No contribution is too 8 | small and all contributions are valued. 9 | 10 | This guide explains the process for contributing to the NodeSecure project's. 11 | 12 | ## [Code of Conduct](https://github.com/NodeSecure/Governance/blob/main/CODE_OF_CONDUCT.md) 13 | 14 | The NodeSecure project has a 15 | [Code of Conduct](https://github.com/NodeSecure/Governance/blob/main/CODE_OF_CONDUCT.md) 16 | that *all* contributors are expected to follow. This code describes the 17 | *minimum* behavior expectations for all contributors. 18 | 19 | See [details on our policy on Code of Conduct](https://github.com/NodeSecure/Governance/blob/main/COC_POLICY.md). 20 | 21 | 22 | ## Developer's Certificate of Origin 1.1 23 | 24 | By making a contribution to this project, I certify that: 25 | 26 | * (a) The contribution was created in whole or in part by me and I 27 | have the right to submit it under the open source license 28 | indicated in the file; or 29 | 30 | * (b) The contribution is based upon previous work that, to the best 31 | of my knowledge, is covered under an appropriate open source 32 | license and I have the right under that license to submit that 33 | work with modifications, whether created in whole or in part 34 | by me, under the same open source license (unless I am 35 | permitted to submit under a different license), as indicated 36 | in the file; or 37 | 38 | * (c) The contribution was provided directly to me by some other 39 | person who certified (a), (b) or (c) and I have not modified 40 | it. 41 | 42 | * (d) I understand and agree that this project and the contribution 43 | are public and that a record of the contribution (including all 44 | personal information I submit with it, including my sign-off) is 45 | maintained indefinitely and may be redistributed consistent with 46 | this project or the open source license(s) involved. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2025 NodeSecure 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | @nodesecure/report 3 |

4 | 5 |
6 | 7 | ![version](https://img.shields.io/badge/dynamic/json.svg?style=for-the-badge&url=https://raw.githubusercontent.com/NodeSecure/report/master/package.json&query=$.version&label=Version) 8 | [![OpenSSF 9 | Scorecard](https://api.securityscorecards.dev/projects/github.com/NodeSecure/report/badge?style=for-the-badge)](https://api.securityscorecards.dev/projects/github.com/NodeSecure/report) 10 | ![MIT](https://img.shields.io/github/license/NodeSecure/report.svg?style=for-the-badge) 11 | ![size](https://img.shields.io/github/languages/code-size/NodeSecure/report?style=for-the-badge) 12 | 13 |
14 | 15 | This project is designed to generate periodic security reports in both HTML and PDF formats. It leverages the [@nodesecure/scanner](https://github.com/NodeSecure/scanner) to retrieve all necessary data. 16 | 17 | | Screen1 | Screen2 | 18 | | :----------------------------------: | :----------------------------------: | 19 | | ![](https://i.imgur.com/Jhr76Ef.jpg) | ![](https://i.imgur.com/OmV7Al6.jpg) | 20 | 21 | ## Features 22 | 23 | - Automatically clones and scans Git repositories using **scanner.cwd**. 24 | - Provides a visual overview of **security threats** and quality issues for multiple Git or NPM packages. 25 | - Facilitates visualization of changes over time. 26 | - Generates reports in both **HTML** and **PDF** formats. 27 | 28 | ## Requirements 29 | 30 | - [Node.js](https://nodejs.org/en/) v20 or higher. 31 | 32 | ## Getting Started 33 | 34 | This package is available in the Node Package Repository and can be easily installed with [npm](https://docs.npmjs.com/getting-started/what-is-npm) or [yarn](https://yarnpkg.com). 35 | 36 | ```bash 37 | $ git clone https://github.com/NodeSecure/report.git 38 | $ cd report 39 | $ npm i 40 | $ npm run build 41 | $ npm link 42 | ``` 43 | 44 | After installation, the `nreport` binary will be available in your terminal. 45 | 46 | ```bash 47 | nreport initialize 48 | nreport execute 49 | ``` 50 | 51 | > [!CAUTION] 52 | > Please read the following sections to understand how to properly set up the configuration. The **initialize** command generates an incomplete basic template. 53 | 54 | ### Environment Variables 55 | 56 | To configure the project you have to register (set) environment variables on your system. These variables can be set in a **.env** file (that file must be created at the root of the project). 57 | 58 | ``` 59 | GIT_TOKEN= 60 | NODE_SECURE_TOKEN= 61 | ``` 62 | 63 | To known how to get a **GIT_TOKEN** or how to register environment variables follow our [Governance Guide](https://github.com/SlimIO/Governance/blob/master/docs/tooling.md#environment-variables). 64 | 65 | > [!NOTE] 66 | > For NODE_SECURE_TOKEN, please check the [NodeSecure CLI documentation](https://github.com/NodeSecure/cli?tab=readme-ov-file#private-registry--verdaccio). 67 | 68 | ### Configuration Example (.nodesecurerc) 69 | 70 | This uses the official NodeSecure [runtime configuration](https://github.com/NodeSecure/rc) (`@nodesecure/rc`) under the hood. 71 | 72 | ```json 73 | { 74 | "version": "1.0.0", 75 | "i18n": "english", 76 | "strategy": "github-advisory", 77 | "report": { 78 | "title": "NodeSecure Security Report", 79 | "logoUrl": "https://avatars.githubusercontent.com/u/85318671?s=200&v=4", 80 | "theme": "light", 81 | "includeTransitiveInternal": false, 82 | "reporters": ["html", "pdf"], 83 | "npm": { 84 | "organizationPrefix": "@nodesecure", 85 | "packages": ["@nodesecure/js-x-ray"] 86 | }, 87 | "git": { 88 | "organizationUrl": "https://github.com/NodeSecure", 89 | "repositories": ["vulnera"] 90 | }, 91 | "charts": [ 92 | { 93 | "name": "Extensions", 94 | "display": true, 95 | "interpolation": "d3.interpolateRainbow", 96 | "type": "bar" 97 | }, 98 | { 99 | "name": "Licenses", 100 | "display": true, 101 | "interpolation": "d3.interpolateCool", 102 | "type": "bar" 103 | }, 104 | { 105 | "name": "Warnings", 106 | "display": true, 107 | "type": "horizontalBar", 108 | "interpolation": "d3.interpolateInferno" 109 | }, 110 | { 111 | "name": "Flags", 112 | "display": true, 113 | "type": "horizontalBar", 114 | "interpolation": "d3.interpolateSinebow" 115 | } 116 | ] 117 | } 118 | } 119 | ``` 120 | 121 | The theme can be either `dark` or `light`. Themes are editable in _public/css/themes_ (feel free to PR new themes if you want). 122 | 123 | > [!NOTE] 124 | > All D3 scale-chromatic for charts can be found [here](https://github.com/d3/d3-scale-chromatic/blob/master/README.md). 125 | 126 | ## API 127 | 128 | > [!CAUTION] 129 | > The API is ESM only 130 | 131 | ### report 132 | 133 | ```ts 134 | function report( 135 | scannerDependencies: Scanner.Payload["dependencies"], 136 | reportConfig: ReportConfiguration, 137 | reportOptions?: ReportOptions 138 | ): Promise; 139 | ``` 140 | 141 | Generates and returns a PDF Buffer based on the provided report options and scanner payload. 142 | 143 | ```ts 144 | /** 145 | * Configuration dedicated for NodeSecure Report 146 | * @see https://github.com/NodeSecure/report 147 | */ 148 | export interface ReportConfiguration { 149 | /** 150 | * @default `light` 151 | */ 152 | theme?: "light" | "dark"; 153 | title: string; 154 | /** 155 | * URL to a logo to show on the final HTML/PDF Report 156 | */ 157 | logoUrl?: string; 158 | /** 159 | * Show/categorize internal dependencies as transitive 160 | * @default false 161 | */ 162 | includeTransitiveInternal?: boolean; 163 | npm?: { 164 | /** 165 | * NPM organization prefix starting with @ 166 | * @example `@nodesecure` 167 | */ 168 | organizationPrefix: string; 169 | packages: string[]; 170 | }; 171 | git?: { 172 | /** 173 | * GitHub organization URL 174 | * @example `https://github.com/NodeSecure` 175 | */ 176 | organizationUrl: string; 177 | /** 178 | * List of repositories 179 | * name are enough, no need to provide .git URL or any equivalent 180 | */ 181 | repositories: string[]; 182 | }; 183 | /** 184 | * @default html,pdf 185 | */ 186 | reporters?: ("html" | "pdf")[]; 187 | charts?: ReportChart[]; 188 | } 189 | 190 | export interface ReportChart { 191 | /** 192 | * List of available charts. 193 | */ 194 | name: "Extensions" | "Licenses" | "Warnings" | "Flags"; 195 | /** 196 | * @default true 197 | */ 198 | display?: boolean; 199 | /** 200 | * Chart.js chart type. 201 | * 202 | * @see https://www.chartjs.org/docs/latest/charts 203 | * @default `bar` 204 | */ 205 | type?: "bar" | "horizontalBar" | "polarArea" | "doughnut"; 206 | /** 207 | * D3 Interpolation color. Will be picked randomly by default if not provided. 208 | * @see https://github.com/d3/d3-scale-chromatic/blob/main/README.md 209 | */ 210 | interpolation?: string; 211 | } 212 | 213 | export interface ReportOptions { 214 | /** 215 | * Location where the report will be saved. 216 | * 217 | * If not provided, default to cwd if HTML or PDF is saved on disk, or a temp directory else. 218 | */ 219 | reportOutputLocation?: string | null; 220 | /** 221 | * Save the PDF on disk 222 | * @default false 223 | */ 224 | savePDFOnDisk?: boolean; 225 | /** 226 | * Save the HTML on disk 227 | * @default false 228 | */ 229 | saveHTMLOnDisk?: boolean; 230 | } 231 | ``` 232 | 233 | ## Scripts 234 | 235 | You can generate a preview of a report using the following NPM scripts 236 | 237 | ```bash 238 | $ npm run preview:light 239 | $ npm run preview:dark 240 | ``` 241 | 242 | ## Debug mode 243 | 244 | You can write in the file "reports/debug-pkg-repo.txt", all data generated from NPM package and GIT repository scanners using the following option. Usefull if you want to get a preview from this data set. 245 | 246 | ```bash 247 | $ nreport exec --debug 248 | ``` 249 | 250 | ## Contributors ✨ 251 | 252 | 253 | [![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors-) 254 | 255 | 256 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 |
Gentilhomme
Gentilhomme

💻 📖 👀 🛡️ 🐛
Vincent Dhennin
Vincent Dhennin

💻 📖 👀
Nicolas Hallaert
Nicolas Hallaert

📖
Max
Max

💻
Kouadio Fabrice Nguessan
Kouadio Fabrice Nguessan

🚧
halcin
halcin

🐛 💻 ️️️️♿️
PierreDemailly
PierreDemailly

💻
Lilleeleex
Lilleeleex

💻
Nishi
Nishi

📖
Erwan Raulo
Erwan Raulo

💻
279 | 280 | 281 | 282 | 283 | 284 | 285 | ## License 286 | 287 | MIT 288 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | To report a security issue, please [publish a private security advisory](https://github.com/NodeSecure/report/security/advisories) with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue. 4 | 5 | Our vulnerability management team will respond within one week. If the issue is confirmed as a vulnerability, we will open a Security Advisory and acknowledge your contributions as part of it. This project follows a 90 day disclosure timeline. 6 | -------------------------------------------------------------------------------- /bin/commands/execute.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import fs from "node:fs/promises"; 3 | import { writeFileSync } from "node:fs"; 4 | import path from "node:path"; 5 | import { inspect } from "node:util"; 6 | 7 | // Import Third-party Dependencies 8 | import * as rc from "@nodesecure/rc"; 9 | import kleur from "kleur"; 10 | 11 | // Import Internal Dependencies 12 | import { store } from "../../src/localStorage.js"; 13 | 14 | import { fetchPackagesAndRepositoriesData } from "../../src/analysis/fetch.js"; 15 | import * as CONSTANTS from "../../src/constants.js"; 16 | import * as reporting from "../../src/reporting/index.js"; 17 | 18 | // CONSTANTS 19 | const kReadConfigOptions = { 20 | createIfDoesNotExist: false, 21 | createMode: "report" 22 | } as const; 23 | 24 | export interface ExecuteOptions { 25 | debug?: boolean; 26 | } 27 | 28 | export async function execute(options: ExecuteOptions = {}) { 29 | const { debug: debugMode } = options; 30 | 31 | if (debugMode) { 32 | console.log(kleur.bgMagenta().bold(" > Debug mode enabled \n")); 33 | } 34 | 35 | const [configResult] = await Promise.all([ 36 | rc.read(process.cwd(), kReadConfigOptions), 37 | init() 38 | ]); 39 | 40 | const config = configResult.unwrap(); 41 | const { report } = config; 42 | 43 | if (!report) { 44 | throw new Error("A valid configuration is required"); 45 | } 46 | if (!report.reporters || report.reporters.length === 0) { 47 | throw new Error("At least one reporter must be selected (either 'HTML' or 'PDF')"); 48 | } 49 | 50 | console.log(`>> title: ${kleur.cyan().bold(report.title)}`); 51 | console.log(`>> reporters: ${kleur.magenta().bold(report.reporters.join(","))}\n`); 52 | 53 | store.run(config, async() => { 54 | try { 55 | const data = await fetchPackagesAndRepositoriesData(); 56 | if (debugMode) { 57 | debug(data); 58 | } 59 | await reporting.proceed(data); 60 | console.log(kleur.green().bold("\n>> Security report successfully generated! Enjoy 🚀.\n")); 61 | } 62 | catch (error) { 63 | console.error(error); 64 | } 65 | finally { 66 | await fs.rm(CONSTANTS.DIRS.CLONES, { 67 | recursive: true, force: true 68 | }); 69 | } 70 | }); 71 | } 72 | 73 | function init() { 74 | const directoriesToInitialize = [ 75 | CONSTANTS.DIRS.JSON, 76 | CONSTANTS.DIRS.CLONES, 77 | CONSTANTS.DIRS.REPORTS 78 | ]; 79 | 80 | return Promise.all( 81 | directoriesToInitialize.map((dir) => fs.mkdir(dir, { recursive: true })) 82 | ); 83 | } 84 | 85 | function debug(obj: any) { 86 | const filePath = path.join(CONSTANTS.DIRS.REPORTS, "debug-pkg-repo.txt"); 87 | writeFileSync(filePath, inspect(obj, { showHidden: true, depth: null }), "utf8"); 88 | } 89 | 90 | -------------------------------------------------------------------------------- /bin/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./execute.js"; 2 | export * from "./init.js"; 3 | -------------------------------------------------------------------------------- /bin/commands/init.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import * as rc from "@nodesecure/rc"; 3 | import kleur from "kleur"; 4 | 5 | export async function init() { 6 | const configLocation = process.cwd(); 7 | 8 | const result = await rc.read(configLocation, { 9 | createIfDoesNotExist: true, 10 | createMode: "report" 11 | }); 12 | 13 | if (result.ok) { 14 | console.log(kleur.green().bold( 15 | "Successfully generated NodeSecure runtime configuration at current location\n" 16 | )); 17 | } 18 | else { 19 | throw new Error( 20 | `Unable to initialize NodeSecure runtime configuration at '${configLocation}'`, 21 | { cause: result.val } 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /bin/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --no-warnings 2 | 3 | // Import Node.js Dependencies 4 | import fs from "node:fs"; 5 | 6 | // Import Third-party Dependencies 7 | import sade from "sade"; 8 | import kleur from "kleur"; 9 | 10 | // Import Internal Dependencies 11 | import * as commands from "./commands/index.js"; 12 | 13 | console.log(kleur.grey().bold(`\n > Executing nreport at: ${kleur.yellow().bold(process.cwd())}\n`)); 14 | 15 | const { version } = JSON.parse( 16 | fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8") 17 | ); 18 | const cli = sade("nreport").version(version); 19 | 20 | cli 21 | .command("execute") 22 | .option("-d, --debug", "Enable debug mode", false) 23 | .alias("exec") 24 | .describe("Execute report at the current working dir with current configuration.") 25 | .example("nreport exec") 26 | .action(commands.execute); 27 | 28 | cli 29 | .command("initialize") 30 | .alias("init") 31 | .describe("Initialize default report configuration") 32 | .example("nreport init") 33 | .action(commands.init); 34 | 35 | cli.parse(process.argv); 36 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { typescriptConfig, globals } from "@openally/config.eslint"; 2 | 3 | export default typescriptConfig({ 4 | languageOptions: { 5 | globals: { 6 | ...globals.browser 7 | } 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodesecure/report", 3 | "version": "3.0.0", 4 | "description": "NodeSecure HTML & PDF graphic security report", 5 | "main": "./dist/src/index.js", 6 | "type": "module", 7 | "bin": { 8 | "nreport": "./dist/bin/index.js" 9 | }, 10 | "exports": { 11 | ".": { 12 | "import": "./dist/src/index.js" 13 | } 14 | }, 15 | "scripts": { 16 | "build": "tsc && npm run build:views && npm run build:public", 17 | "build:views": "rimraf dist/views && cp -r views dist/views", 18 | "build:public": "rimraf dist/public && cp -r public dist/public", 19 | "lint": "eslint src test bin scripts", 20 | "test-only": "glob -c \"tsx --test-reporter=spec --test\" \"./test/**/*.spec.ts\"", 21 | "test": "c8 --all --src ./src -r html npm run test-only", 22 | "test:e2e": "glob -c \"tsx -r dotenv/config --test-reporter=spec --test\" \"./test/**/*.e2e-spec.ts\"", 23 | "preview:light": "tsx --no-warnings ./scripts/preview.js --theme light", 24 | "preview:dark": "tsx --no-warnings ./scripts/preview.js --theme dark", 25 | "prepublishOnly": "npm run build" 26 | }, 27 | "files": [ 28 | "dist" 29 | ], 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/NodeSecure/report.git" 33 | }, 34 | "keywords": [ 35 | "security", 36 | "report", 37 | "nodesecure", 38 | "pdf", 39 | "html", 40 | "chart" 41 | ], 42 | "author": "NodeSecure", 43 | "license": "MIT", 44 | "bugs": { 45 | "url": "https://github.com/NodeSecure/report/issues" 46 | }, 47 | "homepage": "https://github.com/NodeSecure/report#readme", 48 | "dependencies": { 49 | "@nodesecure/flags": "^3.0.3", 50 | "@nodesecure/ossf-scorecard-sdk": "^3.2.1", 51 | "@nodesecure/rc": "^4.0.0", 52 | "@nodesecure/scanner": "^6.0.2", 53 | "@nodesecure/utils": "^2.2.0", 54 | "@openally/mutex": "^1.0.0", 55 | "@topcli/spinner": "^3.0.0", 56 | "esbuild": "^0.25.0", 57 | "filenamify": "^6.0.0", 58 | "kleur": "^4.1.5", 59 | "puppeteer": "24.10.0", 60 | "sade": "^1.8.1", 61 | "zup": "0.0.2" 62 | }, 63 | "devDependencies": { 64 | "@openally/config.eslint": "^2.1.0", 65 | "@openally/config.typescript": "^1.0.3", 66 | "@types/node": "^22.2.0", 67 | "c8": "^10.1.2", 68 | "glob": "^11.0.0", 69 | "open": "^10.1.0", 70 | "rimraf": "^6.0.1", 71 | "tsx": "^4.19.2", 72 | "typescript": "^5.7.2" 73 | }, 74 | "engines": { 75 | "node": ">=20" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /public/css/print.css: -------------------------------------------------------------------------------- 1 | @media print { 2 | 3 | html, 4 | body { 5 | width: 220mm; 6 | margin: 0; 7 | padding: 0; 8 | -webkit-print-color-adjust: exact; 9 | } 10 | 11 | h2 { 12 | break-before: always; 13 | break-after: always; 14 | } 15 | 16 | .page>section { 17 | break-after: always; 18 | } 19 | 20 | .box-stats-container+.box-stats-container { 21 | border-top: none !important; 22 | } 23 | 24 | .box-stats-container:not(.charts) { 25 | break-inside: avoid; 26 | } 27 | 28 | .box-stats-container.charts { 29 | break-inside: avoid; 30 | align-items: center; 31 | background: none !important; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/css/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | 27 | /* HTML5 display-role reset for older browsers */ 28 | article, aside, details, figcaption, figure, 29 | footer, header, hgroup, menu, nav, section { 30 | display: block; 31 | } 32 | body { 33 | line-height: 1; 34 | } 35 | ol, ul { 36 | list-style: none; 37 | } 38 | blockquote, q { 39 | quotes: none; 40 | } 41 | blockquote:before, blockquote:after, 42 | q:before, q:after { 43 | content: ''; 44 | content: none; 45 | } 46 | table { 47 | border-spacing: 0; 48 | } 49 | 50 | @font-face{ 51 | font-family: "mononoki"; 52 | src: url("../font/mononoki-Regular.woff"); 53 | font-style: normal; 54 | } 55 | 56 | @font-face{ 57 | font-family: "mononoki"; 58 | src: url("../font/mononoki-Bold.woff"); 59 | font-style: bold; 60 | } 61 | 62 | @font-face{ 63 | font-family: "mononoki"; 64 | src: url("../font/mononoki-Italic.woff"); 65 | font-style: italic; 66 | } 67 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | @import './print.css'; 2 | @import './reset.css'; 3 | 4 | body { 5 | color: var(--primary-text-color); 6 | font-family: "Roboto", sans-serif; 7 | display: flex; 8 | justify-content: center; 9 | position: relative; 10 | } 11 | 12 | canvas { 13 | max-height: 380px !important; 14 | margin-top: var(--default-margin); 15 | } 16 | 17 | hr { 18 | width: 90%; 19 | } 20 | 21 | h1, 22 | h2, 23 | h3 { 24 | font-family: "mononoki"; 25 | } 26 | 27 | h2, 28 | h3 { 29 | text-align: center; 30 | padding: 5px; 31 | color: var(--h2-h3-text-color); 32 | } 33 | 34 | h1 { 35 | font-size: 3em; 36 | font-weight: bold; 37 | } 38 | 39 | h2 { 40 | font-size: 1.75em; 41 | color: var(--main-color); 42 | text-shadow: 1px 1px 10px var(--h2-shadow); 43 | } 44 | 45 | h2>i { 46 | margin-right: var(--default-margin); 47 | color: inherit !important; 48 | font-size: inherit !important; 49 | } 50 | 51 | h3 { 52 | font-size: 1.2em; 53 | text-shadow: 1px 1px 10px var(--h3-shadow); 54 | } 55 | 56 | h3>span { 57 | color: var(--main-color); 58 | margin-right: var(--default-margin); 59 | } 60 | 61 | i~h2, 62 | i~h3 { 63 | margin-top: 2px; 64 | } 65 | 66 | .fas, 67 | .fab { 68 | color: var(--primary-text-color); 69 | font-size: 20px; 70 | } 71 | 72 | div.page { 73 | width: 21cm; 74 | position: relative; 75 | display: block; 76 | } 77 | 78 | div.page header { 79 | height: 180px; 80 | border-radius: 5px; 81 | display: flex; 82 | align-items: center; 83 | justify-content: center; 84 | color: var(--primary-text-color); 85 | text-shadow: rgba(18, 19, 20, 0.5) 1px 1px 5px; 86 | position: relative; 87 | } 88 | 89 | div.page header>p { 90 | position: absolute; 91 | margin: auto; 92 | bottom: 40px; 93 | font-style: italic; 94 | color: var(--head-paragraph-color); 95 | letter-spacing: 0.7px; 96 | font-size: 15px; 97 | } 98 | 99 | div.page header div.logo { 100 | margin-right: 20px; 101 | } 102 | 103 | div.page header div.logo>img { 104 | width: 64px; 105 | height: 64px; 106 | } 107 | 108 | div.page>section+section { 109 | margin-top: var(--default-margin); 110 | } 111 | 112 | div.page>section h2 { 113 | margin: var(--default-margin) 0; 114 | justify-content: center; 115 | font-weight: bold; 116 | position: relative; 117 | } 118 | 119 | .box-stats-container { 120 | display: flex; 121 | flex-wrap: wrap; 122 | padding: var(--default-margin); 123 | padding-top: calc(var(--default-margin) * 2); 124 | position: relative; 125 | } 126 | 127 | .box-stats-container+.box-stats-container { 128 | border-top: 2px solid var(--faded-border-color); 129 | margin-top: var(--default-margin); 130 | } 131 | 132 | .box-stats-container.charts { 133 | background: var(--charts-background); 134 | border-radius: 4px; 135 | border-top: none !important; 136 | } 137 | 138 | .box-stats-container.charts .fas { 139 | font-size: 30px; 140 | } 141 | 142 | .box-stats-container.charts h3 { 143 | font-size: 1.75em; 144 | } 145 | 146 | .box-stats-container>.box-stats { 147 | flex-grow: 1; 148 | flex-basis: 200px; 149 | flex-shrink: 0; 150 | display: flex; 151 | flex-wrap: wrap; 152 | flex-direction: column; 153 | } 154 | 155 | .box-stats-container>.box-stats+.box-stats { 156 | margin-left: var(--default-margin); 157 | } 158 | 159 | .box-stats-container>.box-stats>.box-title { 160 | display: flex; 161 | flex-direction: column; 162 | align-items: center; 163 | } 164 | 165 | .box-stats-container>.box-stats-resume { 166 | display: flex; 167 | flex-wrap: wrap; 168 | margin-left: -10px; 169 | margin-top: -10px; 170 | width: 100%; 171 | } 172 | 173 | .box-stats-container>.box-stats-resume>.one-stat { 174 | display: flex; 175 | flex-basis: 210px; 176 | flex-grow: 1; 177 | flex-wrap: wrap; 178 | flex-direction: column; 179 | align-items: center; 180 | justify-content: center; 181 | height: 110px; 182 | margin-left: 10px; 183 | margin-bottom: 10px; 184 | border: 1px solid var(--faded-border-color); 185 | border-radius: 4px; 186 | } 187 | 188 | .box-stats-container>.box-stats-resume>.one-stat span { 189 | color: var(--main-color); 190 | font-weight: bold; 191 | border-bottom: 3px solid var(--main-color); 192 | margin-top: 3px; 193 | padding: 5px 2px; 194 | border-radius: 5px; 195 | letter-spacing: 0.7px; 196 | font-family: "mononoki"; 197 | } 198 | 199 | .box-stats-container>.box-stats>.box-avatar { 200 | margin-top: var(--default-margin); 201 | display: flex; 202 | flex-wrap: wrap; 203 | overflow-y: auto; 204 | justify-content: center; 205 | } 206 | 207 | .box-avatar .avatar { 208 | position: relative; 209 | flex-basis: 50px; 210 | width: 50px; 211 | height: 50px; 212 | overflow: hidden; 213 | margin-left: 5px; 214 | margin-top: 5px; 215 | border-radius: 4px; 216 | border: 2px solid var(--avatar-border-color); 217 | box-sizing: border-box; 218 | } 219 | 220 | .box-avatar .avatar img { 221 | width: 58px; 222 | height: 58px; 223 | } 224 | 225 | .box-avatar .avatar p { 226 | font-size: 12px; 227 | border-radius: 2px; 228 | padding: 3px; 229 | background-color: #263238; 230 | color: #FFF; 231 | position: absolute; 232 | bottom: 2px; 233 | right: 2px; 234 | font-weight: bold; 235 | } 236 | 237 | .box-info { 238 | margin-top: var(--default-margin); 239 | margin-bottom: var(--default-margin); 240 | display: flex; 241 | flex-wrap: wrap; 242 | overflow-y: auto; 243 | align-items: center; 244 | justify-content: center; 245 | padding: 10px 15px; 246 | letter-spacing: 0.5px; 247 | border-radius: 6px; 248 | color: var(--info-box-background); 249 | border: 2px dashed var(--info-box-background); 250 | box-sizing: border-box; 251 | } 252 | 253 | .box-info>i { 254 | color: inherit !important; 255 | margin-right: 15px; 256 | } 257 | 258 | ul { 259 | margin-top: var(--default-margin); 260 | } 261 | ul + ul { 262 | margin-right: 20px; 263 | } 264 | 265 | ul>li { 266 | height: 25px; 267 | padding: 0 20px; 268 | display: flex; 269 | align-items: center; 270 | border-bottom: 1px solid var(--primary-border-color); 271 | } 272 | 273 | ul>li:not(:first-child) { 274 | margin-top: 3px; 275 | } 276 | 277 | ul>li>a { 278 | display: flex; 279 | flex-grow: 1; 280 | text-decoration: none; 281 | letter-spacing: 0.5px; 282 | font-family: "mononoki"; 283 | color: var(--secondary-text-color); 284 | } 285 | 286 | ul>li>p,ul>li>span { 287 | font-size: 18px; 288 | } 289 | 290 | ul>li>p>.emoji { 291 | cursor: default; 292 | font-size: 17px; 293 | flex-shrink: 0; 294 | margin-left: 10px; 295 | } 296 | 297 | ul>li:hover>a { 298 | cursor: pointer; 299 | color: var(--li-hover-color); 300 | } 301 | 302 | li[role=link]:focus { 303 | outline: 2px solid black; 304 | } 305 | 306 | .scorecard-item a { 307 | justify-content: space-between; 308 | } 309 | 310 | .scorecard-item .score { 311 | width: 50px; 312 | font-weight: bold; 313 | text-align: right; 314 | } 315 | 316 | .scorecard-item .score.green { 317 | color: rgb(113 203 45); 318 | } 319 | 320 | .scorecard-item .score.red { 321 | color: rgb(219 80 58); 322 | } 323 | 324 | .scorecard-item .score.orange { 325 | color: rgb(252 196 39); 326 | } 327 | 328 | .scorecard-item .score.blue { 329 | color: rgb(39 144 252); 330 | } 331 | 332 | .print-only { 333 | display: none; 334 | } 335 | 336 | .emoji-legend { 337 | margin-top: 15px; 338 | } 339 | 340 | .emoji-legend li:not(:first-child) { 341 | margin-top: 6px; 342 | } 343 | 344 | @media print { 345 | .print-only { 346 | display: block; 347 | display: 100%; 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /public/css/themes/dark.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-text-color: whitesmoke; 3 | --secondary-text-color: rgb(119, 161, 170); 4 | --primary-border-color: rgba(55, 71, 79, 0.25); 5 | --faded-border-color: rgb(78, 99, 109, 0.1); 6 | --head-paragraph-color: #546E7A; 7 | --main-color: #FFEA00; 8 | --default-margin: 15px; 9 | --avatar-border-color: #FFF; 10 | --li-hover-color: #db9d02; 11 | --h2-h3-text-color: #FFFDE7; 12 | --h2-shadow: rgba(49, 46, 38, 0.609); 13 | --h3-shadow: rgba(173, 131, 16, 0.609); 14 | --charts-background: rgba(20, 20, 20, 0.1); 15 | --info-box-background: #673AB7; 16 | } 17 | 18 | body { 19 | background: rgb(38,50,56); 20 | background: radial-gradient(ellipse at center, rgba(38,50,56,1) 0%,rgba(19,25,30,1) 100%); 21 | } 22 | 23 | @media print { 24 | html, body { 25 | background: rgb(38,50,56); 26 | background: radial-gradient(ellipse at center, rgba(38,50,56,1) 0%,rgba(19,25,30,1) 100%); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /public/css/themes/light.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-text-color: #263238; 3 | --secondary-text-color: black; 4 | --primary-border-color: rgba(55, 71, 79, 0.1); 5 | --faded-border-color: rgba(78, 99, 109, 0.1); 6 | --head-paragraph-color: black; 7 | --main-color: #304FFE; 8 | --default-margin: 15px; 9 | --avatar-border-color: #FFF; 10 | --li-hover-color: #338199; 11 | --h2-h3-text-color: #1976D2; 12 | --h2-shadow: rgba(3, 8, 17, 0.137); 13 | --h3-shadow: rgba(51, 63, 70, 0.26); 14 | --charts-background: rgba(230, 230, 230, 0.1); 15 | --info-box-background: #448AFF; 16 | } 17 | 18 | .fas, .fab { 19 | color: #546E7A !important; 20 | } 21 | 22 | body { 23 | background: rgb(238,238,238); 24 | background: radial-gradient(ellipse at center, rgba(238,238,238,1) 0%,rgba(255,255,255,1) 100%); 25 | } 26 | 27 | @media print { 28 | html, body { 29 | background: rgb(238,238,238); 30 | background: radial-gradient(ellipse at center, rgba(238,238,238,1) 0%,rgba(255,255,255,1) 100%); 31 | } 32 | } 33 | 34 | header, h2, h3{ 35 | text-shadow: none !important; 36 | } 37 | 38 | h3 > span { 39 | color: #304FFE !important; 40 | } 41 | 42 | .box-stats-container.charts { 43 | background: none !important; 44 | } 45 | 46 | .avatar { 47 | box-shadow: 1px 1px 2px rgba(20, 20, 20, 0.2); 48 | } 49 | .avatar > p { 50 | background-color: #0D47A1 !important; 51 | } 52 | -------------------------------------------------------------------------------- /public/font/mononoki-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodeSecure/report/2f40fc43e66b288a1e4d36da60d88c075d3d9c2e/public/font/mononoki-Bold.woff -------------------------------------------------------------------------------- /public/font/mononoki-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodeSecure/report/2f40fc43e66b288a1e4d36da60d88c075d3d9c2e/public/font/mononoki-Bold.woff2 -------------------------------------------------------------------------------- /public/font/mononoki-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodeSecure/report/2f40fc43e66b288a1e4d36da60d88c075d3d9c2e/public/font/mononoki-Italic.woff -------------------------------------------------------------------------------- /public/font/mononoki-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodeSecure/report/2f40fc43e66b288a1e4d36da60d88c075d3d9c2e/public/font/mononoki-Italic.woff2 -------------------------------------------------------------------------------- /public/font/mononoki-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodeSecure/report/2f40fc43e66b288a1e4d36da60d88c075d3d9c2e/public/font/mononoki-Regular.woff -------------------------------------------------------------------------------- /public/font/mononoki-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodeSecure/report/2f40fc43e66b288a1e4d36da60d88c075d3d9c2e/public/font/mononoki-Regular.woff2 -------------------------------------------------------------------------------- /public/img/avatar-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodeSecure/report/2f40fc43e66b288a1e4d36da60d88c075d3d9c2e/public/img/avatar-default.png -------------------------------------------------------------------------------- /public/lib/md5.js: -------------------------------------------------------------------------------- 1 | function md5cycle(x, k) { 2 | var a = x[0], b = x[1], c = x[2], d = x[3]; 3 | 4 | a = ff(a, b, c, d, k[0], 7, -680876936); 5 | d = ff(d, a, b, c, k[1], 12, -389564586); 6 | c = ff(c, d, a, b, k[2], 17, 606105819); 7 | b = ff(b, c, d, a, k[3], 22, -1044525330); 8 | a = ff(a, b, c, d, k[4], 7, -176418897); 9 | d = ff(d, a, b, c, k[5], 12, 1200080426); 10 | c = ff(c, d, a, b, k[6], 17, -1473231341); 11 | b = ff(b, c, d, a, k[7], 22, -45705983); 12 | a = ff(a, b, c, d, k[8], 7, 1770035416); 13 | d = ff(d, a, b, c, k[9], 12, -1958414417); 14 | c = ff(c, d, a, b, k[10], 17, -42063); 15 | b = ff(b, c, d, a, k[11], 22, -1990404162); 16 | a = ff(a, b, c, d, k[12], 7, 1804603682); 17 | d = ff(d, a, b, c, k[13], 12, -40341101); 18 | c = ff(c, d, a, b, k[14], 17, -1502002290); 19 | b = ff(b, c, d, a, k[15], 22, 1236535329); 20 | 21 | a = gg(a, b, c, d, k[1], 5, -165796510); 22 | d = gg(d, a, b, c, k[6], 9, -1069501632); 23 | c = gg(c, d, a, b, k[11], 14, 643717713); 24 | b = gg(b, c, d, a, k[0], 20, -373897302); 25 | a = gg(a, b, c, d, k[5], 5, -701558691); 26 | d = gg(d, a, b, c, k[10], 9, 38016083); 27 | c = gg(c, d, a, b, k[15], 14, -660478335); 28 | b = gg(b, c, d, a, k[4], 20, -405537848); 29 | a = gg(a, b, c, d, k[9], 5, 568446438); 30 | d = gg(d, a, b, c, k[14], 9, -1019803690); 31 | c = gg(c, d, a, b, k[3], 14, -187363961); 32 | b = gg(b, c, d, a, k[8], 20, 1163531501); 33 | a = gg(a, b, c, d, k[13], 5, -1444681467); 34 | d = gg(d, a, b, c, k[2], 9, -51403784); 35 | c = gg(c, d, a, b, k[7], 14, 1735328473); 36 | b = gg(b, c, d, a, k[12], 20, -1926607734); 37 | 38 | a = hh(a, b, c, d, k[5], 4, -378558); 39 | d = hh(d, a, b, c, k[8], 11, -2022574463); 40 | c = hh(c, d, a, b, k[11], 16, 1839030562); 41 | b = hh(b, c, d, a, k[14], 23, -35309556); 42 | a = hh(a, b, c, d, k[1], 4, -1530992060); 43 | d = hh(d, a, b, c, k[4], 11, 1272893353); 44 | c = hh(c, d, a, b, k[7], 16, -155497632); 45 | b = hh(b, c, d, a, k[10], 23, -1094730640); 46 | a = hh(a, b, c, d, k[13], 4, 681279174); 47 | d = hh(d, a, b, c, k[0], 11, -358537222); 48 | c = hh(c, d, a, b, k[3], 16, -722521979); 49 | b = hh(b, c, d, a, k[6], 23, 76029189); 50 | a = hh(a, b, c, d, k[9], 4, -640364487); 51 | d = hh(d, a, b, c, k[12], 11, -421815835); 52 | c = hh(c, d, a, b, k[15], 16, 530742520); 53 | b = hh(b, c, d, a, k[2], 23, -995338651); 54 | 55 | a = ii(a, b, c, d, k[0], 6, -198630844); 56 | d = ii(d, a, b, c, k[7], 10, 1126891415); 57 | c = ii(c, d, a, b, k[14], 15, -1416354905); 58 | b = ii(b, c, d, a, k[5], 21, -57434055); 59 | a = ii(a, b, c, d, k[12], 6, 1700485571); 60 | d = ii(d, a, b, c, k[3], 10, -1894986606); 61 | c = ii(c, d, a, b, k[10], 15, -1051523); 62 | b = ii(b, c, d, a, k[1], 21, -2054922799); 63 | a = ii(a, b, c, d, k[8], 6, 1873313359); 64 | d = ii(d, a, b, c, k[15], 10, -30611744); 65 | c = ii(c, d, a, b, k[6], 15, -1560198380); 66 | b = ii(b, c, d, a, k[13], 21, 1309151649); 67 | a = ii(a, b, c, d, k[4], 6, -145523070); 68 | d = ii(d, a, b, c, k[11], 10, -1120210379); 69 | c = ii(c, d, a, b, k[2], 15, 718787259); 70 | b = ii(b, c, d, a, k[9], 21, -343485551); 71 | 72 | x[0] = add32(a, x[0]); 73 | x[1] = add32(b, x[1]); 74 | x[2] = add32(c, x[2]); 75 | x[3] = add32(d, x[3]); 76 | 77 | } 78 | 79 | function cmn(q, a, b, x, s, t) { 80 | a = add32(add32(a, q), add32(x, t)); 81 | return add32((a << s) | (a >>> (32 - s)), b); 82 | } 83 | 84 | function ff(a, b, c, d, x, s, t) { 85 | return cmn((b & c) | ((~b) & d), a, b, x, s, t); 86 | } 87 | 88 | function gg(a, b, c, d, x, s, t) { 89 | return cmn((b & d) | (c & (~d)), a, b, x, s, t); 90 | } 91 | 92 | function hh(a, b, c, d, x, s, t) { 93 | return cmn(b ^ c ^ d, a, b, x, s, t); 94 | } 95 | 96 | function ii(a, b, c, d, x, s, t) { 97 | return cmn(c ^ (b | (~d)), a, b, x, s, t); 98 | } 99 | 100 | function md51(s) { 101 | var n = s.length, 102 | state = [1732584193, -271733879, -1732584194, 271733878], i; 103 | for (i = 64; i <= s.length; i += 64) { 104 | md5cycle(state, md5blk(s.substring(i - 64, i))); 105 | } 106 | s = s.substring(i - 64); 107 | var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 108 | for (i = 0; i < s.length; i++) 109 | tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); 110 | tail[i >> 2] |= 0x80 << ((i % 4) << 3); 111 | if (i > 55) { 112 | md5cycle(state, tail); 113 | for (i = 0; i < 16; i++) tail[i] = 0; 114 | } 115 | tail[14] = n * 8; 116 | md5cycle(state, tail); 117 | return state; 118 | } 119 | 120 | /* there needs to be support for Unicode here, 121 | * unless we pretend that we can redefine the MD-5 122 | * algorithm for multi-byte characters (perhaps 123 | * by adding every four 16-bit characters and 124 | * shortening the sum to 32 bits). Otherwise 125 | * I suggest performing MD-5 as if every character 126 | * was two bytes--e.g., 0040 0025 = @%--but then 127 | * how will an ordinary MD-5 sum be matched? 128 | * There is no way to standardize text to something 129 | * like UTF-8 before transformation; speed cost is 130 | * utterly prohibitive. The JavaScript standard 131 | * itself needs to look at this: it should start 132 | * providing access to strings as preformed UTF-8 133 | * 8-bit unsigned value arrays. 134 | */ 135 | function md5blk(s) { /* I figured global was faster. */ 136 | var md5blks = [], i; /* Andy King said do it this way. */ 137 | for (i = 0; i < 64; i += 4) { 138 | md5blks[i >> 2] = s.charCodeAt(i) 139 | + (s.charCodeAt(i + 1) << 8) 140 | + (s.charCodeAt(i + 2) << 16) 141 | + (s.charCodeAt(i + 3) << 24); 142 | } 143 | return md5blks; 144 | } 145 | 146 | var hex_chr = '0123456789abcdef'.split(''); 147 | 148 | function rhex(n) { 149 | var s = '', j = 0; 150 | for (; j < 4; j++) 151 | s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] 152 | + hex_chr[(n >> (j * 8)) & 0x0F]; 153 | return s; 154 | } 155 | 156 | function hex(x) { 157 | for (var i = 0; i < x.length; i++) 158 | x[i] = rhex(x[i]); 159 | return x.join(''); 160 | } 161 | 162 | export function md5(s) { 163 | return hex(md51(s)); 164 | } 165 | 166 | /* this function is much faster, 167 | so if possible we use it. Some IEs 168 | are the only ones I know of that 169 | need the idiotic second function, 170 | generated by an if clause. */ 171 | 172 | function add32(a, b) { 173 | return (a + b) & 0xFFFFFFFF; 174 | } 175 | 176 | if (md5('hello') != '5d41402abc4b2a76b9719d911017c592') { 177 | function add32(x, y) { 178 | var lsw = (x & 0xFFFF) + (y & 0xFFFF), 179 | msw = (x >> 16) + (y >> 16) + (lsw >> 16); 180 | return (msw << 16) | (lsw & 0xFFFF); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /public/scripts/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | // Import Internal Dependencies 4 | import { md5 } from "../lib/md5.js"; 5 | 6 | // CONSTANTS 7 | const kChartOptions = { 8 | legend: { 9 | display: false 10 | }, 11 | scales: { 12 | y: { 13 | beginAtZero: true 14 | } 15 | }, 16 | plugins: { 17 | legend: { 18 | display: true 19 | }, 20 | datalabels: { 21 | anchor: "center", 22 | textShadowBlur: 4, 23 | textShadowColor: "black", 24 | textStrokeColor: "black", 25 | textStrokeWidth: 1, 26 | labels: { 27 | value: { color: "white" } 28 | }, 29 | font: { 30 | size: 15, 31 | weight: "bold" 32 | } 33 | } 34 | } 35 | }; 36 | const colorRangeInfo = { 37 | colorStart: 0.2, colorEnd: 0.8, useEndAsStart: false 38 | }; 39 | 40 | // https://github.com/d3/d3-scale-chromatic/blob/master/README.md 41 | function calculatePoint(id, intervalSize, { colorStart, colorEnd, useEndAsStart }) { 42 | return (useEndAsStart ? (colorEnd - (id * intervalSize)) : (colorStart + (id * intervalSize))); 43 | } 44 | 45 | function* interpolateColors(dataLength, scale, range) { 46 | const intervalSize = (range.colorEnd - range.colorStart) / dataLength; 47 | 48 | for (let id = 0; id < dataLength; id++) { 49 | yield scale(calculatePoint(id, intervalSize, range)); 50 | } 51 | } 52 | 53 | function createChart(elementId, type = "bar", payload = {}) { 54 | const { labels, data, interpolate = d3.interpolateCool } = payload; 55 | const options = JSON.parse(JSON.stringify(kChartOptions)); 56 | const chartType = (type === "horizontalBar") ? "bar" : type; 57 | if (type === "horizontalBar") { 58 | options.indexAxis = "y"; 59 | } 60 | if (type === "pie") { 61 | options.legend.display = true; 62 | options.plugins.datalabels.align = "center"; 63 | } 64 | else { 65 | options.plugins.legend.display = false; 66 | options.plugins.datalabels.align = type === "bar" ? "top" : "right"; 67 | } 68 | 69 | new Chart(document.getElementById(elementId).getContext("2d"), { 70 | type: chartType, 71 | plugins: [ChartDataLabels], 72 | data: { 73 | label: "", 74 | labels, 75 | legend: { 76 | display: false 77 | }, 78 | datasets: [{ 79 | borderWidth: 0, 80 | backgroundColor: [...interpolateColors(labels.length, interpolate, colorRangeInfo)], 81 | data 82 | }] 83 | }, 84 | options 85 | }); 86 | } 87 | window.createChart = createChart; 88 | 89 | document.addEventListener("DOMContentLoaded", () => { 90 | const assetLocationElement = document.getElementById("asset_location"); 91 | const assetLocation = assetLocationElement.getAttribute("data-location"); 92 | 93 | const avatarsElements = document.querySelectorAll(".avatar"); 94 | for (const avatar of avatarsElements) { 95 | const email = avatar.getAttribute("data-email"); 96 | const aElement = avatar.querySelector("a"); 97 | 98 | const imgEl = document.createElement("img"); 99 | imgEl.src = `https://gravatar.com/avatar/${md5(email)}?&d=404`; 100 | imgEl.onerror = () => { 101 | imgEl.src = assetLocation + "/avatar-default.png"; 102 | }; 103 | aElement.appendChild(imgEl); 104 | } 105 | }); 106 | -------------------------------------------------------------------------------- /scripts/nodesecure_payload.json: -------------------------------------------------------------------------------- 1 | { 2 | "report_theme": "light", 3 | "report_title": "nodesecure", 4 | "report_logo": "https://avatars0.githubusercontent.com/u/29552883?s=200&v=4", 5 | "report_date": "08 Jul 2024, 01:17:54", 6 | "npm_stats": { 7 | "size": { 8 | "all": "6.99 MB", 9 | "internal": "158.24 KB", 10 | "external": "6.83 MB" 11 | }, 12 | "deps": { 13 | "transitive": { 14 | "is-svg@4.4.0": { 15 | "links": { 16 | "npm": "https://www.npmjs.com/package/is-svg/v/4.4.0", 17 | "homepage": "https://github.com/sindresorhus/is-svg#readme", 18 | "repository": "https://github.com/sindresorhus/is-svg" 19 | } 20 | }, 21 | "string-width@5.1.2": { 22 | "links": { 23 | "npm": "https://www.npmjs.com/package/string-width/v/5.1.2", 24 | "homepage": "https://github.com/sindresorhus/string-width#readme", 25 | "repository": "https://github.com/sindresorhus/string-width" 26 | } 27 | } 28 | }, 29 | "node": { 30 | "path": { 31 | "visualizerUrl": "https://nodejs.org/dist/latest/docs/api/path.html" 32 | }, 33 | "fs": { 34 | "visualizerUrl": "https://nodejs.org/dist/latest/docs/api/fs.html" 35 | }, 36 | "stream": { 37 | "visualizerUrl": "https://nodejs.org/dist/latest/docs/api/stream.html" 38 | }, 39 | "util": { 40 | "visualizerUrl": "https://nodejs.org/dist/latest/docs/api/util.html" 41 | }, 42 | "events": { 43 | "visualizerUrl": "https://nodejs.org/dist/latest/docs/api/events.html" 44 | }, 45 | "node:path": { 46 | "visualizerUrl": "https://nodejs.org/dist/latest/docs/api/path.html" 47 | }, 48 | "node:url": { 49 | "visualizerUrl": "https://nodejs.org/dist/latest/docs/api/url.html" 50 | }, 51 | "node:assert": { 52 | "visualizerUrl": "https://nodejs.org/dist/latest/docs/api/assert.html" 53 | }, 54 | "repl": { 55 | "visualizerUrl": "https://nodejs.org/dist/latest/docs/api/repl.html" 56 | } 57 | } 58 | }, 59 | "licenses": { 60 | "Unknown": 0, 61 | "MIT": 19, 62 | "ISC": 1 63 | }, 64 | "flags": { 65 | "hasManyPublishers": 19, 66 | "hasMinifiedCode": 3, 67 | "hasWarnings": 9, 68 | "hasMissingOrUnusedDependency": 1, 69 | "hasIndirectDependencies": 5, 70 | "isOutdated": 4, 71 | "isDead": 1 72 | }, 73 | "flagsList": { 74 | "hasExternalCapacity": { 75 | "emoji": "🌍", 76 | "title": "hasExternalCapacity", 77 | "tooltipDescription": "The package uses at least one Node.js core dependency capable to establish communication outside of localhost" 78 | }, 79 | "hasWarnings": { 80 | "emoji": "🚧", 81 | "title": "hasWarnings", 82 | "tooltipDescription": "The AST analysis has detected warnings (suspect import, unsafe regex ..)" 83 | }, 84 | "hasNativeCode": { 85 | "emoji": "🐲", 86 | "title": "hasNativeCode", 87 | "tooltipDescription": "The package uses and runs C++ or Rust N-API code" 88 | }, 89 | "hasCustomResolver": { 90 | "emoji": "💎", 91 | "title": "hasCustomResolver", 92 | "tooltipDescription": "The package has dependencies who do not resolve on a registry (git, file, ssh etc..)" 93 | }, 94 | "hasNoLicense": { 95 | "emoji": "📜", 96 | "title": "hasNoLicense", 97 | "tooltipDescription": "The package does not have a license" 98 | }, 99 | "hasMultipleLicenses": { 100 | "emoji": "📚", 101 | "title": "hasMultipleLicenses", 102 | "tooltipDescription": "The package has licenses in multiple locations (files or manifest)" 103 | }, 104 | "hasMinifiedCode": { 105 | "emoji": "🔬", 106 | "title": "hasMinifiedCode", 107 | "tooltipDescription": "The package has minified and/or uglified files" 108 | }, 109 | "isDeprecated": { 110 | "emoji": "⛔️", 111 | "title": "isDeprecated", 112 | "tooltipDescription": "The package has been deprecated on NPM" 113 | }, 114 | "hasManyPublishers": { 115 | "emoji": "👥", 116 | "title": "hasManyPublishers", 117 | "tooltipDescription": "The package has several publishers" 118 | }, 119 | "hasScript": { 120 | "emoji": "📦", 121 | "title": "hasScript", 122 | "tooltipDescription": "The package has `post` and/or `pre` (un)install npm script" 123 | }, 124 | "hasIndirectDependencies": { 125 | "emoji": "🌲", 126 | "title": "hasIndirectDependencies", 127 | "tooltipDescription": "The package has indirect dependencies" 128 | }, 129 | "isGit": { 130 | "emoji": "☁️", 131 | "title": "isGit", 132 | "tooltipDescription": "The package (project) is a git repository" 133 | }, 134 | "hasVulnerabilities": { 135 | "emoji": "🚨", 136 | "title": "hasVulnerabilities", 137 | "tooltipDescription": "The package has one or many vulnerabilities" 138 | }, 139 | "hasMissingOrUnusedDependency": { 140 | "emoji": "👀", 141 | "title": "hasMissingOrUnusedDependency", 142 | "tooltipDescription": "A dependency is missing in package.json or a dependency is installed but never used" 143 | }, 144 | "isDead": { 145 | "emoji": "💀", 146 | "title": "isDead", 147 | "tooltipDescription": "The dependency has not received update from at least one year" 148 | }, 149 | "hasBannedFile": { 150 | "emoji": "⚔️", 151 | "title": "hasBannedFile", 152 | "tooltipDescription": "The project has at least one sensitive file" 153 | }, 154 | "isOutdated": { 155 | "emoji": "⌚️", 156 | "title": "isOutdated", 157 | "tooltipDescription": "The current package version is not equal to the package latest version" 158 | }, 159 | "isDuplicated": { 160 | "emoji": "🎭", 161 | "title": "isDuplicated", 162 | "tooltipDescription": "The package is also used somewhere else in the dependency tree but with a different version" 163 | } 164 | }, 165 | "extensions": { 166 | ".js": 19, 167 | ".json": 20, 168 | ".md": 20, 169 | ".ts": 13, 170 | ".cjs": 2, 171 | ".map": 2, 172 | ".cts": 1, 173 | ".mjs": 1, 174 | ".yml": 2, 175 | ".txt": 1 176 | }, 177 | "warnings": { 178 | "obfuscated-code": 10, 179 | "unsafe-regex": 40, 180 | "zero-semver": 2 181 | }, 182 | "authors": { 183 | "martin@kolarik.sk": 2, 184 | "gentilhomme.thomas@gmail.com": 8, 185 | "gabriel.vergnaud@gmail.com": 1, 186 | "huochunpeng@gmail.com": 1, 187 | "weiran.zsd@outlook.com": 1, 188 | "klechon123@gmail.com": 1, 189 | "ts-npm-types@microsoft.com": 1, 190 | "richard.a.harris@gmail.com": 1, 191 | "dmitry.soshnikov@gmail.com": 1, 192 | "davisjam@vt.edu": 2, 193 | "hello@miguelmota.com": 2, 194 | "amitgupta.gwl@gmail.com": 2, 195 | "sindresorhus@gmail.com": 8, 196 | "pierredemailly.pro@gmail.com": 3, 197 | "coulon_antoine@outlook.fr": 3, 198 | "vincent.dhennin@viacesi.fr": 3, 199 | "gorez.tony@gmail.com": 3, 200 | "josh@junon.me": 2, 201 | "mathias@qiwi.be": 1, 202 | "node-team-npm+wombot@google.com": 1, 203 | "komagata@gmail.com": 1 204 | }, 205 | "packages": { 206 | "is-minified-code": { 207 | "isThird": true, 208 | "versions": {}, 209 | "fullName": "is-minified-code", 210 | "isGiven": false, 211 | "flags": { 212 | "hasManyPublishers": { 213 | "emoji": "👥", 214 | "title": "hasManyPublishers", 215 | "tooltipDescription": "The package has several publishers" 216 | } 217 | }, 218 | "2.0.0": { 219 | "hasIndirectDependencies": false 220 | }, 221 | "links": { 222 | "npm": "https://www.npmjs.com/package/is-minified-code/v/2.0.0", 223 | "homepage": "https://github.com/MartinKolarik/is-minified-code/", 224 | "repository": "https://github.com/MartinKolarik/is-minified-code" 225 | } 226 | }, 227 | "frequency-set": { 228 | "isThird": true, 229 | "versions": {}, 230 | "fullName": "frequency-set", 231 | "isGiven": false, 232 | "flags": { 233 | "hasManyPublishers": { 234 | "emoji": "👥", 235 | "title": "hasManyPublishers", 236 | "tooltipDescription": "The package has several publishers" 237 | } 238 | }, 239 | "1.0.2": { 240 | "hasIndirectDependencies": false 241 | }, 242 | "links": { 243 | "npm": "https://www.npmjs.com/package/frequency-set/v/1.0.2", 244 | "homepage": "https://github.com/fraxken/FrequencySet#readme", 245 | "repository": "https://github.com/fraxken/FrequencySet" 246 | } 247 | }, 248 | "ts-pattern": { 249 | "isThird": true, 250 | "versions": {}, 251 | "fullName": "ts-pattern", 252 | "isGiven": false, 253 | "flags": { 254 | "hasMinifiedCode": { 255 | "emoji": "🔬", 256 | "title": "hasMinifiedCode", 257 | "tooltipDescription": "The package has minified and/or uglified files" 258 | }, 259 | "hasManyPublishers": { 260 | "emoji": "👥", 261 | "title": "hasManyPublishers", 262 | "tooltipDescription": "The package has several publishers" 263 | } 264 | }, 265 | "5.2.0": { 266 | "hasIndirectDependencies": false 267 | }, 268 | "links": { 269 | "npm": "https://www.npmjs.com/package/ts-pattern/v/5.2.0", 270 | "homepage": "https://github.com/gvergnaud/ts-pattern#readme", 271 | "repository": "https://github.com/gvergnaud/ts-pattern" 272 | } 273 | }, 274 | "meriyah": { 275 | "isThird": true, 276 | "versions": {}, 277 | "fullName": "meriyah", 278 | "isGiven": false, 279 | "flags": { 280 | "hasMinifiedCode": { 281 | "emoji": "🔬", 282 | "title": "hasMinifiedCode", 283 | "tooltipDescription": "The package has minified and/or uglified files" 284 | }, 285 | "hasWarnings": { 286 | "emoji": "🚧", 287 | "title": "hasWarnings", 288 | "tooltipDescription": "The AST analysis has detected warnings (suspect import, unsafe regex ..)" 289 | }, 290 | "hasManyPublishers": { 291 | "emoji": "👥", 292 | "title": "hasManyPublishers", 293 | "tooltipDescription": "The package has several publishers" 294 | } 295 | }, 296 | "4.5.0": { 297 | "hasIndirectDependencies": false 298 | }, 299 | "links": { 300 | "npm": "https://www.npmjs.com/package/meriyah/v/4.5.0", 301 | "homepage": "https://github.com/meriyah/meriyah", 302 | "repository": "https://github.com/meriyah/meriyah" 303 | } 304 | }, 305 | "@types/estree": { 306 | "isThird": true, 307 | "versions": {}, 308 | "fullName": "@types/estree", 309 | "isGiven": false, 310 | "flags": {}, 311 | "1.0.5": { 312 | "hasIndirectDependencies": false 313 | }, 314 | "links": { 315 | "npm": "https://www.npmjs.com/package/@types/estree/v/1.0.5", 316 | "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/estree", 317 | "repository": "https://github.com/DefinitelyTyped/DefinitelyTyped" 318 | } 319 | }, 320 | "estree-walker": { 321 | "isThird": true, 322 | "versions": {}, 323 | "fullName": "estree-walker", 324 | "isGiven": false, 325 | "flags": { 326 | "hasManyPublishers": { 327 | "emoji": "👥", 328 | "title": "hasManyPublishers", 329 | "tooltipDescription": "The package has several publishers" 330 | } 331 | }, 332 | "3.0.3": { 333 | "hasIndirectDependencies": false 334 | }, 335 | "links": { 336 | "npm": "https://www.npmjs.com/package/estree-walker/v/3.0.3", 337 | "homepage": "https://github.com/Rich-Harris/estree-walker#readme", 338 | "repository": "https://github.com/Rich-Harris/estree-walker" 339 | } 340 | }, 341 | "regexp-tree": { 342 | "isThird": true, 343 | "versions": {}, 344 | "fullName": "regexp-tree", 345 | "isGiven": false, 346 | "flags": { 347 | "hasWarnings": { 348 | "emoji": "🚧", 349 | "title": "hasWarnings", 350 | "tooltipDescription": "The AST analysis has detected warnings (suspect import, unsafe regex ..)" 351 | }, 352 | "hasManyPublishers": { 353 | "emoji": "👥", 354 | "title": "hasManyPublishers", 355 | "tooltipDescription": "The package has several publishers" 356 | } 357 | }, 358 | "0.1.27": { 359 | "hasIndirectDependencies": false 360 | }, 361 | "links": { 362 | "npm": "https://www.npmjs.com/package/regexp-tree/v/0.1.27", 363 | "homepage": "https://github.com/DmitrySoshnikov/regexp-tree", 364 | "repository": "https://github.com/DmitrySoshnikov/regexp-tree" 365 | } 366 | }, 367 | "safe-regex": { 368 | "isThird": true, 369 | "versions": {}, 370 | "fullName": "safe-regex", 371 | "isGiven": false, 372 | "flags": { 373 | "hasWarnings": { 374 | "emoji": "🚧", 375 | "title": "hasWarnings", 376 | "tooltipDescription": "The AST analysis has detected warnings (suspect import, unsafe regex ..)" 377 | }, 378 | "hasManyPublishers": { 379 | "emoji": "👥", 380 | "title": "hasManyPublishers", 381 | "tooltipDescription": "The package has several publishers" 382 | } 383 | }, 384 | "2.1.1": { 385 | "hasIndirectDependencies": false 386 | }, 387 | "links": { 388 | "npm": "https://www.npmjs.com/package/safe-regex/v/2.1.1", 389 | "homepage": "https://github.com/davisjam/safe-regex", 390 | "repository": "https://github.com/davisjam/safe-regex" 391 | } 392 | }, 393 | "is-base64": { 394 | "isThird": true, 395 | "versions": {}, 396 | "fullName": "is-base64", 397 | "isGiven": false, 398 | "flags": { 399 | "hasMinifiedCode": { 400 | "emoji": "🔬", 401 | "title": "hasMinifiedCode", 402 | "tooltipDescription": "The package has minified and/or uglified files" 403 | }, 404 | "hasManyPublishers": { 405 | "emoji": "👥", 406 | "title": "hasManyPublishers", 407 | "tooltipDescription": "The package has several publishers" 408 | } 409 | }, 410 | "1.1.0": { 411 | "hasIndirectDependencies": false 412 | }, 413 | "links": { 414 | "npm": "https://www.npmjs.com/package/is-base64/v/1.1.0", 415 | "homepage": "https://github.com/miguelmota/is-base64", 416 | "repository": "https://github.com/miguelmota/is-base64" 417 | } 418 | }, 419 | "strnum": { 420 | "isThird": true, 421 | "versions": {}, 422 | "fullName": "strnum", 423 | "isGiven": false, 424 | "flags": { 425 | "hasWarnings": { 426 | "emoji": "🚧", 427 | "title": "hasWarnings", 428 | "tooltipDescription": "The AST analysis has detected warnings (suspect import, unsafe regex ..)" 429 | }, 430 | "hasManyPublishers": { 431 | "emoji": "👥", 432 | "title": "hasManyPublishers", 433 | "tooltipDescription": "The package has several publishers" 434 | } 435 | }, 436 | "1.0.5": { 437 | "hasIndirectDependencies": false 438 | }, 439 | "links": { 440 | "npm": "https://www.npmjs.com/package/strnum/v/1.0.5", 441 | "homepage": "https://github.com/NaturalIntelligence/strnum#readme", 442 | "repository": "https://github.com/NaturalIntelligence/strnum" 443 | } 444 | }, 445 | "fast-xml-parser": { 446 | "isThird": true, 447 | "versions": {}, 448 | "fullName": "fast-xml-parser", 449 | "isGiven": false, 450 | "flags": { 451 | "hasMissingOrUnusedDependency": { 452 | "emoji": "👀", 453 | "title": "hasMissingOrUnusedDependency", 454 | "tooltipDescription": "A dependency is missing in package.json or a dependency is installed but never used" 455 | }, 456 | "hasWarnings": { 457 | "emoji": "🚧", 458 | "title": "hasWarnings", 459 | "tooltipDescription": "The AST analysis has detected warnings (suspect import, unsafe regex ..)" 460 | }, 461 | "hasManyPublishers": { 462 | "emoji": "👥", 463 | "title": "hasManyPublishers", 464 | "tooltipDescription": "The package has several publishers" 465 | } 466 | }, 467 | "4.4.0": { 468 | "hasIndirectDependencies": false 469 | }, 470 | "links": { 471 | "npm": "https://www.npmjs.com/package/fast-xml-parser/v/4.4.0", 472 | "homepage": "https://github.com/NaturalIntelligence/fast-xml-parser#readme", 473 | "repository": "https://github.com/NaturalIntelligence/fast-xml-parser" 474 | } 475 | }, 476 | "is-svg": { 477 | "isThird": true, 478 | "versions": {}, 479 | "fullName": "is-svg", 480 | "isGiven": false, 481 | "flags": { 482 | "hasIndirectDependencies": { 483 | "emoji": "🌲", 484 | "title": "hasIndirectDependencies", 485 | "tooltipDescription": "The package has indirect dependencies" 486 | }, 487 | "isOutdated": { 488 | "emoji": "⌚️", 489 | "title": "isOutdated", 490 | "tooltipDescription": "The current package version is not equal to the package latest version" 491 | }, 492 | "hasManyPublishers": { 493 | "emoji": "👥", 494 | "title": "hasManyPublishers", 495 | "tooltipDescription": "The package has several publishers" 496 | } 497 | }, 498 | "4.4.0": { 499 | "hasIndirectDependencies": true 500 | }, 501 | "links": { 502 | "npm": "https://www.npmjs.com/package/is-svg/v/4.4.0", 503 | "homepage": "https://github.com/sindresorhus/is-svg#readme", 504 | "repository": "https://github.com/sindresorhus/is-svg" 505 | } 506 | }, 507 | "@nodesecure/sec-literal": { 508 | "isThird": false, 509 | "versions": {}, 510 | "fullName": "@nodesecure/sec-literal", 511 | "isGiven": false, 512 | "flags": { 513 | "hasIndirectDependencies": { 514 | "emoji": "🌲", 515 | "title": "hasIndirectDependencies", 516 | "tooltipDescription": "The package has indirect dependencies" 517 | }, 518 | "hasWarnings": { 519 | "emoji": "🚧", 520 | "title": "hasWarnings", 521 | "tooltipDescription": "The AST analysis has detected warnings (suspect import, unsafe regex ..)" 522 | }, 523 | "isDead": { 524 | "emoji": "💀", 525 | "title": "isDead", 526 | "tooltipDescription": "The dependency has not received update from at least one year" 527 | }, 528 | "hasManyPublishers": { 529 | "emoji": "👥", 530 | "title": "hasManyPublishers", 531 | "tooltipDescription": "The package has several publishers" 532 | } 533 | }, 534 | "1.2.0": { 535 | "hasIndirectDependencies": true 536 | }, 537 | "links": { 538 | "npm": "https://www.npmjs.com/package/@nodesecure/sec-literal/v/1.2.0", 539 | "homepage": "https://github.com/NodeSecure/sec-literal#readme", 540 | "repository": "https://github.com/NodeSecure/sec-literal" 541 | } 542 | }, 543 | "ansi-regex": { 544 | "isThird": true, 545 | "versions": {}, 546 | "fullName": "ansi-regex", 547 | "isGiven": false, 548 | "flags": { 549 | "hasManyPublishers": { 550 | "emoji": "👥", 551 | "title": "hasManyPublishers", 552 | "tooltipDescription": "The package has several publishers" 553 | } 554 | }, 555 | "6.0.1": { 556 | "hasIndirectDependencies": false 557 | }, 558 | "links": { 559 | "npm": "https://www.npmjs.com/package/ansi-regex/v/6.0.1", 560 | "homepage": "https://github.com/chalk/ansi-regex#readme", 561 | "repository": "https://github.com/chalk/ansi-regex" 562 | } 563 | }, 564 | "strip-ansi": { 565 | "isThird": true, 566 | "versions": {}, 567 | "fullName": "strip-ansi", 568 | "isGiven": false, 569 | "flags": { 570 | "hasManyPublishers": { 571 | "emoji": "👥", 572 | "title": "hasManyPublishers", 573 | "tooltipDescription": "The package has several publishers" 574 | } 575 | }, 576 | "7.1.0": { 577 | "hasIndirectDependencies": false 578 | }, 579 | "links": { 580 | "npm": "https://www.npmjs.com/package/strip-ansi/v/7.1.0", 581 | "homepage": "https://github.com/chalk/strip-ansi#readme", 582 | "repository": "https://github.com/chalk/strip-ansi" 583 | } 584 | }, 585 | "emoji-regex": { 586 | "isThird": true, 587 | "versions": {}, 588 | "fullName": "emoji-regex", 589 | "isGiven": false, 590 | "flags": { 591 | "isOutdated": { 592 | "emoji": "⌚️", 593 | "title": "isOutdated", 594 | "tooltipDescription": "The current package version is not equal to the package latest version" 595 | }, 596 | "hasWarnings": { 597 | "emoji": "🚧", 598 | "title": "hasWarnings", 599 | "tooltipDescription": "The AST analysis has detected warnings (suspect import, unsafe regex ..)" 600 | }, 601 | "hasManyPublishers": { 602 | "emoji": "👥", 603 | "title": "hasManyPublishers", 604 | "tooltipDescription": "The package has several publishers" 605 | } 606 | }, 607 | "9.2.2": { 608 | "hasIndirectDependencies": false 609 | }, 610 | "links": { 611 | "npm": "https://www.npmjs.com/package/emoji-regex/v/9.2.2", 612 | "homepage": "https://mths.be/emoji-regex", 613 | "repository": "https://github.com/mathiasbynens/emoji-regex" 614 | } 615 | }, 616 | "eastasianwidth": { 617 | "isThird": true, 618 | "versions": {}, 619 | "fullName": "eastasianwidth", 620 | "isGiven": false, 621 | "flags": { 622 | "isOutdated": { 623 | "emoji": "⌚️", 624 | "title": "isOutdated", 625 | "tooltipDescription": "The current package version is not equal to the package latest version" 626 | }, 627 | "hasWarnings": { 628 | "emoji": "🚧", 629 | "title": "hasWarnings", 630 | "tooltipDescription": "The AST analysis has detected warnings (suspect import, unsafe regex ..)" 631 | }, 632 | "hasManyPublishers": { 633 | "emoji": "👥", 634 | "title": "hasManyPublishers", 635 | "tooltipDescription": "The package has several publishers" 636 | } 637 | }, 638 | "0.2.0": { 639 | "hasIndirectDependencies": false 640 | }, 641 | "links": { 642 | "npm": "https://www.npmjs.com/package/eastasianwidth/v/0.2.0", 643 | "homepage": "https://github.com/komagata/eastasianwidth#readme", 644 | "repository": "https://github.com/komagata/eastasianwidth" 645 | } 646 | }, 647 | "string-width": { 648 | "isThird": true, 649 | "versions": {}, 650 | "fullName": "string-width", 651 | "isGiven": false, 652 | "flags": { 653 | "hasIndirectDependencies": { 654 | "emoji": "🌲", 655 | "title": "hasIndirectDependencies", 656 | "tooltipDescription": "The package has indirect dependencies" 657 | }, 658 | "isOutdated": { 659 | "emoji": "⌚️", 660 | "title": "isOutdated", 661 | "tooltipDescription": "The current package version is not equal to the package latest version" 662 | }, 663 | "hasManyPublishers": { 664 | "emoji": "👥", 665 | "title": "hasManyPublishers", 666 | "tooltipDescription": "The package has several publishers" 667 | } 668 | }, 669 | "5.1.2": { 670 | "hasIndirectDependencies": true 671 | }, 672 | "links": { 673 | "npm": "https://www.npmjs.com/package/string-width/v/5.1.2", 674 | "homepage": "https://github.com/sindresorhus/string-width#readme", 675 | "repository": "https://github.com/sindresorhus/string-width" 676 | } 677 | }, 678 | "@nodesecure/estree-ast-utils": { 679 | "isThird": false, 680 | "versions": {}, 681 | "fullName": "@nodesecure/estree-ast-utils", 682 | "isGiven": false, 683 | "flags": { 684 | "hasIndirectDependencies": { 685 | "emoji": "🌲", 686 | "title": "hasIndirectDependencies", 687 | "tooltipDescription": "The package has indirect dependencies" 688 | }, 689 | "hasManyPublishers": { 690 | "emoji": "👥", 691 | "title": "hasManyPublishers", 692 | "tooltipDescription": "The package has several publishers" 693 | } 694 | }, 695 | "1.4.1": { 696 | "hasIndirectDependencies": true 697 | }, 698 | "links": { 699 | "npm": "https://www.npmjs.com/package/@nodesecure/estree-ast-utils/v/1.4.1", 700 | "homepage": "https://github.com/NodeSecure/estree-ast-utils#readme", 701 | "repository": "https://github.com/NodeSecure/estree-ast-utils" 702 | } 703 | }, 704 | "@nodesecure/js-x-ray": { 705 | "isThird": false, 706 | "versions": {}, 707 | "fullName": "@nodesecure/js-x-ray", 708 | "isGiven": false, 709 | "flags": { 710 | "hasIndirectDependencies": { 711 | "emoji": "🌲", 712 | "title": "hasIndirectDependencies", 713 | "tooltipDescription": "The package has indirect dependencies" 714 | }, 715 | "hasWarnings": { 716 | "emoji": "🚧", 717 | "title": "hasWarnings", 718 | "tooltipDescription": "The AST analysis has detected warnings (suspect import, unsafe regex ..)" 719 | }, 720 | "hasManyPublishers": { 721 | "emoji": "👥", 722 | "title": "hasManyPublishers", 723 | "tooltipDescription": "The package has several publishers" 724 | } 725 | }, 726 | "7.2.0": { 727 | "hasIndirectDependencies": true 728 | }, 729 | "links": { 730 | "npm": "https://www.npmjs.com/package/@nodesecure/js-x-ray/v/7.2.0", 731 | "homepage": "https://github.com/NodeSecure/js-x-ray#readme", 732 | "repository": "https://github.com/NodeSecure/js-x-ray" 733 | } 734 | } 735 | }, 736 | "packages_count": { 737 | "all": 20, 738 | "internal": 3, 739 | "external": 17 740 | }, 741 | "scorecards": {}, 742 | "showFlags": true 743 | }, 744 | "git_stats": null, 745 | "charts": [ 746 | { 747 | "name": "Extensions", 748 | "help": null 749 | }, 750 | { 751 | "name": "Licenses", 752 | "help": null 753 | }, 754 | { 755 | "name": "Warnings", 756 | "help": null 757 | }, 758 | { 759 | "name": "Flags", 760 | "help": null 761 | } 762 | ] 763 | } 764 | -------------------------------------------------------------------------------- /scripts/preview.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | import { mkdirSync, rmSync, writeFileSync } from "node:fs"; 4 | import { parseArgs } from "node:util"; 5 | 6 | // Import Third-party Dependencies 7 | import open from "open"; 8 | 9 | // Import Internal Dependencies 10 | import { HTMLTemplateGenerator } from "../src/reporting/template.js"; 11 | import { buildFrontAssets } from "../src/reporting/html.js"; 12 | 13 | // CONSTANTS 14 | const kPreviewDir = path.join(import.meta.dirname, "..", "preview"); 15 | 16 | const { values: cliArgs } = parseArgs({ 17 | options: { 18 | theme: { 19 | type: "string", 20 | short: "t", 21 | default: "light", 22 | multiple: false 23 | } 24 | }, 25 | allowPositionals: true, 26 | strict: true 27 | }); 28 | const { theme } = cliArgs; 29 | 30 | rmSync( 31 | kPreviewDir, 32 | { force: true, recursive: true } 33 | ); 34 | mkdirSync( 35 | kPreviewDir, 36 | { recursive: true } 37 | ); 38 | 39 | const payload = (await import( 40 | "./nodesecure_payload.json", 41 | { with: { type: "json" } } 42 | )).default; 43 | payload.report_theme = theme; 44 | 45 | const config = { 46 | theme: theme as ("light" | "dark"), 47 | includeTransitiveInternal: false, 48 | reporters: ["html" as const], 49 | npm: { 50 | organizationPrefix: "@nodesecure", 51 | packages: ["@nodesecure/js-x-ray"] 52 | }, 53 | git: { 54 | organizationUrl: "https://github.com/NodeSecure", 55 | repositories: [] 56 | }, 57 | charts: [ 58 | { 59 | name: "Extensions" as const, 60 | display: true, 61 | interpolation: "d3.interpolateRainbow", 62 | type: "bar" as const 63 | }, 64 | { 65 | name: "Licenses" as const, 66 | display: true, 67 | interpolation: "d3.interpolateCool", 68 | type: "bar" as const 69 | }, 70 | { 71 | name: "Warnings" as const, 72 | display: true, 73 | type: "horizontalBar" as const, 74 | interpolation: "d3.interpolateInferno" 75 | }, 76 | { 77 | name: "Flags" as const, 78 | display: true, 79 | type: "horizontalBar" as const, 80 | interpolation: "d3.interpolateSinebow" 81 | } 82 | ], 83 | title: "nodesecure", 84 | logoUrl: "https://avatars0.githubusercontent.com/u/29552883?s=200&v=4", 85 | showFlags: true 86 | }; 87 | 88 | const HTMLReport = new HTMLTemplateGenerator( 89 | payload, 90 | config 91 | ).render({ asset_location: "./dist" }); 92 | 93 | const previewLocation = path.join(kPreviewDir, "preview.html"); 94 | writeFileSync( 95 | previewLocation, 96 | HTMLReport 97 | ); 98 | 99 | await buildFrontAssets( 100 | path.join(kPreviewDir, "dist"), 101 | { theme } 102 | ); 103 | 104 | await open(previewLocation); 105 | -------------------------------------------------------------------------------- /src/analysis/extractScannerData.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-depth */ 2 | // Import Node.js Dependencies 3 | import fs from "node:fs"; 4 | 5 | // Import Third-party Dependencies 6 | import { formatBytes, getScoreColor, getVCSRepositoryPathAndPlatform } from "@nodesecure/utils"; 7 | import { getManifest, getFlags } from "@nodesecure/flags/web"; 8 | import * as scorecard from "@nodesecure/ossf-scorecard-sdk"; 9 | import type { Payload } from "@nodesecure/scanner"; 10 | import type { RC } from "@nodesecure/rc"; 11 | 12 | // Import Internal Dependencies 13 | import * as localStorage from "../localStorage.js"; 14 | 15 | // CONSTANTS 16 | const kFlagsList = Object.values(getManifest()); 17 | const kWantedFlags = getFlags(); 18 | const kScorecardVisualizerUrl = "https://kooltheba.github.io/openssf-scorecard-api-visualizer/#/projects"; 19 | const kNodeVisualizerUrl = "https://nodejs.org/dist/latest/docs/api"; 20 | 21 | function splitPackageWithOrg(pkg: string) { 22 | // reverse here so if there is no orgPrefix, its value will be undefined 23 | const [name, orgPrefix] = pkg.split("/").reverse(); 24 | 25 | return { orgPrefix, name }; 26 | } 27 | 28 | export interface ReportStat { 29 | size: { 30 | all: string; 31 | internal: string; 32 | external: string; 33 | }; 34 | deps: { 35 | transitive: Record; 36 | node: Record; 37 | }; 38 | licenses: Record; 39 | flags: Record; 40 | flagsList: any; 41 | extensions: Record; 42 | warnings: Record; 43 | authors: Record; 44 | packages: Record; 45 | packages_count: { 46 | all: number; 47 | internal: number; 48 | external: number; 49 | }; 50 | scorecards: Record; 51 | showFlags: boolean; 52 | } 53 | 54 | export interface BuildScannerStatsOptions { 55 | reportConfig?: RC["report"]; 56 | } 57 | 58 | export async function buildStatsFromScannerDependencies( 59 | payloadFiles: string[] | Payload["dependencies"] = [], 60 | options: BuildScannerStatsOptions = Object.create(null) 61 | ): Promise { 62 | const { reportConfig } = options; 63 | 64 | const config = reportConfig ?? localStorage.getConfig().report!; 65 | const sizeStats = { 66 | all: 0, 67 | internal: 0, 68 | external: 0 69 | }; 70 | 71 | const stats: ReportStat = { 72 | size: { 73 | all: "", 74 | internal: "", 75 | external: "" 76 | }, 77 | deps: { 78 | transitive: {}, 79 | node: {} 80 | }, 81 | licenses: { 82 | Unknown: 0 83 | }, 84 | flags: {}, 85 | flagsList: Object.fromEntries(kFlagsList.map((flag) => [flag.title, flag])), 86 | extensions: {}, 87 | warnings: {}, 88 | authors: {}, 89 | packages: {}, 90 | packages_count: { 91 | all: 0, internal: 0, external: 0 92 | }, 93 | scorecards: {}, 94 | showFlags: config.showFlags ?? true 95 | }; 96 | 97 | function getPayloadDependencies( 98 | fileOrJson: string | Payload["dependencies"] 99 | ): Payload["dependencies"] { 100 | if (typeof fileOrJson === "string") { 101 | const buf = fs.readFileSync(fileOrJson); 102 | const dependencies = JSON.parse( 103 | buf.toString() 104 | ) as Payload["dependencies"]; 105 | 106 | return dependencies; 107 | } 108 | 109 | return fileOrJson; 110 | } 111 | 112 | const payloads = Array.isArray(payloadFiles) ? payloadFiles : [payloadFiles]; 113 | const npmConfig = config.npm!; 114 | for (const fileOrJson of payloads) { 115 | const dependencies = getPayloadDependencies(fileOrJson); 116 | 117 | for (const [name, descriptor] of Object.entries(dependencies)) { 118 | const { versions, metadata } = descriptor; 119 | const isThird = npmConfig.organizationPrefix === null ? 120 | true : 121 | !name.startsWith(`${npmConfig.organizationPrefix}/`); 122 | 123 | for (const human of metadata.maintainers) { 124 | if (human.email) { 125 | stats.authors[human.email] = human.email in stats.authors ? 126 | ++stats.authors[human.email] : 1; 127 | } 128 | } 129 | 130 | if (!(name in stats.packages)) { 131 | const { orgPrefix, name: splitName } = splitPackageWithOrg(name); 132 | const isGiven = config.npm?.packages.includes(splitName) && orgPrefix === config.npm?.organizationPrefix; 133 | if (isThird) { 134 | stats.packages_count.external++; 135 | } 136 | stats.packages[name] = { isThird, versions: new Set(), fullName: name, isGiven, flags: {} }; 137 | } 138 | 139 | const curr = stats.packages[name]; 140 | for (const [localVersion, localDescriptor] of Object.entries(versions)) { 141 | if (curr.versions.has(localVersion)) { 142 | continue; 143 | } 144 | const { flags, size, composition, uniqueLicenseIds, author, warnings = [], links = [] } = localDescriptor; 145 | 146 | sizeStats.all += size; 147 | sizeStats[isThird ? "external" : "internal"] += size; 148 | 149 | for (const { kind } of warnings) { 150 | stats.warnings[kind] = kind in stats.warnings ? ++stats.warnings[kind] : 1; 151 | } 152 | 153 | for (const flag of flags) { 154 | if (!(flag in kWantedFlags)) { 155 | continue; 156 | } 157 | stats.flags[flag] = flag in stats.flags ? ++stats.flags[flag] : 1; 158 | stats.packages[name].flags[flag] = { ...stats.flagsList[flag] }; 159 | } 160 | 161 | (composition.required_nodejs) 162 | .forEach((dep) => (stats.deps.node[dep] = { visualizerUrl: `${kNodeVisualizerUrl}/${dep.replace("node:", "")}.html` })); 163 | for (const extName of composition.extensions.filter((extName) => extName !== "")) { 164 | stats.extensions[extName] = extName in stats.extensions ? ++stats.extensions[extName] : 1; 165 | } 166 | 167 | for (const licenseName of uniqueLicenseIds) { 168 | stats.licenses[licenseName] = licenseName in stats.licenses ? 169 | ++stats.licenses[licenseName] : 1; 170 | } 171 | 172 | if (author?.email) { 173 | stats.authors[author.email] = author.email in stats.authors ? 174 | ++stats.authors[author.email] : 1; 175 | } 176 | 177 | curr.versions.add(localVersion); 178 | const hasIndirectDependencies = flags.includes("hasIndirectDependencies"); 179 | id: if (hasIndirectDependencies) { 180 | if (!config.includeTransitiveInternal && name.startsWith(npmConfig.organizationPrefix)) { 181 | break id; 182 | } 183 | 184 | stats.deps.transitive[`${name}@${localVersion}`] = { links }; 185 | } 186 | curr[localVersion] = { hasIndirectDependencies }; 187 | 188 | if (!curr.links) { 189 | Object.assign(curr, { links }); 190 | } 191 | } 192 | } 193 | } 194 | 195 | const givenPackages = Object.values(stats.packages).filter((pkg) => pkg.isGiven); 196 | 197 | await Promise.all(givenPackages.map(async(pkg) => { 198 | const { fullName } = pkg; 199 | const { score } = await scorecard.result(fullName, { resolveOnVersionControl: false }); 200 | const [repo, platform] = getVCSRepositoryPathAndPlatform(pkg.links?.repository) ?? []; 201 | stats.scorecards[fullName] = { 202 | score, 203 | color: getScoreColor(score), 204 | visualizerUrl: repo ? `${kScorecardVisualizerUrl}/${platform}/${repo}` : "#" 205 | }; 206 | })); 207 | 208 | stats.packages_count.all = Object.keys(stats.packages).length; 209 | stats.packages_count.internal = stats.packages_count.all - stats.packages_count.external; 210 | stats.size.all = formatBytes(sizeStats.all); 211 | stats.size.internal = formatBytes(sizeStats.internal); 212 | stats.size.external = formatBytes(sizeStats.external); 213 | 214 | return stats; 215 | } 216 | 217 | -------------------------------------------------------------------------------- /src/analysis/fetch.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | 4 | // Import Third-party Dependencies 5 | import kleur from "kleur"; 6 | 7 | // Import Internal Dependencies 8 | import { buildStatsFromScannerDependencies } from "./extractScannerData.js"; 9 | import * as scanner from "./scanner.js"; 10 | import * as localStorage from "../localStorage.js"; 11 | import * as utils from "../utils/index.js"; 12 | import * as CONSTANTS from "../constants.js"; 13 | 14 | export async function fetchPackagesAndRepositoriesData( 15 | verbose = true 16 | ) { 17 | const config = localStorage.getConfig().report!; 18 | 19 | const fetchNpm = (config.npm?.packages ?? []).length > 0; 20 | const fetchGit = (config.git?.repositories ?? []).length > 0; 21 | if (!fetchGit && !fetchNpm) { 22 | throw new Error( 23 | "No git repositories and no npm packages to fetch in the local configuration!" 24 | ); 25 | } 26 | 27 | const pkgStats = fetchNpm && config.npm ? 28 | await fetchPackagesStats( 29 | utils.formatNpmPackages( 30 | config.npm.organizationPrefix, 31 | config.npm.packages 32 | ), 33 | verbose 34 | ) : 35 | null; 36 | 37 | const repoStats = fetchGit && config.git ? 38 | await fetchRepositoriesStats( 39 | config.git.repositories, 40 | config.git.organizationUrl, 41 | verbose 42 | ) : 43 | null; 44 | 45 | return { 46 | pkgStats, 47 | repoStats 48 | }; 49 | } 50 | 51 | async function fetchPackagesStats( 52 | packages: string[], 53 | verbose = true 54 | ) { 55 | const jsonFiles = await utils.runInSpinner( 56 | { 57 | title: `[Fetcher: ${kleur.yellow().bold("NPM")}]`, 58 | start: "Fetching NPM packages metadata on the NPM Registry", 59 | verbose 60 | }, 61 | async() => Promise.all(packages.map(scanner.from)) 62 | ); 63 | 64 | return buildStatsFromScannerDependencies( 65 | jsonFiles.filter((value) => value !== null) 66 | ); 67 | } 68 | 69 | async function fetchRepositoriesStats( 70 | repositories: string[], 71 | organizationUrl: string, 72 | verbose = true 73 | ) { 74 | const jsonFiles = await utils.runInSpinner( 75 | { 76 | title: `[Fetcher: ${kleur.yellow().bold("GIT")}]`, 77 | start: "Cloning GIT repositories", 78 | verbose 79 | }, 80 | async(spinner) => { 81 | const repos = await Promise.all( 82 | repositories.map((repositoryName) => { 83 | const trimmedRepositoryName = repositoryName.trim(); 84 | 85 | return utils.cloneGITRepository( 86 | path.join(CONSTANTS.DIRS.CLONES, trimmedRepositoryName), 87 | `${organizationUrl}/${trimmedRepositoryName}.git` 88 | ); 89 | }) 90 | ); 91 | spinner.text = "Fetching repositories metadata on the NPM Registry"; 92 | 93 | return Promise.all(repos.map(scanner.cwd)); 94 | } 95 | ); 96 | 97 | return buildStatsFromScannerDependencies( 98 | jsonFiles.filter((value) => value !== null) 99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /src/analysis/scanner.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | import fs from "node:fs/promises"; 4 | 5 | // Import Third-party Dependencies 6 | import { Mutex } from "@openally/mutex"; 7 | import * as scanner from "@nodesecure/scanner"; 8 | 9 | // Import Internal Dependencies 10 | import * as CONSTANTS from "../constants.js"; 11 | 12 | // CONSTANTS 13 | const kMaxAnalysisLock = new Mutex({ concurrency: 2 }); 14 | 15 | export async function from( 16 | packageName: string 17 | ): Promise { 18 | const release = await kMaxAnalysisLock.acquire(); 19 | 20 | try { 21 | const name = `${packageName}.json`; 22 | const { dependencies } = await scanner.from(packageName, { 23 | maxDepth: 4, 24 | vulnerabilityStrategy: "none" 25 | }); 26 | 27 | const filePath = path.join(CONSTANTS.DIRS.JSON, name); 28 | await fs.mkdir(path.dirname(filePath), { recursive: true }); 29 | await fs.writeFile(filePath, JSON.stringify(dependencies, null, 2)); 30 | 31 | return filePath; 32 | } 33 | catch { 34 | return null; 35 | } 36 | finally { 37 | release(); 38 | } 39 | } 40 | 41 | export async function cwd( 42 | dir: string 43 | ): Promise { 44 | const release = await kMaxAnalysisLock.acquire(); 45 | 46 | try { 47 | const name = `${path.basename(dir)}.json`; 48 | const { dependencies } = await scanner.cwd(dir, { 49 | maxDepth: 4, 50 | vulnerabilityStrategy: "none" 51 | }); 52 | 53 | const filePath = path.join(CONSTANTS.DIRS.JSON, name); 54 | await fs.writeFile(filePath, JSON.stringify(dependencies, null, 2)); 55 | 56 | return filePath; 57 | } 58 | catch { 59 | return null; 60 | } 61 | finally { 62 | release(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/api/report.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import * as path from "node:path"; 3 | import * as os from "node:os"; 4 | import * as fs from "node:fs/promises"; 5 | 6 | // Import Third-party Dependencies 7 | import { type Payload } from "@nodesecure/scanner"; 8 | import { type RC } from "@nodesecure/rc"; 9 | 10 | // Import Internal Dependencies 11 | import { buildStatsFromScannerDependencies } from "../analysis/extractScannerData.js"; 12 | import { HTML, PDF } from "../reporting/index.js"; 13 | 14 | export interface ReportLocationOptions { 15 | includesPDF: boolean; 16 | savePDFOnDisk: boolean; 17 | saveHTMLOnDisk: boolean; 18 | } 19 | 20 | /** 21 | * Determine the final location of the report (on current working directory or in a temporary directory) 22 | */ 23 | async function reportLocation( 24 | location: string | null, 25 | options: ReportLocationOptions 26 | ): Promise { 27 | const { 28 | includesPDF, 29 | savePDFOnDisk, 30 | saveHTMLOnDisk 31 | } = options; 32 | 33 | if (location) { 34 | return location; 35 | } 36 | 37 | if ((includesPDF && savePDFOnDisk) || saveHTMLOnDisk) { 38 | return process.cwd(); 39 | } 40 | 41 | return fs.mkdtemp(path.join(os.tmpdir(), "nsecure-report-")); 42 | } 43 | 44 | export interface ReportOptions { 45 | reportOutputLocation?: string; 46 | savePDFOnDisk?: boolean; 47 | saveHTMLOnDisk?: boolean; 48 | } 49 | 50 | export async function report( 51 | scannerDependencies: Payload["dependencies"], 52 | reportConfig: NonNullable, 53 | reportOptions: ReportOptions = Object.create(null) 54 | ): Promise { 55 | const { 56 | reportOutputLocation = null, 57 | savePDFOnDisk = false, 58 | saveHTMLOnDisk = false 59 | } = reportOptions; 60 | const includesPDF = reportConfig.reporters?.includes("pdf") ?? false; 61 | const includesHTML = reportConfig.reporters?.includes("html") ?? false; 62 | if (!includesPDF && !includesHTML) { 63 | throw new Error("At least one reporter must be enabled (pdf or html)"); 64 | } 65 | 66 | const [pkgStats, finalReportLocation] = await Promise.all([ 67 | buildStatsFromScannerDependencies(scannerDependencies, { 68 | reportConfig 69 | }), 70 | reportLocation(reportOutputLocation, { includesPDF, savePDFOnDisk, saveHTMLOnDisk }) 71 | ]); 72 | 73 | let reportHTMLPath: string | undefined; 74 | try { 75 | reportHTMLPath = await HTML( 76 | { 77 | pkgStats, 78 | repoStats: null 79 | }, 80 | reportConfig, 81 | finalReportLocation 82 | ); 83 | 84 | if (includesPDF) { 85 | return await PDF(reportHTMLPath, { 86 | title: reportConfig.title, 87 | saveOnDisk: savePDFOnDisk, 88 | reportOutputLocation: finalReportLocation 89 | }); 90 | } 91 | 92 | return reportHTMLPath; 93 | } 94 | finally { 95 | if (reportHTMLPath && (!includesHTML || saveHTMLOnDisk === false)) { 96 | await fs.rm(reportHTMLPath, { 97 | force: true, 98 | recursive: true 99 | }); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | 5 | // Import Third-party Dependencies 6 | import * as rc from "@nodesecure/rc"; 7 | 8 | // CONSTANTS 9 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 10 | 11 | export const DIRS = Object.freeze({ 12 | JSON: path.join(rc.homedir(), "json"), 13 | CLONES: path.join(rc.homedir(), "clones"), 14 | PUBLIC: path.join(__dirname, "..", "public"), 15 | THEMES: path.join(__dirname, "..", "public", "css", "themes"), 16 | VIEWS: path.join(__dirname, "..", "views"), 17 | REPORTS: path.join(process.cwd(), "reports") 18 | }); 19 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./api/report.js"; 2 | -------------------------------------------------------------------------------- /src/localStorage.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { AsyncLocalStorage } from "node:async_hooks"; 3 | 4 | // Import Third-party Dependencies 5 | import type { RC } from "@nodesecure/rc"; 6 | 7 | export const store = new AsyncLocalStorage(); 8 | 9 | export function getConfig(): RC { 10 | const runtimeConfig = store.getStore(); 11 | if (runtimeConfig === undefined) { 12 | throw new Error("unable to fetch AsyncLocalStorage runtime config"); 13 | } 14 | 15 | return runtimeConfig; 16 | } 17 | -------------------------------------------------------------------------------- /src/reporting/html.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | import { readdirSync, promises as fs } from "node:fs"; 4 | 5 | // Import Third-party Dependencies 6 | import esbuild from "esbuild"; 7 | import type { RC } from "@nodesecure/rc"; 8 | 9 | // Import Internal Dependencies 10 | import * as utils from "../utils/index.js"; 11 | import * as CONSTANTS from "../constants.js"; 12 | import * as localStorage from "../localStorage.js"; 13 | import { HTMLTemplateGenerator } from "./template.js"; 14 | import type { ReportStat } from "../analysis/extractScannerData.js"; 15 | 16 | // CONSTANTS 17 | const kDateFormatter = Intl.DateTimeFormat("en-GB", { 18 | day: "2-digit", 19 | month: "short", 20 | year: "numeric", 21 | hour: "numeric", 22 | minute: "numeric", 23 | second: "numeric" 24 | }); 25 | 26 | const kStaticESBuildConfig = { 27 | allowOverwrite: true, 28 | loader: { 29 | ".jpg": "file", 30 | ".png": "file", 31 | ".woff": "file", 32 | ".woff2": "file", 33 | ".eot": "file", 34 | ".ttf": "file", 35 | ".svg": "file" 36 | }, 37 | platform: "browser", 38 | bundle: true, 39 | sourcemap: true, 40 | treeShaking: true, 41 | logLevel: "silent" 42 | } as const; 43 | 44 | const kImagesDir = path.join(CONSTANTS.DIRS.PUBLIC, "img"); 45 | const kAvailableThemes = new Set( 46 | readdirSync(CONSTANTS.DIRS.THEMES) 47 | .map((file) => path.basename(file, ".css")) 48 | ); 49 | 50 | export interface HTMLReportData { 51 | pkgStats: ReportStat | null; 52 | repoStats: ReportStat | null; 53 | } 54 | 55 | export async function HTML( 56 | data: HTMLReportData, 57 | reportOptions: RC["report"] | null = null, 58 | reportOutputLocation = CONSTANTS.DIRS.REPORTS 59 | ): Promise { 60 | const { pkgStats, repoStats } = data; 61 | 62 | const config = reportOptions ?? localStorage.getConfig().report!; 63 | const assetsOutputLocation = path.join(reportOutputLocation, "..", "dist"); 64 | const reportTheme = config.theme && kAvailableThemes.has(config.theme) ? config.theme : "dark"; 65 | const reportFinalOutputLocation = path.join( 66 | reportOutputLocation, 67 | utils.cleanReportName(config.title, ".html") 68 | ); 69 | 70 | const charts = (config.charts ?? []) 71 | .flatMap(({ display, name }) => (display ? [{ name }] : [])); 72 | 73 | const HTMLReport = new HTMLTemplateGenerator( 74 | { 75 | report_theme: reportTheme, 76 | report_title: config.title, 77 | report_logo: config.logoUrl, 78 | report_date: kDateFormatter.format(new Date()), 79 | npm_stats: pkgStats, 80 | git_stats: repoStats, 81 | charts 82 | }, 83 | reportOptions 84 | ).render(); 85 | 86 | await Promise.all([ 87 | fs.writeFile( 88 | reportFinalOutputLocation, 89 | HTMLReport 90 | ), 91 | buildFrontAssets( 92 | assetsOutputLocation, 93 | { theme: reportTheme } 94 | ) 95 | ]); 96 | 97 | return reportFinalOutputLocation; 98 | } 99 | 100 | export async function buildFrontAssets( 101 | outdir: string, 102 | options: { theme?: string; } = {} 103 | ): Promise { 104 | const { theme = "light" } = options; 105 | 106 | await esbuild.build({ 107 | ...kStaticESBuildConfig, 108 | entryPoints: [ 109 | path.join(CONSTANTS.DIRS.PUBLIC, "scripts", "main.js"), 110 | path.join(CONSTANTS.DIRS.PUBLIC, "css", "style.css"), 111 | path.join(CONSTANTS.DIRS.PUBLIC, "css", "themes", `${theme}.css`) 112 | ], 113 | outdir 114 | }); 115 | 116 | const imagesFiles = await fs.readdir(kImagesDir); 117 | await Promise.all([ 118 | ...imagesFiles.map((name) => fs.copyFile( 119 | path.join(kImagesDir, name), 120 | path.join(outdir, name) 121 | )) 122 | ]); 123 | } 124 | -------------------------------------------------------------------------------- /src/reporting/index.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import kleur from "kleur"; 3 | 4 | // Import Internal Dependencies 5 | import * as utils from "../utils/index.js"; 6 | import * as localStorage from "../localStorage.js"; 7 | 8 | // Import Reporters 9 | import { HTML, type HTMLReportData } from "./html.js"; 10 | import { PDF } from "./pdf.js"; 11 | 12 | export async function proceed( 13 | data: HTMLReportData, 14 | verbose = true 15 | ): Promise { 16 | const reportHTMLPath = await utils.runInSpinner( 17 | { 18 | title: `[Reporter: ${kleur.yellow().bold("HTML")}]`, 19 | start: "Building template and assets", 20 | verbose 21 | }, 22 | async() => HTML(data) 23 | ); 24 | 25 | const { reporters = [], title } = localStorage.getConfig().report!; 26 | if (!reporters.includes("pdf")) { 27 | return; 28 | } 29 | 30 | await utils.runInSpinner( 31 | { 32 | title: `[Reporter: ${kleur.yellow().bold("PDF")}]`, 33 | start: "Using puppeteer to convert HTML content to PDF", 34 | verbose 35 | }, 36 | async() => PDF(reportHTMLPath, { title }) 37 | ); 38 | } 39 | 40 | export { HTML, PDF }; 41 | -------------------------------------------------------------------------------- /src/reporting/pdf.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | import { pathToFileURL } from "node:url"; 4 | 5 | // Import Third-party Dependencies 6 | import puppeteer from "puppeteer"; 7 | 8 | // Import Internal Dependencies 9 | import * as CONSTANTS from "../constants.js"; 10 | import * as utils from "../utils/index.js"; 11 | 12 | export interface PDFReportOptions { 13 | title: string; 14 | saveOnDisk?: boolean; 15 | reportOutputLocation?: string; 16 | } 17 | 18 | export async function PDF( 19 | reportHTMLPath: string, 20 | options: PDFReportOptions 21 | ): Promise { 22 | const { 23 | title, 24 | saveOnDisk = true, 25 | reportOutputLocation = CONSTANTS.DIRS.REPORTS 26 | } = options; 27 | 28 | const browser = await puppeteer.launch({ 29 | args: ["--no-sandbox", "--disable-setuid-sandbox"] 30 | }); 31 | const page = await browser.newPage(); 32 | 33 | try { 34 | await page.emulateMediaType("print"); 35 | await page.goto(pathToFileURL(reportHTMLPath).href, { 36 | waitUntil: "networkidle0", 37 | timeout: 20_000 38 | }); 39 | 40 | const reportPath = saveOnDisk ? path.join( 41 | reportOutputLocation, 42 | utils.cleanReportName(title, ".pdf") 43 | ) : undefined; 44 | const pdfUint8Array = await page.pdf({ 45 | path: reportPath, 46 | format: "A4", 47 | printBackground: true 48 | }); 49 | 50 | return saveOnDisk ? 51 | (reportPath as string) : 52 | Buffer.from(pdfUint8Array); 53 | } 54 | finally { 55 | await page.close(); 56 | await browser.close(); 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /src/reporting/template.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | import { readFileSync } from "node:fs"; 4 | 5 | // Import Third-party Dependencies 6 | import compile from "zup"; 7 | import type { RC } from "@nodesecure/rc"; 8 | 9 | // Import Internal Dependencies 10 | import * as utils from "../utils/index.js"; 11 | import * as CONSTANTS from "../constants.js"; 12 | import * as localStorage from "../localStorage.js"; 13 | import type { ReportStat } from "../analysis/extractScannerData.js"; 14 | 15 | const kHTMLStaticTemplate = readFileSync( 16 | path.join(CONSTANTS.DIRS.VIEWS, "template.html"), 17 | "utf8" 18 | ); 19 | 20 | export interface HTMLTemplateGeneratorPayload { 21 | report_theme: string; 22 | report_title: string; 23 | report_logo: string | undefined; 24 | report_date: string; 25 | npm_stats: ReportStat | null; 26 | git_stats: ReportStat | null; 27 | charts: any[]; 28 | } 29 | 30 | export interface HTMLTemplateGenerationRenderOptions { 31 | asset_location?: string; 32 | } 33 | 34 | export class HTMLTemplateGenerator { 35 | public payload: HTMLTemplateGeneratorPayload; 36 | public config: RC["report"] | null; 37 | 38 | constructor( 39 | payload: HTMLTemplateGeneratorPayload, 40 | config: RC["report"] | null = null 41 | ) { 42 | this.payload = payload; 43 | this.config = config; 44 | } 45 | 46 | render( 47 | options: HTMLTemplateGenerationRenderOptions = {} 48 | ) { 49 | const { asset_location = "../dist" } = options; 50 | 51 | const config = this.config ?? localStorage.getConfig().report; 52 | const compiledTemplate = compile(kHTMLStaticTemplate); 53 | 54 | const html = compiledTemplate({ 55 | ...this.payload, 56 | asset_location 57 | }) as string; 58 | 59 | const charts = [ 60 | ...utils.generateChartArray( 61 | this.payload.npm_stats, 62 | this.payload.git_stats, 63 | config 64 | ) 65 | ].join("\n"); 66 | 67 | return html 68 | .concat(`\n`); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/utils/charts.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import { taggedString } from "@nodesecure/utils"; 3 | import type { RC } from "@nodesecure/rc"; 4 | 5 | // Import Internal Dependencies 6 | import type { ReportStat } from "../analysis/extractScannerData.js"; 7 | 8 | // CONSTANTS 9 | const kChartTemplate = taggedString`\tcreateChart("${0}", "${4}", { labels: [${1}], interpolate: ${3}, data: [${2}] });`; 10 | 11 | // eslint-disable-next-line max-params 12 | function toChart( 13 | baliseName: string, 14 | data: object, 15 | interpolateName: string | undefined, 16 | type = "bar" 17 | ) { 18 | const graphLabels = Object 19 | .keys(data) 20 | .map((key) => `"${key}"`) 21 | .join(","); 22 | 23 | return kChartTemplate( 24 | baliseName, 25 | graphLabels, 26 | Object.values(data).join(","), 27 | interpolateName!, 28 | type 29 | ); 30 | } 31 | 32 | export function* generateChartArray( 33 | pkgStats: ReportStat | null, 34 | repoStats: ReportStat | null, 35 | config: RC["report"] 36 | ) { 37 | const displayableCharts = config?.charts?.filter((chart) => chart.display) ?? []; 38 | 39 | if (pkgStats !== null) { 40 | for (const chart of displayableCharts) { 41 | const name = chart.name.toLowerCase(); 42 | 43 | yield toChart( 44 | `npm_${name}_canvas`, 45 | pkgStats[name], 46 | chart.interpolation, 47 | chart.type 48 | ); 49 | } 50 | } 51 | if (repoStats !== null) { 52 | for (const chart of displayableCharts) { 53 | const name = chart.name.toLowerCase(); 54 | 55 | yield toChart( 56 | `git_${name}_canvas`, 57 | repoStats[name], 58 | chart.interpolation, 59 | chart.type 60 | ); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/cleanReportName.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | 4 | // Import Third-party Dependencies 5 | import filenamify from "filenamify"; 6 | 7 | export function cleanReportName( 8 | name: string, 9 | extension: string | null = null 10 | ): string { 11 | const cleanName = filenamify(name); 12 | if (extension === null) { 13 | return cleanName; 14 | } 15 | 16 | return path.extname(cleanName) === extension ? 17 | cleanName : 18 | `${cleanName}${extension}`; 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/cloneGITRepository.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { execFile } from "node:child_process"; 3 | import { promisify } from "node:util"; 4 | 5 | const execFilePromise = promisify(execFile); 6 | 7 | export async function cloneGITRepository( 8 | dir: string, 9 | url: string 10 | ): Promise { 11 | const oauthUrl = url.replace("https://", `https://oauth2:${process.env.GIT_TOKEN}@`); 12 | 13 | await execFilePromise("git", ["clone", oauthUrl, dir]); 14 | 15 | return dir; 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/formatNpmPackages.ts: -------------------------------------------------------------------------------- 1 | export function formatNpmPackages( 2 | organizationPrefix: string, 3 | packages: string[] 4 | ): string[] { 5 | if (organizationPrefix === "") { 6 | return packages; 7 | } 8 | 9 | // in case the user has already added the organization prefix 10 | return packages.map((pkg) => (pkg.startsWith(organizationPrefix) ? 11 | pkg : 12 | `${organizationPrefix}/${pkg}`) 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cleanReportName.js"; 2 | export * from "./cloneGITRepository.js"; 3 | export * from "./runInSpinner.js"; 4 | export * from "./formatNpmPackages.js"; 5 | export * from "./charts.js"; 6 | -------------------------------------------------------------------------------- /src/utils/runInSpinner.ts: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import { Spinner } from "@topcli/spinner"; 3 | import kleur from "kleur"; 4 | 5 | export interface RunInSpinnerOptions { 6 | title: string; 7 | start: string; 8 | verbose: boolean; 9 | } 10 | 11 | export type RunInSpinnerHandler = (spinner: Spinner) => Promise; 12 | 13 | export async function runInSpinner( 14 | options: RunInSpinnerOptions, 15 | asyncHandler: RunInSpinnerHandler 16 | ): Promise { 17 | const { title, verbose = true, start = void 0 } = options; 18 | 19 | const spinner = new Spinner({ verbose }) 20 | .start(start, { withPrefix: `${kleur.gray().bold(title)} - ` }); 21 | 22 | try { 23 | const response = await asyncHandler(spinner); 24 | 25 | const elapsed = `${spinner.elapsedTime.toFixed(2)}ms`; 26 | spinner.succeed(kleur.white().bold(`successfully executed in ${kleur.green().bold(elapsed)}`)); 27 | 28 | return response; 29 | } 30 | catch (err: any) { 31 | spinner.failed(err.message); 32 | 33 | throw err; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/api/report.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | import os from "node:os"; 4 | import fs from "node:fs/promises"; 5 | import fsSync from "node:fs"; 6 | import { describe, test } from "node:test"; 7 | import assert from "node:assert"; 8 | 9 | // Import Third-party Dependencies 10 | import { from } from "@nodesecure/scanner"; 11 | 12 | // Import Internal Dependencies 13 | import { report } from "../../src/index.js"; 14 | 15 | // CONSTANTS 16 | const kReportPayload = { 17 | title: "test_runner", 18 | theme: "light" as const, 19 | includeTransitiveInternal: false, 20 | npm: { 21 | organizationPrefix: "@nodesecure", 22 | packages: [] 23 | }, 24 | reporters: [ 25 | "pdf" as const 26 | ], 27 | charts: [ 28 | { 29 | name: "Extensions" as const, 30 | display: true, 31 | interpolation: "d3.interpolateRainbow", 32 | type: "bar" as const 33 | }, 34 | { 35 | name: "Licenses" as const, 36 | display: true, 37 | interpolation: "d3.interpolateCool", 38 | type: "bar" as const 39 | }, 40 | { 41 | name: "Warnings" as const, 42 | display: true, 43 | type: "horizontalBar" as const, 44 | interpolation: "d3.interpolateInferno" 45 | }, 46 | { 47 | name: "Flags" as const, 48 | display: true, 49 | type: "horizontalBar" as const, 50 | interpolation: "d3.interpolateSinebow" 51 | } 52 | ] 53 | }; 54 | 55 | describe("(API) report", { concurrency: 1 }, () => { 56 | test("it should successfully generate a PDF and should not save PDF or HTML", async() => { 57 | const reportOutputLocation = await fs.mkdtemp( 58 | path.join(os.tmpdir(), "test-runner-report-pdf-") 59 | ); 60 | 61 | const payload = await from("sade"); 62 | 63 | const generatedPDF = await report( 64 | payload.dependencies, 65 | structuredClone(kReportPayload), 66 | { reportOutputLocation } 67 | ); 68 | try { 69 | assert.ok(Buffer.isBuffer(generatedPDF)); 70 | assert.ok(isPDF(generatedPDF)); 71 | 72 | const files = (await fs.readdir(reportOutputLocation, { withFileTypes: true })) 73 | .flatMap((dirent) => (dirent.isFile() ? [dirent.name] : [])); 74 | assert.deepEqual( 75 | files, 76 | [] 77 | ); 78 | } 79 | finally { 80 | await fs.rm(reportOutputLocation, { force: true, recursive: true }); 81 | } 82 | }); 83 | 84 | test("should save HTML when saveHTMLOnDisk is truthy", async() => { 85 | const reportOutputLocation = await fs.mkdtemp( 86 | path.join(os.tmpdir(), "test-runner-report-pdf-") 87 | ); 88 | 89 | const payload = await from("sade"); 90 | 91 | const generatedPDF = await report( 92 | payload.dependencies, 93 | { 94 | ...kReportPayload, 95 | reporters: ["pdf", "html"] 96 | }, 97 | { reportOutputLocation, saveHTMLOnDisk: true } 98 | ); 99 | try { 100 | assert.ok(Buffer.isBuffer(generatedPDF)); 101 | assert.ok(isPDF(generatedPDF)); 102 | 103 | const files = (await fs.readdir(reportOutputLocation, { withFileTypes: true })) 104 | .flatMap((dirent) => (dirent.isFile() ? [dirent.name] : [])); 105 | assert.deepEqual( 106 | files, 107 | ["test_runner.html"] 108 | ); 109 | } 110 | finally { 111 | await fs.rm(reportOutputLocation, { force: true, recursive: true }); 112 | } 113 | }); 114 | 115 | test("should save PDF when savePDFOnDisk is truthy", async() => { 116 | const reportOutputLocation = await fs.mkdtemp( 117 | path.join(os.tmpdir(), "test-runner-report-pdf-") 118 | ); 119 | 120 | const payload = await from("sade"); 121 | 122 | const generatedPDFPath = await report( 123 | payload.dependencies, 124 | { 125 | ...kReportPayload, 126 | reporters: ["pdf", "html"] 127 | }, 128 | { reportOutputLocation, savePDFOnDisk: true } 129 | ); 130 | try { 131 | assert.ok(typeof generatedPDFPath === "string"); 132 | assert.ok(fsSync.existsSync(generatedPDFPath), "when saving PDF, we return the path to the PDF instead of the buffer"); 133 | 134 | const files = (await fs.readdir(reportOutputLocation, { withFileTypes: true })) 135 | .flatMap((dirent) => (dirent.isFile() ? [dirent.name] : [])); 136 | assert.deepEqual( 137 | files, 138 | ["test_runner.pdf"] 139 | ); 140 | } 141 | finally { 142 | await fs.rm(reportOutputLocation, { force: true, recursive: true }); 143 | } 144 | }); 145 | 146 | test("should throw when no reporter is enabled", async() => { 147 | const payload = await from("sade"); 148 | 149 | await assert.rejects( 150 | () => report(payload.dependencies, { ...kReportPayload, reporters: [] }), 151 | { message: "At least one reporter must be enabled (pdf or html)" } 152 | ); 153 | }); 154 | }); 155 | 156 | function isPDF(buf) { 157 | return ( 158 | Buffer.isBuffer(buf) && buf.lastIndexOf("%PDF-") === 0 && buf.lastIndexOf("%%EOF") > -1 159 | ); 160 | } 161 | -------------------------------------------------------------------------------- /test/commands/execute.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { fileURLToPath } from "node:url"; 3 | import path from "node:path"; 4 | import fs from "node:fs/promises"; 5 | import { afterEach, describe, it } from "node:test"; 6 | import assert from "node:assert"; 7 | import { stripVTControlCharacters } from "node:util"; 8 | 9 | // Import Internal Dependencies 10 | import { filterProcessStdout } from "../helpers/reportCommandRunner.js"; 11 | import * as CONSTANTS from "../../src/constants.js"; 12 | 13 | // CONSTANTS 14 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 15 | const kProcessDir = path.join(__dirname, "../.."); 16 | 17 | describe("Report execute command", () => { 18 | afterEach(async() => await fs.rm(CONSTANTS.DIRS.CLONES, { 19 | recursive: true, force: true 20 | })); 21 | 22 | it("should execute command on fixture '.nodesecurerc'", async() => { 23 | const options = { 24 | cmd: "node", 25 | args: ["dist/bin/index.js", "execute"], 26 | cwd: kProcessDir 27 | }; 28 | 29 | function byMessage(buffer) { 30 | const message = ".*"; 31 | const afterNonAlphaNum = String.raw`?<=[^a-zA-Z\d\s:]\s`; 32 | const beforeTime = String.raw`?=\s\d{1,5}.\d{1,4}ms`; 33 | const withoutDuplicates = String.raw`(?![\s\S]*\1)`; 34 | 35 | const matchMessage = `(${afterNonAlphaNum})(${message})(${beforeTime})|(${afterNonAlphaNum})(${message})`; 36 | const reg = new RegExp(`(${matchMessage})${withoutDuplicates}`, "g"); 37 | 38 | const matchedMessages = stripVTControlCharacters(buffer.toString()).match(reg); 39 | 40 | return matchedMessages ?? [""]; 41 | } 42 | 43 | const expectedLines = [ 44 | `Executing nreport at: ${kProcessDir}`, 45 | "title: Default report title", 46 | "reporters: html,pdf", 47 | "[Fetcher: NPM] - Fetching NPM packages metadata on the NPM Registry", 48 | "", 49 | "[Fetcher: NPM] - successfully executed in", 50 | "[Fetcher: GIT] - Cloning GIT repositories", 51 | "[Fetcher: GIT] - Fetching repositories metadata on the NPM Registry", 52 | "[Fetcher: GIT] - successfully executed in", 53 | "[Reporter: HTML] - Building template and assets", 54 | "[Reporter: HTML] - successfully executed in", 55 | "[Reporter: PDF] - Using puppeteer to convert HTML content to PDF", 56 | "[Reporter: PDF] - successfully executed in", 57 | "Security report successfully generated! Enjoy 🚀." 58 | ]; 59 | 60 | const actualLines = await filterProcessStdout(options, byMessage); 61 | assert.deepEqual(actualLines, expectedLines, "we are expecting these lines"); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/commands/initialize.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { fileURLToPath } from "node:url"; 3 | import fs from "node:fs"; 4 | import os from "node:os"; 5 | import path from "node:path"; 6 | import { before, describe, it } from "node:test"; 7 | import assert from "node:assert"; 8 | import { stripVTControlCharacters } from "node:util"; 9 | 10 | // Import Internal Dependencies 11 | import { runProcess } from "../helpers/reportCommandRunner.js"; 12 | 13 | // CONSTANTS 14 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 15 | const kBinDir = path.join(__dirname, "../..", "dist/bin/index.js"); 16 | const kProcessDir = os.tmpdir(); 17 | const kConfigFilePath = path.join(kProcessDir, ".nodesecurerc"); 18 | 19 | describe("Report init command if config does not exists", () => { 20 | before(() => { 21 | if (fs.existsSync(kConfigFilePath)) { 22 | fs.unlinkSync(kConfigFilePath); 23 | } 24 | }); 25 | 26 | it("should create config if not exists", async() => { 27 | const lines = [ 28 | /.*/, 29 | / > Executing nreport at: .*$/, 30 | /.*/, 31 | /Successfully generated NodeSecure runtime configuration at current location/, 32 | /.*/ 33 | ]; 34 | 35 | const processOptions = { 36 | cmd: "node", 37 | args: [kBinDir, "initialize"], 38 | cwd: kProcessDir 39 | }; 40 | 41 | for await (const line of runProcess(processOptions)) { 42 | const regexp = lines.shift(); 43 | assert.ok(regexp, "we are expecting this line"); 44 | assert.ok(regexp.test(stripVTControlCharacters(line)), `line (${line}) matches ${regexp}`); 45 | } 46 | 47 | // to prevent false positive if no lines have been emitted from process 48 | assert.equal(lines.length, 0); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/fixtures/.nodesecurerc: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "i18n": "english", 4 | "strategy": "github-advisory", 5 | "registry": "https://registry.npmjs.org", 6 | "report": { 7 | "theme": "light", 8 | "includeTransitiveInternal": false, 9 | "reporters": [ 10 | "html", 11 | "pdf" 12 | ], 13 | "charts": [ 14 | { 15 | "name": "Extensions", 16 | "display": true, 17 | "interpolation": "d3.interpolateRainbow", 18 | "type": "bar" 19 | }, 20 | { 21 | "name": "Licenses", 22 | "display": true, 23 | "interpolation": "d3.interpolateCool", 24 | "type": "bar" 25 | }, 26 | { 27 | "name": "Warnings", 28 | "display": true, 29 | "type": "horizontalBar", 30 | "interpolation": "d3.interpolateInferno" 31 | }, 32 | { 33 | "name": "Flags", 34 | "display": true, 35 | "type": "horizontalBar", 36 | "interpolation": "d3.interpolateSinebow" 37 | } 38 | ], 39 | "title": "Default report title", 40 | "showFlags": true 41 | } 42 | } -------------------------------------------------------------------------------- /test/helpers/reportCommandRunner.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { ChildProcess, spawn } from "node:child_process"; 3 | import { createInterface } from "node:readline"; 4 | import { stripVTControlCharacters } from "node:util"; 5 | 6 | export async function* runProcess(options) { 7 | const childProcess = spawnedProcess(options); 8 | try { 9 | if (!childProcess.stdout) { 10 | return; 11 | } 12 | 13 | const rStream = createInterface(childProcess.stdout); 14 | 15 | for await (const line of rStream) { 16 | yield stripVTControlCharacters(line); 17 | } 18 | } 19 | finally { 20 | childProcess.kill(); 21 | } 22 | } 23 | 24 | export function filterProcessStdout(options, filter): Promise { 25 | const { resolve, reject, promise } = Promise.withResolvers(); 26 | 27 | const childProcess = spawnedProcess(options); 28 | const output = new Set(); 29 | 30 | childProcess.stdout?.on("data", (buffer) => { 31 | filter(buffer).forEach((filteredData) => { 32 | output.add(filteredData); 33 | }); 34 | }); 35 | 36 | childProcess.on("close", () => { 37 | resolve(Array.from(output)); 38 | }); 39 | 40 | childProcess.on("error", (err) => { 41 | reject(err); 42 | }); 43 | 44 | return promise; 45 | } 46 | 47 | function spawnedProcess(options): ChildProcess { 48 | const { cmd, args = [], cwd = process.cwd() } = options; 49 | 50 | return spawn(cmd, args, { 51 | stdio: ["ignore", "pipe", "pipe", "ipc"], 52 | cwd 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /test/utils/cleanReportName.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { describe, it } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { cleanReportName } from "../../src/utils/index.js"; 7 | 8 | describe("cleanReportName", () => { 9 | it("should remove invalid Windows characters", () => { 10 | const invalidStr = ""; 11 | 12 | assert.strictEqual( 13 | cleanReportName(invalidStr), 14 | "!foo!bar!" 15 | ); 16 | }); 17 | 18 | it("should add the extension if it's missing from the input", () => { 19 | const fileName = "foo*bar"; 20 | 21 | assert.strictEqual( 22 | cleanReportName(fileName, ".png"), 23 | "foo!bar.png" 24 | ); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/utils/cloneGITRepository.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { describe, it } from "node:test"; 3 | import fs from "node:fs/promises"; 4 | import os from "node:os"; 5 | import path from "node:path"; 6 | import assert from "node:assert"; 7 | 8 | // Import Internal Dependencies 9 | import { cloneGITRepository } from "../../src/utils/index.js"; 10 | 11 | describe("cloneGITRepository", () => { 12 | it("should clone a remote GIT repository", async() => { 13 | const dest = await fs.mkdtemp( 14 | path.join(os.tmpdir(), "nsecure-report-git-") 15 | ); 16 | 17 | try { 18 | const dir = await cloneGITRepository( 19 | dest, 20 | "https://github.com/NodeSecure/Governance.git" 21 | ); 22 | 23 | assert.strictEqual(dir, dest); 24 | const files = (await fs.readdir(dest, { withFileTypes: true })) 25 | .flatMap((dirent) => (dirent.isFile() ? [dirent.name] : [])); 26 | 27 | assert.ok(files.includes("CODE_OF_CONDUCT.md")); 28 | assert.ok(files.includes("README.md")); 29 | } 30 | finally { 31 | await fs.rm(dest, { force: true, recursive: true }); 32 | } 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/utils/formatNpmPackages.spec.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { describe, test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { formatNpmPackages } from "../../src/utils/index.js"; 7 | 8 | describe("formatNpmPackages", () => { 9 | test("If no organizationPrefix is provided, it should return the packages list as is.", () => { 10 | const packages = [ 11 | "@nodesecure/js-x-ray" 12 | ]; 13 | const formatedPackages = formatNpmPackages("", packages); 14 | 15 | assert.strictEqual( 16 | formatedPackages, 17 | packages 18 | ); 19 | }); 20 | 21 | test(`Given an organizationPrefix, it must add the prefix to the packages where it is missing 22 | and ignore those who already have the prefix.`, () => { 23 | const packages = [ 24 | "@nodesecure/js-x-ray", 25 | "scanner" 26 | ]; 27 | const formatedPackages = formatNpmPackages("@nodesecure", packages); 28 | 29 | assert.notStrictEqual( 30 | formatedPackages, 31 | packages 32 | ); 33 | assert.deepEqual( 34 | formatedPackages, 35 | [ 36 | "@nodesecure/js-x-ray", 37 | "@nodesecure/scanner" 38 | ] 39 | ); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@openally/config.typescript", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src", 8 | "bin", 9 | "package.json" 10 | ], 11 | "exclude": ["node_modules", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /views/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | Security 21 | 22 |
23 |
24 | [[ if (z.report_logo) { ]] 25 | 28 | [[ } ]] 29 |

[[=z.report_title]]

30 |

[[=z.report_date]]

31 |
32 | 33 |
34 |

NPM Packages Statistics

35 |
36 |
37 |
38 | 39 |

Internal size

40 | [[=z.npm_stats.size.internal]] 41 |
42 |
43 | 44 |

Third size

45 | [[=z.npm_stats.size.external]] 46 |
47 |
48 | 49 |

All size

50 | [[=z.npm_stats.size.all]] 51 |
52 |
53 |
54 |
55 |
56 |
57 | 58 |

[[=z.npm_stats.packages_count.internal]]Internal Dependencies

59 |
60 |
    61 | [[ for (const [name, option] of Object.entries(z.npm_stats.packages)) { ]] 62 | [[ if (!option.isThird) { ]] 63 |
  • 64 | 65 | [[=name]] 66 | 67 | [[ if (z.npm_stats.showFlags) { ]] 68 | [[ for (const flag of Object.values(option.flags)) {]] 69 | 70 | [[=flag.emoji]] 71 | 72 | [[ } ]] 73 | [[ } ]] 74 |
  • 75 | [[ } ]] 76 | [[ } ]] 77 |
78 |
79 |
80 |
81 | 82 |

[[=z.npm_stats.packages_count.external]]Third-party Dependencies

83 |
84 |
    85 | [[ for (const [name, option] of Object.entries(z.npm_stats.packages)) { ]] 86 | [[ if (option.isThird) { ]] 87 |
  • 88 | 89 | [[=name]] 90 | 91 | [[ if (z.npm_stats.showFlags) { ]] 92 | [[ for (const flag of Object.values(option.flags)) {]] 93 | 94 | [[=flag.emoji]] 95 | 96 | [[ } ]] 97 | [[ } ]] 98 |
  • 99 | [[ } ]] 100 | [[ } ]] 101 |
102 |
103 | 104 | [[ if (z.npm_stats.showFlags) { ]] 105 | 116 | [[ } ]] 117 |
118 |
119 |
120 |
121 | 122 |

[[=Object.keys(z.npm_stats.deps.transitive).length]]Transitive Dependencies

123 |
124 |
    125 | [[ for (const [name, { links }] of Object.entries(z.npm_stats.deps.transitive)) { ]] 126 |
  • 127 | 128 | [[=name]] 129 | 130 |
  • 131 | [[ } ]] 132 |
133 |
134 |
135 |
136 | 137 |

[[=Object.keys(z.npm_stats.deps.node).length]]Node.js Core Dependencies

138 |
139 |
    140 | [[ for (const [name, { visualizerUrl }] of Object.entries(z.npm_stats.deps.node)) { ]] 141 |
  • 142 | 143 | [[=name]] 144 | 145 |
  • 146 | [[ } ]] 147 |
148 |
149 |
150 |
151 |
152 |
153 | 154 |

Authors & Maintainers

155 |
156 |
157 | [[ for (const [email, count] of Object.entries(z.npm_stats.authors)) { ]] 158 |
159 | 160 |

[[=count]]

161 |
162 | [[ } ]] 163 |
164 |
165 |
166 | [[ if (Object.keys(z.npm_stats.scorecards).length > 0) { ]] 167 |
168 |
169 |
170 |

Scorecards

171 |
172 | 182 |
183 |
184 | [[ } ]] 185 | [[ for (const { name, help } of z.charts) { ]] 186 |
187 |
188 |
189 |

[[=name]]

190 |
191 | 192 |
193 |
194 | [[ if (help !== null) { ]] 195 |
196 | 197 |

[[=help]]

198 |
199 | [[ } ]] 200 | [[ } ]] 201 |
202 | [[ if (z.git_stats !== null ) { ]] 203 |
204 |

GIT Repositories Statistics

205 |
206 |
207 |
208 | 209 |

Internal Size

210 | [[=z.git_stats.size.internal]] 211 |
212 |
213 | 214 |

Third Size

215 | [[=z.git_stats.size.external]] 216 |
217 |
218 | 219 |

All Size

220 | [[=z.git_stats.size.all]] 221 |
222 |
223 |
224 |
225 |
226 |
227 | 228 |

[[=z.git_stats.packages_count.internal]]Internal Dependencies

229 |
230 |
    231 | [[ for (const [name, option] of Object.entries(z.git_stats.packages)) { ]] 232 | [[ if (!option.isThird) { ]] 233 |
  • 234 | 235 | [[=name]] 236 | 237 | [[ if (z.git_stats.showFlags) { ]] 238 | [[ for (const flag of Object.values(option.flags)) {]] 239 | 240 | [[=flag.emoji]] 241 | 242 | [[ } ]] 243 | [[ } ]] 244 |
  • 245 | [[ } ]] 246 | [[ } ]] 247 |
248 |
249 |
250 |
251 | 252 |

[[=z.git_stats.packages_count.external]]Third-party Dependencies

253 |
254 |
    255 | [[ for (const [name, option] of Object.entries(z.git_stats.packages)) { ]] 256 | [[ if (option.isThird) { ]] 257 |
  • 258 | 259 | [[=name]] 260 | 261 | [[ if (z.git_stats.showFlags) { ]] 262 | [[ for (const flag of Object.values(option.flags)) {]] 263 | 264 | [[=flag.emoji]] 265 | 266 | [[ } ]] 267 | [[ } ]] 268 |
  • 269 | [[ } ]] 270 | [[ } ]] 271 |
272 |
273 | 274 | [[ if (z.git_stats.showFlags) { ]] 275 | 286 | [[ } ]] 287 |
288 |
289 |
290 |
291 | 292 |

[[=Object.keys(z.git_stats.deps.transitive).length]]Transitive Dependencies

293 |
294 |
    295 | [[ for (const [name, { links }] of Object.entries(z.git_stats.deps.transitive)) { ]] 296 |
  • 297 | 298 | [[=name]] 299 | 300 |
  • 301 | [[ } ]] 302 |
303 |
304 |
305 |
306 | 307 |

[[=Object.keys(z.git_stats.deps.node).length]]Node.js Core Dependencies

308 |
309 |
    310 | [[ for (const [name, { visualizerUrl }] of Object.entries(z.git_stats.deps.node)) { ]] 311 |
  • 312 | 313 | [[=name]] 314 | 315 |
  • 316 | [[ } ]] 317 |
318 |
319 |
320 |
321 |
322 |
323 | 324 |

Authors & Maintainers

325 |
326 |
327 | [[ for (const [email, count] of Object.entries(z.git_stats.authors)) { ]] 328 |
329 | 330 |

[[=count]]

331 |
332 | [[ } ]] 333 |
334 |
335 |
336 | [[ if (Object.keys(z.git_stats.scorecards).length > 0) { ]] 337 |
338 |
339 |
340 |

Scorecards

341 |
342 | 352 |
353 |
354 | [[ } ]] 355 | [[ for (const { name, help } of z.charts) { ]] 356 |
357 |
358 |
359 |

[[=name]]

360 |
361 | 362 |
363 |
364 | [[ if (help !== null) { ]] 365 |
366 | 367 |

[[=help]]

368 |
369 | [[ } ]] 370 | [[ } ]] 371 |
372 | [[ } ]] 373 |
374 | --------------------------------------------------------------------------------