├── .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 |
3 |
4 |
5 |
6 |
7 | 
8 | [](https://api.securityscorecards.dev/projects/github.com/NodeSecure/report)
10 | 
11 | 
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 | |  |  |
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 | [](#contributors-)
254 |
255 |
256 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
257 |
258 |
259 |
260 |
261 |
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 |
26 |

27 |
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 |
106 |
107 |
108 | [[ for (const flag of Object.keys(z.npm_stats.flags)) { ]]
109 | -
110 | [[=z.npm_stats.flagsList[flag].emoji]]
111 | [[=z.npm_stats.flagsList[flag].tooltipDescription]]
112 |
113 | [[ } ]]
114 |
115 |
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 |
276 |
277 |
278 | [[ for (const flag of Object.keys(z.git_stats.flags)) { ]]
279 | -
280 | [[=z.git_stats.flagsList[flag].emoji]]
281 | [[=z.git_stats.flagsList[flag].tooltipDescription]]
282 |
283 | [[ } ]]
284 |
285 |
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 |
--------------------------------------------------------------------------------